##// END OF EJS Templates
Merged r15879 (#23969)....
Jean-Philippe Lang -
r15507:52b19aca3c4d
parent child
Show More

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

@@ -1,1715 +1,1720
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 include Redmine::Utils::DateCalculation
21 21 include Redmine::I18n
22 22 before_save :set_parent_id
23 23 include Redmine::NestedSet::IssueNestedSet
24 24
25 25 belongs_to :project
26 26 belongs_to :tracker
27 27 belongs_to :status, :class_name => 'IssueStatus'
28 28 belongs_to :author, :class_name => 'User'
29 29 belongs_to :assigned_to, :class_name => 'Principal'
30 30 belongs_to :fixed_version, :class_name => 'Version'
31 31 belongs_to :priority, :class_name => 'IssuePriority'
32 32 belongs_to :category, :class_name => 'IssueCategory'
33 33
34 34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
35 35 has_many :visible_journals,
36 36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
37 37 :class_name => 'Journal',
38 38 :as => :journalized
39 39
40 40 has_many :time_entries, :dependent => :destroy
41 41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
42 42
43 43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45 45
46 46 acts_as_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"],
50 50 :preload => [:project, :status, :tracker],
51 51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
52 52
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 :scope => preload(:project, :author, :tracker, :status),
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, :project, :tracker
66 66 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
67 67 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
68 68 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
69 69
70 70 validates_length_of :subject, :maximum => 255
71 71 validates_inclusion_of :done_ratio, :in => 0..100
72 72 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73 73 validates :start_date, :date => true
74 74 validates :due_date, :date => true
75 75 validate :validate_issue, :validate_required_fields
76 76 attr_protected :id
77 77
78 78 scope :visible, lambda {|*args|
79 79 joins(:project).
80 80 where(Issue.visible_condition(args.shift || User.current, *args))
81 81 }
82 82
83 83 scope :open, lambda {|*args|
84 84 is_closed = args.size > 0 ? !args.first : false
85 85 joins(:status).
86 86 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
87 87 }
88 88
89 89 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
90 90 scope :on_active_project, lambda {
91 91 joins(:project).
92 92 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
93 93 }
94 94 scope :fixed_version, lambda {|versions|
95 95 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
96 96 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
97 97 }
98 98 scope :assigned_to, lambda {|arg|
99 99 arg = Array(arg).uniq
100 100 ids = arg.map {|p| p.is_a?(Principal) ? p.id : p}
101 101 ids += arg.select {|p| p.is_a?(User)}.map(&:group_ids).flatten.uniq
102 102 ids.compact!
103 103 ids.any? ? where(:assigned_to_id => ids) : none
104 104 }
105 105
106 106 before_validation :clear_disabled_fields
107 107 before_create :default_assign
108 108 before_save :close_duplicates, :update_done_ratio_from_issue_status,
109 109 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
110 110 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
111 111 after_save :reschedule_following_issues, :update_nested_set_attributes,
112 112 :update_parent_attributes, :create_journal
113 113 # Should be after_create but would be called before previous after_save callbacks
114 114 after_save :after_create_from_copy
115 115 after_destroy :update_parent_attributes
116 116 after_create :send_notification
117 117 # Keep it at the end of after_save callbacks
118 118 after_save :clear_assigned_to_was
119 119
120 120 # Returns a SQL conditions string used to find all issues visible by the specified user
121 121 def self.visible_condition(user, options={})
122 122 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
123 123 sql = if user.id && user.logged?
124 124 case role.issues_visibility
125 125 when 'all'
126 126 '1=1'
127 127 when 'default'
128 128 user_ids = [user.id] + user.groups.map(&:id).compact
129 129 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
130 130 when 'own'
131 131 user_ids = [user.id] + user.groups.map(&:id).compact
132 132 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
133 133 else
134 134 '1=0'
135 135 end
136 136 else
137 137 "(#{table_name}.is_private = #{connection.quoted_false})"
138 138 end
139 139 unless role.permissions_all_trackers?(:view_issues)
140 140 tracker_ids = role.permissions_tracker_ids(:view_issues)
141 141 if tracker_ids.any?
142 142 sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))"
143 143 else
144 144 sql = '1=0'
145 145 end
146 146 end
147 147 sql
148 148 end
149 149 end
150 150
151 151 # Returns true if usr or current user is allowed to view the issue
152 152 def visible?(usr=nil)
153 153 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
154 154 visible = if user.logged?
155 155 case role.issues_visibility
156 156 when 'all'
157 157 true
158 158 when 'default'
159 159 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
160 160 when 'own'
161 161 self.author == user || user.is_or_belongs_to?(assigned_to)
162 162 else
163 163 false
164 164 end
165 165 else
166 166 !self.is_private?
167 167 end
168 168 unless role.permissions_all_trackers?(:view_issues)
169 169 visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id)
170 170 end
171 171 visible
172 172 end
173 173 end
174 174
175 175 # Returns true if user or current user is allowed to edit or add notes to the issue
176 176 def editable?(user=User.current)
177 177 attributes_editable?(user) || notes_addable?(user)
178 178 end
179 179
180 180 # Returns true if user or current user is allowed to edit the issue
181 181 def attributes_editable?(user=User.current)
182 182 user_tracker_permission?(user, :edit_issues)
183 183 end
184 184
185 185 # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable?
186 186 def attachments_editable?(user=User.current)
187 187 attributes_editable?(user)
188 188 end
189 189
190 190 # Returns true if user or current user is allowed to add notes to the issue
191 191 def notes_addable?(user=User.current)
192 192 user_tracker_permission?(user, :add_issue_notes)
193 193 end
194 194
195 195 # Returns true if user or current user is allowed to delete the issue
196 196 def deletable?(user=User.current)
197 197 user_tracker_permission?(user, :delete_issues)
198 198 end
199 199
200 200 def initialize(attributes=nil, *args)
201 201 super
202 202 if new_record?
203 203 # set default values for new records only
204 204 self.priority ||= IssuePriority.default
205 205 self.watcher_user_ids = []
206 206 end
207 207 end
208 208
209 209 def create_or_update
210 210 super
211 211 ensure
212 212 @status_was = nil
213 213 end
214 214 private :create_or_update
215 215
216 216 # AR#Persistence#destroy would raise and RecordNotFound exception
217 217 # if the issue was already deleted or updated (non matching lock_version).
218 218 # This is a problem when bulk deleting issues or deleting a project
219 219 # (because an issue may already be deleted if its parent was deleted
220 220 # first).
221 221 # The issue is reloaded by the nested_set before being deleted so
222 222 # the lock_version condition should not be an issue but we handle it.
223 223 def destroy
224 224 super
225 225 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
226 226 # Stale or already deleted
227 227 begin
228 228 reload
229 229 rescue ActiveRecord::RecordNotFound
230 230 # The issue was actually already deleted
231 231 @destroyed = true
232 232 return freeze
233 233 end
234 234 # The issue was stale, retry to destroy
235 235 super
236 236 end
237 237
238 238 alias :base_reload :reload
239 239 def reload(*args)
240 240 @workflow_rule_by_attribute = nil
241 241 @assignable_versions = nil
242 242 @relations = nil
243 243 @spent_hours = nil
244 244 @total_spent_hours = nil
245 245 @total_estimated_hours = nil
246 246 base_reload(*args)
247 247 end
248 248
249 249 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
250 250 def available_custom_fields
251 251 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
252 252 end
253 253
254 254 def visible_custom_field_values(user=nil)
255 255 user_real = user || User.current
256 256 custom_field_values.select do |value|
257 257 value.custom_field.visible_by?(project, user_real)
258 258 end
259 259 end
260 260
261 261 # Copies attributes from another issue, arg can be an id or an Issue
262 262 def copy_from(arg, options={})
263 263 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
264 264 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on", "closed_on")
265 265 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
266 266 self.status = issue.status
267 267 self.author = User.current
268 268 unless options[:attachments] == false
269 269 self.attachments = issue.attachments.map do |attachement|
270 270 attachement.copy(:container => self)
271 271 end
272 272 end
273 273 @copied_from = issue
274 274 @copy_options = options
275 275 self
276 276 end
277 277
278 278 # Returns an unsaved copy of the issue
279 279 def copy(attributes=nil, copy_options={})
280 280 copy = self.class.new.copy_from(self, copy_options)
281 281 copy.attributes = attributes if attributes
282 282 copy
283 283 end
284 284
285 285 # Returns true if the issue is a copy
286 286 def copy?
287 287 @copied_from.present?
288 288 end
289 289
290 290 def status_id=(status_id)
291 291 if status_id.to_s != self.status_id.to_s
292 292 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
293 293 end
294 294 self.status_id
295 295 end
296 296
297 297 # Sets the status.
298 298 def status=(status)
299 299 if status != self.status
300 300 @workflow_rule_by_attribute = nil
301 301 end
302 302 association(:status).writer(status)
303 303 end
304 304
305 305 def priority_id=(pid)
306 306 self.priority = nil
307 307 write_attribute(:priority_id, pid)
308 308 end
309 309
310 310 def category_id=(cid)
311 311 self.category = nil
312 312 write_attribute(:category_id, cid)
313 313 end
314 314
315 315 def fixed_version_id=(vid)
316 316 self.fixed_version = nil
317 317 write_attribute(:fixed_version_id, vid)
318 318 end
319 319
320 320 def tracker_id=(tracker_id)
321 321 if tracker_id.to_s != self.tracker_id.to_s
322 322 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
323 323 end
324 324 self.tracker_id
325 325 end
326 326
327 327 # Sets the tracker.
328 328 # This will set the status to the default status of the new tracker if:
329 329 # * the status was the default for the previous tracker
330 330 # * or if the status was not part of the new tracker statuses
331 331 # * or the status was nil
332 332 def tracker=(tracker)
333 333 tracker_was = self.tracker
334 334 association(:tracker).writer(tracker)
335 335 if tracker != tracker_was
336 336 if status == tracker_was.try(:default_status)
337 337 self.status = nil
338 338 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
339 339 self.status = nil
340 340 end
341 341 reassign_custom_field_values
342 342 @workflow_rule_by_attribute = nil
343 343 end
344 344 self.status ||= default_status
345 345 self.tracker
346 346 end
347 347
348 348 def project_id=(project_id)
349 349 if project_id.to_s != self.project_id.to_s
350 350 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
351 351 end
352 352 self.project_id
353 353 end
354 354
355 355 # Sets the project.
356 356 # Unless keep_tracker argument is set to true, this will change the tracker
357 357 # to the first tracker of the new project if the previous tracker is not part
358 358 # of the new project trackers.
359 359 # This will:
360 360 # * clear the fixed_version is it's no longer valid for the new project.
361 361 # * clear the parent issue if it's no longer valid for the new project.
362 362 # * set the category to the category with the same name in the new
363 363 # project if it exists, or clear it if it doesn't.
364 364 # * for new issue, set the fixed_version to the project default version
365 365 # if it's a valid fixed_version.
366 366 def project=(project, keep_tracker=false)
367 367 project_was = self.project
368 368 association(:project).writer(project)
369 369 if project_was && project && project_was != project
370 370 @assignable_versions = nil
371 371
372 372 unless keep_tracker || project.trackers.include?(tracker)
373 373 self.tracker = project.trackers.first
374 374 end
375 375 # Reassign to the category with same name if any
376 376 if category
377 377 self.category = project.issue_categories.find_by_name(category.name)
378 378 end
379 379 # Keep the fixed_version if it's still valid in the new_project
380 380 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
381 381 self.fixed_version = nil
382 382 end
383 383 # Clear the parent task if it's no longer valid
384 384 unless valid_parent_project?
385 385 self.parent_issue_id = nil
386 386 end
387 387 reassign_custom_field_values
388 388 @workflow_rule_by_attribute = nil
389 389 end
390 390 # Set fixed_version to the project default version if it's valid
391 391 if new_record? && fixed_version.nil? && project && project.default_version_id?
392 392 if project.shared_versions.open.exists?(project.default_version_id)
393 393 self.fixed_version_id = project.default_version_id
394 394 end
395 395 end
396 396 self.project
397 397 end
398 398
399 399 def description=(arg)
400 400 if arg.is_a?(String)
401 401 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
402 402 end
403 403 write_attribute(:description, arg)
404 404 end
405 405
406 406 # Overrides assign_attributes so that project and tracker get assigned first
407 407 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
408 408 return if new_attributes.nil?
409 409 attrs = new_attributes.dup
410 410 attrs.stringify_keys!
411 411
412 412 %w(project project_id tracker tracker_id).each do |attr|
413 413 if attrs.has_key?(attr)
414 414 send "#{attr}=", attrs.delete(attr)
415 415 end
416 416 end
417 417 send :assign_attributes_without_project_and_tracker_first, attrs, *args
418 418 end
419 419 # Do not redefine alias chain on reload (see #4838)
420 420 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
421 421
422 422 def attributes=(new_attributes)
423 423 assign_attributes new_attributes
424 424 end
425 425
426 426 def estimated_hours=(h)
427 427 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
428 428 end
429 429
430 430 safe_attributes 'project_id',
431 431 'tracker_id',
432 432 'status_id',
433 433 'category_id',
434 434 'assigned_to_id',
435 435 'priority_id',
436 436 'fixed_version_id',
437 437 'subject',
438 438 'description',
439 439 'start_date',
440 440 'due_date',
441 441 'done_ratio',
442 442 'estimated_hours',
443 443 'custom_field_values',
444 444 'custom_fields',
445 445 'lock_version',
446 446 'notes',
447 447 :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
448 448
449 449 safe_attributes 'notes',
450 450 :if => lambda {|issue, user| issue.notes_addable?(user)}
451 451
452 452 safe_attributes 'private_notes',
453 453 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
454 454
455 455 safe_attributes 'watcher_user_ids',
456 456 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
457 457
458 458 safe_attributes 'is_private',
459 459 :if => lambda {|issue, user|
460 460 user.allowed_to?(:set_issues_private, issue.project) ||
461 461 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
462 462 }
463 463
464 464 safe_attributes 'parent_issue_id',
465 465 :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
466 466 user.allowed_to?(:manage_subtasks, issue.project)}
467 467
468 468 def safe_attribute_names(user=nil)
469 469 names = super
470 470 names -= disabled_core_fields
471 471 names -= read_only_attribute_names(user)
472 472 if new_record?
473 473 # Make sure that project_id can always be set for new issues
474 474 names |= %w(project_id)
475 475 end
476 476 if dates_derived?
477 477 names -= %w(start_date due_date)
478 478 end
479 479 if priority_derived?
480 480 names -= %w(priority_id)
481 481 end
482 482 if done_ratio_derived?
483 483 names -= %w(done_ratio)
484 484 end
485 485 names
486 486 end
487 487
488 488 # Safely sets attributes
489 489 # Should be called from controllers instead of #attributes=
490 490 # attr_accessible is too rough because we still want things like
491 491 # Issue.new(:project => foo) to work
492 492 def safe_attributes=(attrs, user=User.current)
493 493 return unless attrs.is_a?(Hash)
494 494
495 495 attrs = attrs.deep_dup
496 496
497 497 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
498 498 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
499 499 if p.is_a?(String) && !p.match(/^\d*$/)
500 500 p_id = Project.find_by_identifier(p).try(:id)
501 501 else
502 502 p_id = p.to_i
503 503 end
504 504 if allowed_target_projects(user).where(:id => p_id).exists?
505 505 self.project_id = p_id
506 506 end
507 507
508 508 if project_id_changed? && attrs['category_id'].to_s == category_id_was.to_s
509 509 # Discard submitted category on previous project
510 510 attrs.delete('category_id')
511 511 end
512 512 end
513 513
514 514 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
515 515 if allowed_target_trackers(user).where(:id => t.to_i).exists?
516 516 self.tracker_id = t
517 517 end
518 518 end
519 519 if project
520 520 # Set a default tracker to accept custom field values
521 521 # even if tracker is not specified
522 522 self.tracker ||= allowed_target_trackers(user).first
523 523 end
524 524
525 525 statuses_allowed = new_statuses_allowed_to(user)
526 526 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
527 527 if statuses_allowed.collect(&:id).include?(s.to_i)
528 528 self.status_id = s
529 529 end
530 530 end
531 531 if new_record? && !statuses_allowed.include?(status)
532 532 self.status = statuses_allowed.first || default_status
533 533 end
534 534 if (u = attrs.delete('assigned_to_id')) && safe_attribute?('assigned_to_id')
535 535 if u.blank?
536 536 self.assigned_to_id = nil
537 537 else
538 538 u = u.to_i
539 539 if assignable_users.any?{|assignable_user| assignable_user.id == u}
540 540 self.assigned_to_id = u
541 541 end
542 542 end
543 543 end
544 544
545 545
546 546 attrs = delete_unsafe_attributes(attrs, user)
547 547 return if attrs.empty?
548 548
549 549 if attrs['parent_issue_id'].present?
550 550 s = attrs['parent_issue_id'].to_s
551 551 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
552 552 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
553 553 end
554 554 end
555 555
556 556 if attrs['custom_field_values'].present?
557 557 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
558 558 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
559 559 end
560 560
561 561 if attrs['custom_fields'].present?
562 562 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
563 563 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
564 564 end
565 565
566 566 # mass-assignment security bypass
567 567 assign_attributes attrs, :without_protection => true
568 568 end
569 569
570 570 def disabled_core_fields
571 571 tracker ? tracker.disabled_core_fields : []
572 572 end
573 573
574 574 # Returns the custom_field_values that can be edited by the given user
575 575 def editable_custom_field_values(user=nil)
576 576 read_only = read_only_attribute_names(user)
577 577 visible_custom_field_values(user).reject do |value|
578 578 read_only.include?(value.custom_field_id.to_s)
579 579 end
580 580 end
581 581
582 582 # Returns the custom fields that can be edited by the given user
583 583 def editable_custom_fields(user=nil)
584 584 editable_custom_field_values(user).map(&:custom_field).uniq
585 585 end
586 586
587 587 # Returns the names of attributes that are read-only for user or the current user
588 588 # For users with multiple roles, the read-only fields are the intersection of
589 589 # read-only fields of each role
590 590 # The result is an array of strings where sustom fields are represented with their ids
591 591 #
592 592 # Examples:
593 593 # issue.read_only_attribute_names # => ['due_date', '2']
594 594 # issue.read_only_attribute_names(user) # => []
595 595 def read_only_attribute_names(user=nil)
596 596 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
597 597 end
598 598
599 599 # Returns the names of required attributes for user or the current user
600 600 # For users with multiple roles, the required fields are the intersection of
601 601 # required fields of each role
602 602 # The result is an array of strings where sustom fields are represented with their ids
603 603 #
604 604 # Examples:
605 605 # issue.required_attribute_names # => ['due_date', '2']
606 606 # issue.required_attribute_names(user) # => []
607 607 def required_attribute_names(user=nil)
608 608 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
609 609 end
610 610
611 611 # Returns true if the attribute is required for user
612 612 def required_attribute?(name, user=nil)
613 613 required_attribute_names(user).include?(name.to_s)
614 614 end
615 615
616 616 # Returns a hash of the workflow rule by attribute for the given user
617 617 #
618 618 # Examples:
619 619 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
620 620 def workflow_rule_by_attribute(user=nil)
621 621 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
622 622
623 623 user_real = user || User.current
624 624 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
625 625 roles = roles.select(&:consider_workflow?)
626 626 return {} if roles.empty?
627 627
628 628 result = {}
629 629 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
630 630 if workflow_permissions.any?
631 631 workflow_rules = workflow_permissions.inject({}) do |h, wp|
632 632 h[wp.field_name] ||= {}
633 633 h[wp.field_name][wp.role_id] = wp.rule
634 634 h
635 635 end
636 636 fields_with_roles = {}
637 637 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
638 638 fields_with_roles[field_id] ||= []
639 639 fields_with_roles[field_id] << role_id
640 640 end
641 641 roles.each do |role|
642 642 fields_with_roles.each do |field_id, role_ids|
643 643 unless role_ids.include?(role.id)
644 644 field_name = field_id.to_s
645 645 workflow_rules[field_name] ||= {}
646 646 workflow_rules[field_name][role.id] = 'readonly'
647 647 end
648 648 end
649 649 end
650 650 workflow_rules.each do |attr, rules|
651 651 next if rules.size < roles.size
652 652 uniq_rules = rules.values.uniq
653 653 if uniq_rules.size == 1
654 654 result[attr] = uniq_rules.first
655 655 else
656 656 result[attr] = 'required'
657 657 end
658 658 end
659 659 end
660 660 @workflow_rule_by_attribute = result if user.nil?
661 661 result
662 662 end
663 663 private :workflow_rule_by_attribute
664 664
665 665 def done_ratio
666 666 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
667 667 status.default_done_ratio
668 668 else
669 669 read_attribute(:done_ratio)
670 670 end
671 671 end
672 672
673 673 def self.use_status_for_done_ratio?
674 674 Setting.issue_done_ratio == 'issue_status'
675 675 end
676 676
677 677 def self.use_field_for_done_ratio?
678 678 Setting.issue_done_ratio == 'issue_field'
679 679 end
680 680
681 681 def validate_issue
682 682 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
683 683 errors.add :due_date, :greater_than_start_date
684 684 end
685 685
686 686 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
687 687 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
688 688 end
689 689
690 690 if fixed_version
691 691 if !assignable_versions.include?(fixed_version)
692 692 errors.add :fixed_version_id, :inclusion
693 693 elsif reopening? && fixed_version.closed?
694 694 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
695 695 end
696 696 end
697 697
698 698 # Checks that the issue can not be added/moved to a disabled tracker
699 699 if project && (tracker_id_changed? || project_id_changed?)
700 700 if tracker && !project.trackers.include?(tracker)
701 701 errors.add :tracker_id, :inclusion
702 702 end
703 703 end
704 704
705 705 # Checks parent issue assignment
706 706 if @invalid_parent_issue_id.present?
707 707 errors.add :parent_issue_id, :invalid
708 708 elsif @parent_issue
709 709 if !valid_parent_project?(@parent_issue)
710 710 errors.add :parent_issue_id, :invalid
711 711 elsif (@parent_issue != parent) && (
712 712 self.would_reschedule?(@parent_issue) ||
713 713 @parent_issue.self_and_ancestors.any? {|a| a.relations_from.any? {|r| r.relation_type == IssueRelation::TYPE_PRECEDES && r.issue_to.would_reschedule?(self)}}
714 714 )
715 715 errors.add :parent_issue_id, :invalid
716 716 elsif !new_record?
717 717 # moving an existing issue
718 718 if move_possible?(@parent_issue)
719 719 # move accepted
720 720 else
721 721 errors.add :parent_issue_id, :invalid
722 722 end
723 723 end
724 724 end
725 725 end
726 726
727 727 # Validates the issue against additional workflow requirements
728 728 def validate_required_fields
729 729 user = new_record? ? author : current_journal.try(:user)
730 730
731 731 required_attribute_names(user).each do |attribute|
732 732 if attribute =~ /^\d+$/
733 733 attribute = attribute.to_i
734 734 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
735 735 if v && Array(v.value).detect(&:present?).nil?
736 736 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
737 737 end
738 738 else
739 739 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
740 740 next if attribute == 'category_id' && project.try(:issue_categories).blank?
741 741 next if attribute == 'fixed_version_id' && assignable_versions.blank?
742 742 errors.add attribute, :blank
743 743 end
744 744 end
745 745 end
746 746 end
747 747
748 748 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
749 749 # so that custom values that are not editable are not validated (eg. a custom field that
750 750 # is marked as required should not trigger a validation error if the user is not allowed
751 751 # to edit this field).
752 752 def validate_custom_field_values
753 753 user = new_record? ? author : current_journal.try(:user)
754 754 if new_record? || custom_field_values_changed?
755 755 editable_custom_field_values(user).each(&:validate_value)
756 756 end
757 757 end
758 758
759 759 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
760 760 # even if the user turns off the setting later
761 761 def update_done_ratio_from_issue_status
762 762 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
763 763 self.done_ratio = status.default_done_ratio
764 764 end
765 765 end
766 766
767 767 def init_journal(user, notes = "")
768 768 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
769 769 end
770 770
771 771 # Returns the current journal or nil if it's not initialized
772 772 def current_journal
773 773 @current_journal
774 774 end
775 775
776 776 # Returns the names of attributes that are journalized when updating the issue
777 777 def journalized_attribute_names
778 778 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
779 779 if tracker
780 780 names -= tracker.disabled_core_fields
781 781 end
782 782 names
783 783 end
784 784
785 785 # Returns the id of the last journal or nil
786 786 def last_journal_id
787 787 if new_record?
788 788 nil
789 789 else
790 790 journals.maximum(:id)
791 791 end
792 792 end
793 793
794 794 # Returns a scope for journals that have an id greater than journal_id
795 795 def journals_after(journal_id)
796 796 scope = journals.reorder("#{Journal.table_name}.id ASC")
797 797 if journal_id.present?
798 798 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
799 799 end
800 800 scope
801 801 end
802 802
803 803 # Returns the initial status of the issue
804 804 # Returns nil for a new issue
805 805 def status_was
806 806 if status_id_changed?
807 807 if status_id_was.to_i > 0
808 808 @status_was ||= IssueStatus.find_by_id(status_id_was)
809 809 end
810 810 else
811 811 @status_was ||= status
812 812 end
813 813 end
814 814
815 815 # Return true if the issue is closed, otherwise false
816 816 def closed?
817 817 status.present? && status.is_closed?
818 818 end
819 819
820 820 # Returns true if the issue was closed when loaded
821 821 def was_closed?
822 822 status_was.present? && status_was.is_closed?
823 823 end
824 824
825 825 # Return true if the issue is being reopened
826 826 def reopening?
827 827 if new_record?
828 828 false
829 829 else
830 830 status_id_changed? && !closed? && was_closed?
831 831 end
832 832 end
833 833 alias :reopened? :reopening?
834 834
835 835 # Return true if the issue is being closed
836 836 def closing?
837 837 if new_record?
838 838 closed?
839 839 else
840 840 status_id_changed? && closed? && !was_closed?
841 841 end
842 842 end
843 843
844 844 # Returns true if the issue is overdue
845 845 def overdue?
846 846 due_date.present? && (due_date < User.current.today) && !closed?
847 847 end
848 848
849 849 # Is the amount of work done less than it should for the due date
850 850 def behind_schedule?
851 851 return false if start_date.nil? || due_date.nil?
852 852 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
853 853 return done_date <= User.current.today
854 854 end
855 855
856 856 # Does this issue have children?
857 857 def children?
858 858 !leaf?
859 859 end
860 860
861 861 # Users the issue can be assigned to
862 862 def assignable_users
863 863 users = project.assignable_users(tracker).to_a
864 864 users << author if author && author.active?
865 865 users << assigned_to if assigned_to
866 866 users.uniq.sort
867 867 end
868 868
869 869 # Versions that the issue can be assigned to
870 870 def assignable_versions
871 871 return @assignable_versions if @assignable_versions
872 872
873 873 versions = project.shared_versions.open.to_a
874 874 if fixed_version
875 875 if fixed_version_id_changed?
876 876 # nothing to do
877 877 elsif project_id_changed?
878 878 if project.shared_versions.include?(fixed_version)
879 879 versions << fixed_version
880 880 end
881 881 else
882 882 versions << fixed_version
883 883 end
884 884 end
885 885 @assignable_versions = versions.uniq.sort
886 886 end
887 887
888 888 # Returns true if this issue is blocked by another issue that is still open
889 889 def blocked?
890 890 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
891 891 end
892 892
893 893 # Returns the default status of the issue based on its tracker
894 894 # Returns nil if tracker is nil
895 895 def default_status
896 896 tracker.try(:default_status)
897 897 end
898 898
899 899 # Returns an array of statuses that user is able to apply
900 900 def new_statuses_allowed_to(user=User.current, include_default=false)
901 901 if new_record? && @copied_from
902 902 [default_status, @copied_from.status].compact.uniq.sort
903 903 else
904 904 initial_status = nil
905 905 if new_record?
906 906 # nop
907 907 elsif tracker_id_changed?
908 908 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
909 909 initial_status = default_status
910 910 elsif tracker.issue_status_ids.include?(status_id_was)
911 911 initial_status = IssueStatus.find_by_id(status_id_was)
912 912 else
913 913 initial_status = default_status
914 914 end
915 915 else
916 916 initial_status = status_was
917 917 end
918 918
919 919 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
920 920 assignee_transitions_allowed = initial_assigned_to_id.present? &&
921 921 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
922 922
923 923 statuses = []
924 924 statuses += IssueStatus.new_statuses_allowed(
925 925 initial_status,
926 926 user.admin ? Role.all.to_a : user.roles_for_project(project),
927 927 tracker,
928 928 author == user,
929 929 assignee_transitions_allowed
930 930 )
931 931 statuses << initial_status unless statuses.empty?
932 932 statuses << default_status if include_default || (new_record? && statuses.empty?)
933 933 statuses = statuses.compact.uniq.sort
934 934 if blocked?
935 935 statuses.reject!(&:is_closed?)
936 936 end
937 937 statuses
938 938 end
939 939 end
940 940
941 941 # Returns the previous assignee (user or group) if changed
942 942 def assigned_to_was
943 943 # assigned_to_id_was is reset before after_save callbacks
944 944 user_id = @previous_assigned_to_id || assigned_to_id_was
945 945 if user_id && user_id != assigned_to_id
946 946 @assigned_to_was ||= Principal.find_by_id(user_id)
947 947 end
948 948 end
949 949
950 950 # Returns the original tracker
951 951 def tracker_was
952 952 Tracker.find_by_id(tracker_id_was)
953 953 end
954 954
955 955 # Returns the users that should be notified
956 956 def notified_users
957 957 notified = []
958 958 # Author and assignee are always notified unless they have been
959 959 # locked or don't want to be notified
960 960 notified << author if author
961 961 if assigned_to
962 962 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
963 963 end
964 964 if assigned_to_was
965 965 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
966 966 end
967 967 notified = notified.select {|u| u.active? && u.notify_about?(self)}
968 968
969 969 notified += project.notified_users
970 970 notified.uniq!
971 971 # Remove users that can not view the issue
972 972 notified.reject! {|user| !visible?(user)}
973 973 notified
974 974 end
975 975
976 976 # Returns the email addresses that should be notified
977 977 def recipients
978 978 notified_users.collect(&:mail)
979 979 end
980 980
981 981 def each_notification(users, &block)
982 982 if users.any?
983 983 if custom_field_values.detect {|value| !value.custom_field.visible?}
984 984 users_by_custom_field_visibility = users.group_by do |user|
985 985 visible_custom_field_values(user).map(&:custom_field_id).sort
986 986 end
987 987 users_by_custom_field_visibility.values.each do |users|
988 988 yield(users)
989 989 end
990 990 else
991 991 yield(users)
992 992 end
993 993 end
994 994 end
995 995
996 996 def notify?
997 997 @notify != false
998 998 end
999 999
1000 1000 def notify=(arg)
1001 1001 @notify = arg
1002 1002 end
1003 1003
1004 1004 # Returns the number of hours spent on this issue
1005 1005 def spent_hours
1006 1006 @spent_hours ||= time_entries.sum(:hours) || 0
1007 1007 end
1008 1008
1009 1009 # Returns the total number of hours spent on this issue and its descendants
1010 1010 def total_spent_hours
1011 1011 @total_spent_hours ||= if leaf?
1012 1012 spent_hours
1013 1013 else
1014 1014 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
1015 1015 end
1016 1016 end
1017 1017
1018 1018 def total_estimated_hours
1019 1019 if leaf?
1020 1020 estimated_hours
1021 1021 else
1022 1022 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
1023 1023 end
1024 1024 end
1025 1025
1026 1026 def relations
1027 1027 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1028 1028 end
1029 1029
1030 1030 # Preloads relations for a collection of issues
1031 1031 def self.load_relations(issues)
1032 1032 if issues.any?
1033 1033 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
1034 1034 issues.each do |issue|
1035 1035 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
1036 1036 end
1037 1037 end
1038 1038 end
1039 1039
1040 1040 # Preloads visible spent time for a collection of issues
1041 1041 def self.load_visible_spent_hours(issues, user=User.current)
1042 1042 if issues.any?
1043 1043 hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
1044 1044 issues.each do |issue|
1045 1045 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
1046 1046 end
1047 1047 end
1048 1048 end
1049 1049
1050 1050 # Preloads visible total spent time for a collection of issues
1051 1051 def self.load_visible_total_spent_hours(issues, user=User.current)
1052 1052 if issues.any?
1053 1053 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
1054 1054 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
1055 1055 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
1056 1056 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
1057 1057 issues.each do |issue|
1058 1058 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
1059 1059 end
1060 1060 end
1061 1061 end
1062 1062
1063 1063 # Preloads visible relations for a collection of issues
1064 1064 def self.load_visible_relations(issues, user=User.current)
1065 1065 if issues.any?
1066 1066 issue_ids = issues.map(&:id)
1067 1067 # Relations with issue_from in given issues and visible issue_to
1068 1068 relations_from = IssueRelation.joins(:issue_to => :project).
1069 1069 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
1070 1070 # Relations with issue_to in given issues and visible issue_from
1071 1071 relations_to = IssueRelation.joins(:issue_from => :project).
1072 1072 where(visible_condition(user)).
1073 1073 where(:issue_to_id => issue_ids).to_a
1074 1074 issues.each do |issue|
1075 1075 relations =
1076 1076 relations_from.select {|relation| relation.issue_from_id == issue.id} +
1077 1077 relations_to.select {|relation| relation.issue_to_id == issue.id}
1078 1078
1079 1079 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
1080 1080 end
1081 1081 end
1082 1082 end
1083 1083
1084 1084 # Finds an issue relation given its id.
1085 1085 def find_relation(relation_id)
1086 1086 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
1087 1087 end
1088 1088
1089 1089 # Returns true if this issue blocks the other issue, otherwise returns false
1090 1090 def blocks?(other)
1091 1091 all = [self]
1092 1092 last = [self]
1093 1093 while last.any?
1094 1094 current = last.map {|i| i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to)}.flatten.uniq
1095 1095 current -= last
1096 1096 current -= all
1097 1097 return true if current.include?(other)
1098 1098 last = current
1099 1099 all += last
1100 1100 end
1101 1101 false
1102 1102 end
1103 1103
1104 1104 # Returns true if the other issue might be rescheduled if the start/due dates of this issue change
1105 1105 def would_reschedule?(other)
1106 1106 all = [self]
1107 1107 last = [self]
1108 1108 while last.any?
1109 1109 current = last.map {|i|
1110 1110 i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
1111 1111 i.leaves.to_a +
1112 1112 i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)}
1113 1113 }.flatten.uniq
1114 1114 current -= last
1115 1115 current -= all
1116 1116 return true if current.include?(other)
1117 1117 last = current
1118 1118 all += last
1119 1119 end
1120 1120 false
1121 1121 end
1122 1122
1123 1123 # Returns an array of issues that duplicate this one
1124 1124 def duplicates
1125 1125 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1126 1126 end
1127 1127
1128 1128 # Returns the due date or the target due date if any
1129 1129 # Used on gantt chart
1130 1130 def due_before
1131 1131 due_date || (fixed_version ? fixed_version.effective_date : nil)
1132 1132 end
1133 1133
1134 1134 # Returns the time scheduled for this issue.
1135 1135 #
1136 1136 # Example:
1137 1137 # Start Date: 2/26/09, End Date: 3/04/09
1138 1138 # duration => 6
1139 1139 def duration
1140 1140 (start_date && due_date) ? due_date - start_date : 0
1141 1141 end
1142 1142
1143 1143 # Returns the duration in working days
1144 1144 def working_duration
1145 1145 (start_date && due_date) ? working_days(start_date, due_date) : 0
1146 1146 end
1147 1147
1148 1148 def soonest_start(reload=false)
1149 1149 if @soonest_start.nil? || reload
1150 1150 dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
1151 1151 p = @parent_issue || parent
1152 1152 if p && Setting.parent_issue_dates == 'derived'
1153 1153 dates << p.soonest_start
1154 1154 end
1155 1155 @soonest_start = dates.compact.max
1156 1156 end
1157 1157 @soonest_start
1158 1158 end
1159 1159
1160 1160 # Sets start_date on the given date or the next working day
1161 1161 # and changes due_date to keep the same working duration.
1162 1162 def reschedule_on(date)
1163 1163 wd = working_duration
1164 1164 date = next_working_date(date)
1165 1165 self.start_date = date
1166 1166 self.due_date = add_working_days(date, wd)
1167 1167 end
1168 1168
1169 1169 # Reschedules the issue on the given date or the next working day and saves the record.
1170 1170 # If the issue is a parent task, this is done by rescheduling its subtasks.
1171 1171 def reschedule_on!(date)
1172 1172 return if date.nil?
1173 1173 if leaf? || !dates_derived?
1174 1174 if start_date.nil? || start_date != date
1175 1175 if start_date && start_date > date
1176 1176 # Issue can not be moved earlier than its soonest start date
1177 1177 date = [soonest_start(true), date].compact.max
1178 1178 end
1179 1179 reschedule_on(date)
1180 1180 begin
1181 1181 save
1182 1182 rescue ActiveRecord::StaleObjectError
1183 1183 reload
1184 1184 reschedule_on(date)
1185 1185 save
1186 1186 end
1187 1187 end
1188 1188 else
1189 1189 leaves.each do |leaf|
1190 1190 if leaf.start_date
1191 1191 # Only move subtask if it starts at the same date as the parent
1192 1192 # or if it starts before the given date
1193 1193 if start_date == leaf.start_date || date > leaf.start_date
1194 1194 leaf.reschedule_on!(date)
1195 1195 end
1196 1196 else
1197 1197 leaf.reschedule_on!(date)
1198 1198 end
1199 1199 end
1200 1200 end
1201 1201 end
1202 1202
1203 1203 def dates_derived?
1204 1204 !leaf? && Setting.parent_issue_dates == 'derived'
1205 1205 end
1206 1206
1207 1207 def priority_derived?
1208 1208 !leaf? && Setting.parent_issue_priority == 'derived'
1209 1209 end
1210 1210
1211 1211 def done_ratio_derived?
1212 1212 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1213 1213 end
1214 1214
1215 1215 def <=>(issue)
1216 1216 if issue.nil?
1217 1217 -1
1218 1218 elsif root_id != issue.root_id
1219 1219 (root_id || 0) <=> (issue.root_id || 0)
1220 1220 else
1221 1221 (lft || 0) <=> (issue.lft || 0)
1222 1222 end
1223 1223 end
1224 1224
1225 1225 def to_s
1226 1226 "#{tracker} ##{id}: #{subject}"
1227 1227 end
1228 1228
1229 1229 # Returns a string of css classes that apply to the issue
1230 1230 def css_classes(user=User.current)
1231 1231 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1232 1232 s << ' closed' if closed?
1233 1233 s << ' overdue' if overdue?
1234 1234 s << ' child' if child?
1235 1235 s << ' parent' unless leaf?
1236 1236 s << ' private' if is_private?
1237 1237 if user.logged?
1238 1238 s << ' created-by-me' if author_id == user.id
1239 1239 s << ' assigned-to-me' if assigned_to_id == user.id
1240 1240 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1241 1241 end
1242 1242 s
1243 1243 end
1244 1244
1245 1245 # Unassigns issues from +version+ if it's no longer shared with issue's project
1246 1246 def self.update_versions_from_sharing_change(version)
1247 1247 # Update issues assigned to the version
1248 1248 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1249 1249 end
1250 1250
1251 1251 # Unassigns issues from versions that are no longer shared
1252 1252 # after +project+ was moved
1253 1253 def self.update_versions_from_hierarchy_change(project)
1254 1254 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1255 1255 # Update issues of the moved projects and issues assigned to a version of a moved project
1256 1256 Issue.update_versions(
1257 1257 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1258 1258 moved_project_ids, moved_project_ids]
1259 1259 )
1260 1260 end
1261 1261
1262 1262 def parent_issue_id=(arg)
1263 1263 s = arg.to_s.strip.presence
1264 1264 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1265 1265 @invalid_parent_issue_id = nil
1266 1266 elsif s.blank?
1267 1267 @parent_issue = nil
1268 1268 @invalid_parent_issue_id = nil
1269 1269 else
1270 1270 @parent_issue = nil
1271 1271 @invalid_parent_issue_id = arg
1272 1272 end
1273 1273 end
1274 1274
1275 1275 def parent_issue_id
1276 1276 if @invalid_parent_issue_id
1277 1277 @invalid_parent_issue_id
1278 1278 elsif instance_variable_defined? :@parent_issue
1279 1279 @parent_issue.nil? ? nil : @parent_issue.id
1280 1280 else
1281 1281 parent_id
1282 1282 end
1283 1283 end
1284 1284
1285 1285 def set_parent_id
1286 1286 self.parent_id = parent_issue_id
1287 1287 end
1288 1288
1289 1289 # Returns true if issue's project is a valid
1290 1290 # parent issue project
1291 1291 def valid_parent_project?(issue=parent)
1292 1292 return true if issue.nil? || issue.project_id == project_id
1293 1293
1294 1294 case Setting.cross_project_subtasks
1295 1295 when 'system'
1296 1296 true
1297 1297 when 'tree'
1298 1298 issue.project.root == project.root
1299 1299 when 'hierarchy'
1300 1300 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1301 1301 when 'descendants'
1302 1302 issue.project.is_or_is_ancestor_of?(project)
1303 1303 else
1304 1304 false
1305 1305 end
1306 1306 end
1307 1307
1308 1308 # Returns an issue scope based on project and scope
1309 1309 def self.cross_project_scope(project, scope=nil)
1310 1310 if project.nil?
1311 1311 return Issue
1312 1312 end
1313 1313 case scope
1314 1314 when 'all', 'system'
1315 1315 Issue
1316 1316 when 'tree'
1317 1317 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1318 1318 :lft => project.root.lft, :rgt => project.root.rgt)
1319 1319 when 'hierarchy'
1320 1320 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1321 1321 :lft => project.lft, :rgt => project.rgt)
1322 1322 when 'descendants'
1323 1323 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1324 1324 :lft => project.lft, :rgt => project.rgt)
1325 1325 else
1326 1326 Issue.where(:project_id => project.id)
1327 1327 end
1328 1328 end
1329 1329
1330 1330 def self.by_tracker(project)
1331 1331 count_and_group_by(:project => project, :association => :tracker)
1332 1332 end
1333 1333
1334 1334 def self.by_version(project)
1335 1335 count_and_group_by(:project => project, :association => :fixed_version)
1336 1336 end
1337 1337
1338 1338 def self.by_priority(project)
1339 1339 count_and_group_by(:project => project, :association => :priority)
1340 1340 end
1341 1341
1342 1342 def self.by_category(project)
1343 1343 count_and_group_by(:project => project, :association => :category)
1344 1344 end
1345 1345
1346 1346 def self.by_assigned_to(project)
1347 1347 count_and_group_by(:project => project, :association => :assigned_to)
1348 1348 end
1349 1349
1350 1350 def self.by_author(project)
1351 1351 count_and_group_by(:project => project, :association => :author)
1352 1352 end
1353 1353
1354 1354 def self.by_subproject(project)
1355 1355 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1356 1356 r.reject {|r| r["project_id"] == project.id.to_s}
1357 1357 end
1358 1358
1359 1359 # Query generator for selecting groups of issue counts for a project
1360 1360 # based on specific criteria
1361 1361 #
1362 1362 # Options
1363 1363 # * project - Project to search in.
1364 1364 # * with_subprojects - Includes subprojects issues if set to true.
1365 1365 # * association - Symbol. Association for grouping.
1366 1366 def self.count_and_group_by(options)
1367 1367 assoc = reflect_on_association(options[:association])
1368 1368 select_field = assoc.foreign_key
1369 1369
1370 1370 Issue.
1371 1371 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1372 1372 joins(:status, assoc.name).
1373 1373 group(:status_id, :is_closed, select_field).
1374 1374 count.
1375 1375 map do |columns, total|
1376 1376 status_id, is_closed, field_value = columns
1377 1377 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1378 1378 {
1379 1379 "status_id" => status_id.to_s,
1380 1380 "closed" => is_closed,
1381 1381 select_field => field_value.to_s,
1382 1382 "total" => total.to_s
1383 1383 }
1384 1384 end
1385 1385 end
1386 1386
1387 1387 # Returns a scope of projects that user can assign the issue to
1388 1388 def allowed_target_projects(user=User.current)
1389 1389 current_project = new_record? ? nil : project
1390 1390 self.class.allowed_target_projects(user, current_project)
1391 1391 end
1392 1392
1393 1393 # Returns a scope of projects that user can assign issues to
1394 1394 # If current_project is given, it will be included in the scope
1395 1395 def self.allowed_target_projects(user=User.current, current_project=nil)
1396 1396 condition = Project.allowed_to_condition(user, :add_issues)
1397 1397 if current_project
1398 1398 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1399 1399 end
1400 1400 Project.where(condition).having_trackers
1401 1401 end
1402 1402
1403 1403 # Returns a scope of trackers that user can assign the issue to
1404 1404 def allowed_target_trackers(user=User.current)
1405 1405 self.class.allowed_target_trackers(project, user, tracker_id_was)
1406 1406 end
1407 1407
1408 1408 # Returns a scope of trackers that user can assign project issues to
1409 1409 def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
1410 1410 if project
1411 1411 scope = project.trackers.sorted
1412 1412 unless user.admin?
1413 1413 roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
1414 1414 unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
1415 1415 tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
1416 1416 if current_tracker
1417 1417 tracker_ids << current_tracker
1418 1418 end
1419 1419 scope = scope.where(:id => tracker_ids)
1420 1420 end
1421 1421 end
1422 1422 scope
1423 1423 else
1424 1424 Tracker.none
1425 1425 end
1426 1426 end
1427 1427
1428 1428 private
1429 1429
1430 1430 def user_tracker_permission?(user, permission)
1431 if project && !project.active?
1432 perm = Redmine::AccessControl.permission(permission)
1433 return false unless perm && perm.read?
1434 end
1435
1431 1436 if user.admin?
1432 1437 true
1433 1438 else
1434 1439 roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
1435 1440 roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)}
1436 1441 end
1437 1442 end
1438 1443
1439 1444 def after_project_change
1440 1445 # Update project_id on related time entries
1441 1446 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1442 1447
1443 1448 # Delete issue relations
1444 1449 unless Setting.cross_project_issue_relations?
1445 1450 relations_from.clear
1446 1451 relations_to.clear
1447 1452 end
1448 1453
1449 1454 # Move subtasks that were in the same project
1450 1455 children.each do |child|
1451 1456 next unless child.project_id == project_id_was
1452 1457 # Change project and keep project
1453 1458 child.send :project=, project, true
1454 1459 unless child.save
1455 1460 raise ActiveRecord::Rollback
1456 1461 end
1457 1462 end
1458 1463 end
1459 1464
1460 1465 # Callback for after the creation of an issue by copy
1461 1466 # * adds a "copied to" relation with the copied issue
1462 1467 # * copies subtasks from the copied issue
1463 1468 def after_create_from_copy
1464 1469 return unless copy? && !@after_create_from_copy_handled
1465 1470
1466 1471 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1467 1472 if @current_journal
1468 1473 @copied_from.init_journal(@current_journal.user)
1469 1474 end
1470 1475 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1471 1476 unless relation.save
1472 1477 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1473 1478 end
1474 1479 end
1475 1480
1476 1481 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1477 1482 copy_options = (@copy_options || {}).merge(:subtasks => false)
1478 1483 copied_issue_ids = {@copied_from.id => self.id}
1479 1484 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1480 1485 # Do not copy self when copying an issue as a descendant of the copied issue
1481 1486 next if child == self
1482 1487 # Do not copy subtasks of issues that were not copied
1483 1488 next unless copied_issue_ids[child.parent_id]
1484 1489 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1485 1490 unless child.visible?
1486 1491 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1487 1492 next
1488 1493 end
1489 1494 copy = Issue.new.copy_from(child, copy_options)
1490 1495 if @current_journal
1491 1496 copy.init_journal(@current_journal.user)
1492 1497 end
1493 1498 copy.author = author
1494 1499 copy.project = project
1495 1500 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1496 1501 unless copy.save
1497 1502 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
1498 1503 next
1499 1504 end
1500 1505 copied_issue_ids[child.id] = copy.id
1501 1506 end
1502 1507 end
1503 1508 @after_create_from_copy_handled = true
1504 1509 end
1505 1510
1506 1511 def update_nested_set_attributes
1507 1512 if parent_id_changed?
1508 1513 update_nested_set_attributes_on_parent_change
1509 1514 end
1510 1515 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1511 1516 end
1512 1517
1513 1518 # Updates the nested set for when an existing issue is moved
1514 1519 def update_nested_set_attributes_on_parent_change
1515 1520 former_parent_id = parent_id_was
1516 1521 # delete invalid relations of all descendants
1517 1522 self_and_descendants.each do |issue|
1518 1523 issue.relations.each do |relation|
1519 1524 relation.destroy unless relation.valid?
1520 1525 end
1521 1526 end
1522 1527 # update former parent
1523 1528 recalculate_attributes_for(former_parent_id) if former_parent_id
1524 1529 end
1525 1530
1526 1531 def update_parent_attributes
1527 1532 if parent_id
1528 1533 recalculate_attributes_for(parent_id)
1529 1534 association(:parent).reset
1530 1535 end
1531 1536 end
1532 1537
1533 1538 def recalculate_attributes_for(issue_id)
1534 1539 if issue_id && p = Issue.find_by_id(issue_id)
1535 1540 if p.priority_derived?
1536 1541 # priority = highest priority of open children
1537 1542 # priority is left unchanged if all children are closed and there's no default priority defined
1538 1543 if priority_position = p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1539 1544 p.priority = IssuePriority.find_by_position(priority_position)
1540 1545 elsif default_priority = IssuePriority.default
1541 1546 p.priority = default_priority
1542 1547 end
1543 1548 end
1544 1549
1545 1550 if p.dates_derived?
1546 1551 # start/due dates = lowest/highest dates of children
1547 1552 p.start_date = p.children.minimum(:start_date)
1548 1553 p.due_date = p.children.maximum(:due_date)
1549 1554 if p.start_date && p.due_date && p.due_date < p.start_date
1550 1555 p.start_date, p.due_date = p.due_date, p.start_date
1551 1556 end
1552 1557 end
1553 1558
1554 1559 if p.done_ratio_derived?
1555 1560 # done ratio = weighted average ratio of leaves
1556 1561 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1557 1562 child_count = p.children.count
1558 1563 if child_count > 0
1559 1564 average = p.children.where("estimated_hours > 0").average(:estimated_hours).to_f
1560 1565 if average == 0
1561 1566 average = 1
1562 1567 end
1563 1568 done = p.children.joins(:status).
1564 1569 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1565 1570 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1566 1571 progress = done / (average * child_count)
1567 1572 p.done_ratio = progress.round
1568 1573 end
1569 1574 end
1570 1575 end
1571 1576
1572 1577 # ancestors will be recursively updated
1573 1578 p.save(:validate => false)
1574 1579 end
1575 1580 end
1576 1581
1577 1582 # Update issues so their versions are not pointing to a
1578 1583 # fixed_version that is not shared with the issue's project
1579 1584 def self.update_versions(conditions=nil)
1580 1585 # Only need to update issues with a fixed_version from
1581 1586 # a different project and that is not systemwide shared
1582 1587 Issue.joins(:project, :fixed_version).
1583 1588 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1584 1589 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1585 1590 " AND #{Version.table_name}.sharing <> 'system'").
1586 1591 where(conditions).each do |issue|
1587 1592 next if issue.project.nil? || issue.fixed_version.nil?
1588 1593 unless issue.project.shared_versions.include?(issue.fixed_version)
1589 1594 issue.init_journal(User.current)
1590 1595 issue.fixed_version = nil
1591 1596 issue.save
1592 1597 end
1593 1598 end
1594 1599 end
1595 1600
1596 1601 # Callback on file attachment
1597 1602 def attachment_added(attachment)
1598 1603 if current_journal && !attachment.new_record?
1599 1604 current_journal.journalize_attachment(attachment, :added)
1600 1605 end
1601 1606 end
1602 1607
1603 1608 # Callback on attachment deletion
1604 1609 def attachment_removed(attachment)
1605 1610 if current_journal && !attachment.new_record?
1606 1611 current_journal.journalize_attachment(attachment, :removed)
1607 1612 current_journal.save
1608 1613 end
1609 1614 end
1610 1615
1611 1616 # Called after a relation is added
1612 1617 def relation_added(relation)
1613 1618 if current_journal
1614 1619 current_journal.journalize_relation(relation, :added)
1615 1620 current_journal.save
1616 1621 end
1617 1622 end
1618 1623
1619 1624 # Called after a relation is removed
1620 1625 def relation_removed(relation)
1621 1626 if current_journal
1622 1627 current_journal.journalize_relation(relation, :removed)
1623 1628 current_journal.save
1624 1629 end
1625 1630 end
1626 1631
1627 1632 # Default assignment based on category
1628 1633 def default_assign
1629 1634 if assigned_to.nil? && category && category.assigned_to
1630 1635 self.assigned_to = category.assigned_to
1631 1636 end
1632 1637 end
1633 1638
1634 1639 # Updates start/due dates of following issues
1635 1640 def reschedule_following_issues
1636 1641 if start_date_changed? || due_date_changed?
1637 1642 relations_from.each do |relation|
1638 1643 relation.set_issue_to_dates
1639 1644 end
1640 1645 end
1641 1646 end
1642 1647
1643 1648 # Closes duplicates if the issue is being closed
1644 1649 def close_duplicates
1645 1650 if closing?
1646 1651 duplicates.each do |duplicate|
1647 1652 # Reload is needed in case the duplicate was updated by a previous duplicate
1648 1653 duplicate.reload
1649 1654 # Don't re-close it if it's already closed
1650 1655 next if duplicate.closed?
1651 1656 # Same user and notes
1652 1657 if @current_journal
1653 1658 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1654 1659 duplicate.private_notes = @current_journal.private_notes
1655 1660 end
1656 1661 duplicate.update_attribute :status, self.status
1657 1662 end
1658 1663 end
1659 1664 end
1660 1665
1661 1666 # Make sure updated_on is updated when adding a note and set updated_on now
1662 1667 # so we can set closed_on with the same value on closing
1663 1668 def force_updated_on_change
1664 1669 if @current_journal || changed?
1665 1670 self.updated_on = current_time_from_proper_timezone
1666 1671 if new_record?
1667 1672 self.created_on = updated_on
1668 1673 end
1669 1674 end
1670 1675 end
1671 1676
1672 1677 # Callback for setting closed_on when the issue is closed.
1673 1678 # The closed_on attribute stores the time of the last closing
1674 1679 # and is preserved when the issue is reopened.
1675 1680 def update_closed_on
1676 1681 if closing?
1677 1682 self.closed_on = updated_on
1678 1683 end
1679 1684 end
1680 1685
1681 1686 # Saves the changes in a Journal
1682 1687 # Called after_save
1683 1688 def create_journal
1684 1689 if current_journal
1685 1690 current_journal.save
1686 1691 end
1687 1692 end
1688 1693
1689 1694 def send_notification
1690 1695 if notify? && Setting.notified_events.include?('issue_added')
1691 1696 Mailer.deliver_issue_add(self)
1692 1697 end
1693 1698 end
1694 1699
1695 1700 # Stores the previous assignee so we can still have access
1696 1701 # to it during after_save callbacks (assigned_to_id_was is reset)
1697 1702 def set_assigned_to_was
1698 1703 @previous_assigned_to_id = assigned_to_id_was
1699 1704 end
1700 1705
1701 1706 # Clears the previous assignee at the end of after_save callbacks
1702 1707 def clear_assigned_to_was
1703 1708 @assigned_to_was = nil
1704 1709 @previous_assigned_to_id = nil
1705 1710 end
1706 1711
1707 1712 def clear_disabled_fields
1708 1713 if tracker
1709 1714 tracker.disabled_core_fields.each do |attribute|
1710 1715 send "#{attribute}=", nil
1711 1716 end
1712 1717 self.done_ratio ||= 0
1713 1718 end
1714 1719 end
1715 1720 end
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,2979 +1,2995
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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, :email_addresses, :user_preferences, :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 setup
35 35 set_language_if_valid 'en'
36 36 end
37 37
38 38 def teardown
39 39 User.current = nil
40 40 end
41 41
42 42 def test_initialize
43 43 issue = Issue.new
44 44
45 45 assert_nil issue.project_id
46 46 assert_nil issue.tracker_id
47 47 assert_nil issue.status_id
48 48 assert_nil issue.author_id
49 49 assert_nil issue.assigned_to_id
50 50 assert_nil issue.category_id
51 51
52 52 assert_equal IssuePriority.default, issue.priority
53 53 end
54 54
55 55 def test_create
56 56 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
57 57 :status_id => 1, :priority => IssuePriority.all.first,
58 58 :subject => 'test_create',
59 59 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
60 60 assert issue.save
61 61 issue.reload
62 62 assert_equal 1.5, issue.estimated_hours
63 63 end
64 64
65 65 def test_create_minimal
66 66 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :subject => 'test_create')
67 67 assert issue.save
68 68 assert_equal issue.tracker.default_status, issue.status
69 69 assert issue.description.nil?
70 70 assert_nil issue.estimated_hours
71 71 end
72 72
73 73 def test_create_with_all_fields_disabled
74 74 tracker = Tracker.find(1)
75 75 tracker.core_fields = []
76 76 tracker.save!
77 77
78 78 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :subject => 'test_create_with_all_fields_disabled')
79 79 assert_save issue
80 80 end
81 81
82 82 def test_start_date_format_should_be_validated
83 83 set_language_if_valid 'en'
84 84 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
85 85 issue = Issue.new(:start_date => invalid_date)
86 86 assert !issue.valid?
87 87 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
88 88 end
89 89 end
90 90
91 91 def test_due_date_format_should_be_validated
92 92 set_language_if_valid 'en'
93 93 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
94 94 issue = Issue.new(:due_date => invalid_date)
95 95 assert !issue.valid?
96 96 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
97 97 end
98 98 end
99 99
100 100 def test_due_date_lesser_than_start_date_should_not_validate
101 101 set_language_if_valid 'en'
102 102 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
103 103 assert !issue.valid?
104 104 assert_include 'Due date must be greater than start date', issue.errors.full_messages
105 105 end
106 106
107 107 def test_start_date_lesser_than_soonest_start_should_not_validate_on_create
108 108 issue = Issue.generate(:start_date => '2013-06-04')
109 109 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
110 110 assert !issue.valid?
111 111 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
112 112 end
113 113
114 114 def test_start_date_lesser_than_soonest_start_should_not_validate_on_update_if_changed
115 115 issue = Issue.generate!(:start_date => '2013-06-04')
116 116 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
117 117 issue.start_date = '2013-06-07'
118 118 assert !issue.valid?
119 119 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
120 120 end
121 121
122 122 def test_start_date_lesser_than_soonest_start_should_validate_on_update_if_unchanged
123 123 issue = Issue.generate!(:start_date => '2013-06-04')
124 124 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
125 125 assert issue.valid?
126 126 end
127 127
128 128 def test_estimated_hours_should_be_validated
129 129 set_language_if_valid 'en'
130 130 ['-2'].each do |invalid|
131 131 issue = Issue.new(:estimated_hours => invalid)
132 132 assert !issue.valid?
133 133 assert_include 'Estimated time is invalid', issue.errors.full_messages
134 134 end
135 135 end
136 136
137 137 def test_create_with_required_custom_field
138 138 set_language_if_valid 'en'
139 139 field = IssueCustomField.find_by_name('Database')
140 140 field.update_attribute(:is_required, true)
141 141
142 142 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
143 143 :status_id => 1, :subject => 'test_create',
144 144 :description => 'IssueTest#test_create_with_required_custom_field')
145 145 assert issue.available_custom_fields.include?(field)
146 146 # No value for the custom field
147 147 assert !issue.save
148 148 assert_equal ["Database cannot be blank"], issue.errors.full_messages
149 149 # Blank value
150 150 issue.custom_field_values = { field.id => '' }
151 151 assert !issue.save
152 152 assert_equal ["Database cannot be blank"], issue.errors.full_messages
153 153 # Invalid value
154 154 issue.custom_field_values = { field.id => 'SQLServer' }
155 155 assert !issue.save
156 156 assert_equal ["Database is not included in the list"], issue.errors.full_messages
157 157 # Valid value
158 158 issue.custom_field_values = { field.id => 'PostgreSQL' }
159 159 assert issue.save
160 160 issue.reload
161 161 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
162 162 end
163 163
164 164 def test_create_with_group_assignment
165 165 with_settings :issue_group_assignment => '1' do
166 166 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
167 167 :subject => 'Group assignment',
168 168 :assigned_to_id => 11).save
169 169 issue = Issue.order('id DESC').first
170 170 assert_kind_of Group, issue.assigned_to
171 171 assert_equal Group.find(11), issue.assigned_to
172 172 end
173 173 end
174 174
175 175 def test_create_with_parent_issue_id
176 176 issue = Issue.new(:project_id => 1, :tracker_id => 1,
177 177 :author_id => 1, :subject => 'Group assignment',
178 178 :parent_issue_id => 1)
179 179 assert_save issue
180 180 assert_equal 1, issue.parent_issue_id
181 181 assert_equal Issue.find(1), issue.parent
182 182 end
183 183
184 184 def test_create_with_sharp_parent_issue_id
185 185 issue = Issue.new(:project_id => 1, :tracker_id => 1,
186 186 :author_id => 1, :subject => 'Group assignment',
187 187 :parent_issue_id => "#1")
188 188 assert_save issue
189 189 assert_equal 1, issue.parent_issue_id
190 190 assert_equal Issue.find(1), issue.parent
191 191 end
192 192
193 193 def test_create_with_invalid_parent_issue_id
194 194 set_language_if_valid 'en'
195 195 issue = Issue.new(:project_id => 1, :tracker_id => 1,
196 196 :author_id => 1, :subject => 'Group assignment',
197 197 :parent_issue_id => '01ABC')
198 198 assert !issue.save
199 199 assert_equal '01ABC', issue.parent_issue_id
200 200 assert_include 'Parent task is invalid', issue.errors.full_messages
201 201 end
202 202
203 203 def test_create_with_invalid_sharp_parent_issue_id
204 204 set_language_if_valid 'en'
205 205 issue = Issue.new(:project_id => 1, :tracker_id => 1,
206 206 :author_id => 1, :subject => 'Group assignment',
207 207 :parent_issue_id => '#01ABC')
208 208 assert !issue.save
209 209 assert_equal '#01ABC', issue.parent_issue_id
210 210 assert_include 'Parent task is invalid', issue.errors.full_messages
211 211 end
212 212
213 213 def assert_visibility_match(user, issues)
214 214 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
215 215 end
216 216
217 217 def test_visible_scope_for_anonymous
218 218 # Anonymous user should see issues of public projects only
219 219 issues = Issue.visible(User.anonymous).to_a
220 220 assert issues.any?
221 221 assert_nil issues.detect {|issue| !issue.project.is_public?}
222 222 assert_nil issues.detect {|issue| issue.is_private?}
223 223 assert_visibility_match User.anonymous, issues
224 224 end
225 225
226 226 def test_visible_scope_for_anonymous_without_view_issues_permissions
227 227 # Anonymous user should not see issues without permission
228 228 Role.anonymous.remove_permission!(:view_issues)
229 229 issues = Issue.visible(User.anonymous).to_a
230 230 assert issues.empty?
231 231 assert_visibility_match User.anonymous, issues
232 232 end
233 233
234 234 def test_visible_scope_for_anonymous_without_view_issues_permissions_and_membership
235 235 Role.anonymous.remove_permission!(:view_issues)
236 236 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [2])
237 237
238 238 issues = Issue.visible(User.anonymous).all
239 239 assert issues.any?
240 240 assert_equal [1], issues.map(&:project_id).uniq.sort
241 241 assert_visibility_match User.anonymous, issues
242 242 end
243 243
244 244 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
245 245 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
246 246 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
247 247 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
248 248 assert !issue.visible?(User.anonymous)
249 249 end
250 250
251 251 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
252 252 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
253 253 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
254 254 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
255 255 assert !issue.visible?(User.anonymous)
256 256 end
257 257
258 258 def test_visible_scope_for_non_member
259 259 user = User.find(9)
260 260 assert user.projects.empty?
261 261 # Non member user should see issues of public projects only
262 262 issues = Issue.visible(user).to_a
263 263 assert issues.any?
264 264 assert_nil issues.detect {|issue| !issue.project.is_public?}
265 265 assert_nil issues.detect {|issue| issue.is_private?}
266 266 assert_visibility_match user, issues
267 267 end
268 268
269 269 def test_visible_scope_for_non_member_with_own_issues_visibility
270 270 Role.non_member.update_attribute :issues_visibility, 'own'
271 271 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
272 272 user = User.find(9)
273 273
274 274 issues = Issue.visible(user).to_a
275 275 assert issues.any?
276 276 assert_nil issues.detect {|issue| issue.author != user}
277 277 assert_visibility_match user, issues
278 278 end
279 279
280 280 def test_visible_scope_for_non_member_without_view_issues_permissions
281 281 # Non member user should not see issues without permission
282 282 Role.non_member.remove_permission!(:view_issues)
283 283 user = User.find(9)
284 284 assert user.projects.empty?
285 285 issues = Issue.visible(user).to_a
286 286 assert issues.empty?
287 287 assert_visibility_match user, issues
288 288 end
289 289
290 290 def test_visible_scope_for_non_member_without_view_issues_permissions_and_membership
291 291 Role.non_member.remove_permission!(:view_issues)
292 292 Member.create!(:project_id => 1, :principal => Group.non_member, :role_ids => [2])
293 293 user = User.find(9)
294 294
295 295 issues = Issue.visible(user).all
296 296 assert issues.any?
297 297 assert_equal [1], issues.map(&:project_id).uniq.sort
298 298 assert_visibility_match user, issues
299 299 end
300 300
301 301 def test_visible_scope_for_member
302 302 user = User.find(9)
303 303 # User should see issues of projects for which user has view_issues permissions only
304 304 Role.non_member.remove_permission!(:view_issues)
305 305 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
306 306 issues = Issue.visible(user).to_a
307 307 assert issues.any?
308 308 assert_nil issues.detect {|issue| issue.project_id != 3}
309 309 assert_nil issues.detect {|issue| issue.is_private?}
310 310 assert_visibility_match user, issues
311 311 end
312 312
313 313 def test_visible_scope_for_member_without_view_issues_permission_and_non_member_role_having_the_permission
314 314 Role.non_member.add_permission!(:view_issues)
315 315 Role.find(1).remove_permission!(:view_issues)
316 316 user = User.find(2)
317 317
318 318 assert_equal 0, Issue.where(:project_id => 1).visible(user).count
319 319 assert_equal false, Issue.where(:project_id => 1).first.visible?(user)
320 320 end
321 321
322 322 def test_visible_scope_with_custom_non_member_role_having_restricted_permission
323 323 role = Role.generate!(:permissions => [:view_project])
324 324 assert Role.non_member.has_permission?(:view_issues)
325 325 user = User.generate!
326 326 Member.create!(:principal => Group.non_member, :project_id => 1, :roles => [role])
327 327
328 328 issues = Issue.visible(user).to_a
329 329 assert issues.any?
330 330 assert_nil issues.detect {|issue| issue.project_id == 1}
331 331 end
332 332
333 333 def test_visible_scope_with_custom_non_member_role_having_extended_permission
334 334 role = Role.generate!(:permissions => [:view_project, :view_issues])
335 335 Role.non_member.remove_permission!(:view_issues)
336 336 user = User.generate!
337 337 Member.create!(:principal => Group.non_member, :project_id => 1, :roles => [role])
338 338
339 339 issues = Issue.visible(user).to_a
340 340 assert issues.any?
341 341 assert_not_nil issues.detect {|issue| issue.project_id == 1}
342 342 end
343 343
344 344 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
345 345 user = User.find(8)
346 346 assert user.groups.any?
347 347 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
348 348 Role.non_member.remove_permission!(:view_issues)
349 349
350 350 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 3,
351 351 :status_id => 1, :priority => IssuePriority.all.first,
352 352 :subject => 'Assignment test',
353 353 :assigned_to => user.groups.first,
354 354 :is_private => true)
355 355
356 356 Role.find(2).update_attribute :issues_visibility, 'default'
357 357 issues = Issue.visible(User.find(8)).to_a
358 358 assert issues.any?
359 359 assert issues.include?(issue)
360 360
361 361 Role.find(2).update_attribute :issues_visibility, 'own'
362 362 issues = Issue.visible(User.find(8)).to_a
363 363 assert issues.any?
364 364 assert_include issue, issues
365 365 end
366 366
367 367 def test_visible_scope_for_member_with_limited_tracker_ids
368 368 role = Role.find(1)
369 369 role.set_permission_trackers :view_issues, [2]
370 370 role.save!
371 371 user = User.find(2)
372 372
373 373 issues = Issue.where(:project_id => 1).visible(user).to_a
374 374 assert issues.any?
375 375 assert_equal [2], issues.map(&:tracker_id).uniq
376 376
377 377 assert Issue.where(:project_id => 1).all? {|issue| issue.visible?(user) ^ issue.tracker_id != 2}
378 378 end
379 379
380 380 def test_visible_scope_should_consider_tracker_ids_on_each_project
381 381 user = User.generate!
382 382
383 383 project1 = Project.generate!
384 384 role1 = Role.generate!
385 385 role1.add_permission! :view_issues
386 386 role1.set_permission_trackers :view_issues, :all
387 387 role1.save!
388 388 User.add_to_project(user, project1, role1)
389 389
390 390 project2 = Project.generate!
391 391 role2 = Role.generate!
392 392 role2.add_permission! :view_issues
393 393 role2.set_permission_trackers :view_issues, [2]
394 394 role2.save!
395 395 User.add_to_project(user, project2, role2)
396 396
397 397 visible_issues = [
398 398 Issue.generate!(:project => project1, :tracker_id => 1),
399 399 Issue.generate!(:project => project1, :tracker_id => 2),
400 400 Issue.generate!(:project => project2, :tracker_id => 2)
401 401 ]
402 402 hidden_issue = Issue.generate!(:project => project2, :tracker_id => 1)
403 403
404 404 issues = Issue.where(:project_id => [project1.id, project2.id]).visible(user)
405 405 assert_equal visible_issues.map(&:id), issues.ids.sort
406 406
407 407 assert visible_issues.all? {|issue| issue.visible?(user)}
408 408 assert !hidden_issue.visible?(user)
409 409 end
410 410
411 411 def test_visible_scope_should_not_consider_roles_without_view_issues_permission
412 412 user = User.generate!
413 413 role1 = Role.generate!
414 414 role1.remove_permission! :view_issues
415 415 role1.set_permission_trackers :view_issues, :all
416 416 role1.save!
417 417 role2 = Role.generate!
418 418 role2.add_permission! :view_issues
419 419 role2.set_permission_trackers :view_issues, [2]
420 420 role2.save!
421 421 User.add_to_project(user, Project.find(1), [role1, role2])
422 422
423 423 issues = Issue.where(:project_id => 1).visible(user).to_a
424 424 assert issues.any?
425 425 assert_equal [2], issues.map(&:tracker_id).uniq
426 426
427 427 assert Issue.where(:project_id => 1).all? {|issue| issue.visible?(user) ^ issue.tracker_id != 2}
428 428 end
429 429
430 430 def test_visible_scope_for_admin
431 431 user = User.find(1)
432 432 user.members.each(&:destroy)
433 433 assert user.projects.empty?
434 434 issues = Issue.visible(user).to_a
435 435 assert issues.any?
436 436 # Admin should see issues on private projects that admin does not belong to
437 437 assert issues.detect {|issue| !issue.project.is_public?}
438 438 # Admin should see private issues of other users
439 439 assert issues.detect {|issue| issue.is_private? && issue.author != user}
440 440 assert_visibility_match user, issues
441 441 end
442 442
443 443 def test_visible_scope_with_project
444 444 project = Project.find(1)
445 445 issues = Issue.visible(User.find(2), :project => project).to_a
446 446 projects = issues.collect(&:project).uniq
447 447 assert_equal 1, projects.size
448 448 assert_equal project, projects.first
449 449 end
450 450
451 451 def test_visible_scope_with_project_and_subprojects
452 452 project = Project.find(1)
453 453 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).to_a
454 454 projects = issues.collect(&:project).uniq
455 455 assert projects.size > 1
456 456 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
457 457 end
458 458
459 459 def test_visible_and_nested_set_scopes
460 460 user = User.generate!
461 461 parent = Issue.generate!(:assigned_to => user)
462 462 assert parent.visible?(user)
463 463 child1 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
464 464 child2 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
465 465 parent.reload
466 466 child1.reload
467 467 child2.reload
468 468 assert child1.visible?(user)
469 469 assert child2.visible?(user)
470 470 assert_equal 2, parent.descendants.count
471 471 assert_equal 2, parent.descendants.visible(user).count
472 472 # awesome_nested_set 2-1-stable branch has regression.
473 473 # https://github.com/collectiveidea/awesome_nested_set/commit/3d5ac746542b564f6586c2316180254b088bebb6
474 474 # ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous column name: lft:
475 475 assert_equal 2, parent.descendants.collect{|i| i}.size
476 476 assert_equal 2, parent.descendants.visible(user).collect{|i| i}.size
477 477 end
478 478
479 479 def test_visible_scope_with_unsaved_user_should_not_raise_an_error
480 480 user = User.new
481 481 assert_nothing_raised do
482 482 Issue.visible(user).to_a
483 483 end
484 484 end
485 485
486 486 def test_open_scope
487 487 issues = Issue.open.to_a
488 488 assert_nil issues.detect(&:closed?)
489 489 end
490 490
491 491 def test_open_scope_with_arg
492 492 issues = Issue.open(false).to_a
493 493 assert_equal issues, issues.select(&:closed?)
494 494 end
495 495
496 496 def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues
497 497 version = Version.find(2)
498 498 assert version.fixed_issues.any?
499 499 assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort
500 500 end
501 501
502 502 def test_fixed_version_scope_with_empty_array_should_return_no_result
503 503 assert_equal 0, Issue.fixed_version([]).count
504 504 end
505 505
506 506 def test_assigned_to_scope_should_return_issues_assigned_to_the_user
507 507 user = User.generate!
508 508 issue = Issue.generate!
509 509 Issue.where(:id => issue.id).update_all :assigned_to_id => user.id
510 510 assert_equal [issue], Issue.assigned_to(user).to_a
511 511 end
512 512
513 513 def test_assigned_to_scope_should_return_issues_assigned_to_the_user_groups
514 514 group = Group.generate!
515 515 user = User.generate!
516 516 group.users << user
517 517 issue = Issue.generate!
518 518 Issue.where(:id => issue.id).update_all :assigned_to_id => group.id
519 519 assert_equal [issue], Issue.assigned_to(user).to_a
520 520 end
521 521
522 def test_issue_should_be_readonly_on_closed_project
523 issue = Issue.find(1)
524 user = User.find(1)
525
526 assert_equal true, issue.visible?(user)
527 assert_equal true, issue.editable?(user)
528 assert_equal true, issue.deletable?(user)
529
530 issue.project.close
531 issue.reload
532
533 assert_equal true, issue.visible?(user)
534 assert_equal false, issue.editable?(user)
535 assert_equal false, issue.deletable?(user)
536 end
537
522 538 def test_errors_full_messages_should_include_custom_fields_errors
523 539 field = IssueCustomField.find_by_name('Database')
524 540
525 541 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
526 542 :status_id => 1, :subject => 'test_create',
527 543 :description => 'IssueTest#test_create_with_required_custom_field')
528 544 assert issue.available_custom_fields.include?(field)
529 545 # Invalid value
530 546 issue.custom_field_values = { field.id => 'SQLServer' }
531 547
532 548 assert !issue.valid?
533 549 assert_equal 1, issue.errors.full_messages.size
534 550 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
535 551 issue.errors.full_messages.first
536 552 end
537 553
538 554 def test_update_issue_with_required_custom_field
539 555 field = IssueCustomField.find_by_name('Database')
540 556 field.update_attribute(:is_required, true)
541 557
542 558 issue = Issue.find(1)
543 559 assert_nil issue.custom_value_for(field)
544 560 assert issue.available_custom_fields.include?(field)
545 561 # No change to custom values, issue can be saved
546 562 assert issue.save
547 563 # Blank value
548 564 issue.custom_field_values = { field.id => '' }
549 565 assert !issue.save
550 566 # Valid value
551 567 issue.custom_field_values = { field.id => 'PostgreSQL' }
552 568 assert issue.save
553 569 issue.reload
554 570 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
555 571 end
556 572
557 573 def test_should_not_update_attributes_if_custom_fields_validation_fails
558 574 issue = Issue.find(1)
559 575 field = IssueCustomField.find_by_name('Database')
560 576 assert issue.available_custom_fields.include?(field)
561 577
562 578 issue.custom_field_values = { field.id => 'Invalid' }
563 579 issue.subject = 'Should be not be saved'
564 580 assert !issue.save
565 581
566 582 issue.reload
567 583 assert_equal "Cannot print recipes", issue.subject
568 584 end
569 585
570 586 def test_should_not_recreate_custom_values_objects_on_update
571 587 field = IssueCustomField.find_by_name('Database')
572 588
573 589 issue = Issue.find(1)
574 590 issue.custom_field_values = { field.id => 'PostgreSQL' }
575 591 assert issue.save
576 592 custom_value = issue.custom_value_for(field)
577 593 issue.reload
578 594 issue.custom_field_values = { field.id => 'MySQL' }
579 595 assert issue.save
580 596 issue.reload
581 597 assert_equal custom_value.id, issue.custom_value_for(field).id
582 598 end
583 599
584 600 def test_setting_project_should_set_version_to_default_version
585 601 version = Version.generate!(:project_id => 1)
586 602 Project.find(1).update_attribute(:default_version_id, version.id)
587 603
588 604 issue = Issue.new(:project_id => 1)
589 605 assert_equal version, issue.fixed_version
590 606 end
591 607
592 608 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
593 609 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
594 610 :status_id => 1, :subject => 'Test',
595 611 :custom_field_values => {'2' => 'Test'})
596 612 assert !Tracker.find(2).custom_field_ids.include?(2)
597 613
598 614 issue = Issue.find(issue.id)
599 615 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
600 616
601 617 issue = Issue.find(issue.id)
602 618 custom_value = issue.custom_value_for(2)
603 619 assert_not_nil custom_value
604 620 assert_equal 'Test', custom_value.value
605 621 end
606 622
607 623 def test_assigning_tracker_id_should_reload_custom_fields_values
608 624 issue = Issue.new(:project => Project.find(1))
609 625 assert issue.custom_field_values.empty?
610 626 issue.tracker_id = 1
611 627 assert issue.custom_field_values.any?
612 628 end
613 629
614 630 def test_assigning_attributes_should_assign_project_and_tracker_first
615 631 seq = sequence('seq')
616 632 issue = Issue.new
617 633 issue.expects(:project_id=).in_sequence(seq)
618 634 issue.expects(:tracker_id=).in_sequence(seq)
619 635 issue.expects(:subject=).in_sequence(seq)
620 636 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
621 637 end
622 638
623 639 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
624 640 attributes = ActiveSupport::OrderedHash.new
625 641 attributes['custom_field_values'] = { '1' => 'MySQL' }
626 642 attributes['tracker_id'] = '1'
627 643 issue = Issue.new(:project => Project.find(1))
628 644 issue.attributes = attributes
629 645 assert_equal 'MySQL', issue.custom_field_value(1)
630 646 end
631 647
632 648 def test_changing_tracker_should_clear_disabled_core_fields
633 649 tracker = Tracker.find(2)
634 650 tracker.core_fields = tracker.core_fields - %w(due_date)
635 651 tracker.save!
636 652
637 653 issue = Issue.generate!(:tracker_id => 1, :start_date => Date.today, :due_date => Date.today)
638 654 issue.save!
639 655
640 656 issue.tracker_id = 2
641 657 issue.save!
642 658 assert_not_nil issue.start_date
643 659 assert_nil issue.due_date
644 660 end
645 661
646 662 def test_changing_tracker_should_not_add_cleared_fields_to_journal
647 663 tracker = Tracker.find(2)
648 664 tracker.core_fields = tracker.core_fields - %w(due_date)
649 665 tracker.save!
650 666
651 667 issue = Issue.generate!(:tracker_id => 1, :due_date => Date.today)
652 668 issue.save!
653 669
654 670 assert_difference 'Journal.count' do
655 671 issue.init_journal User.find(1)
656 672 issue.tracker_id = 2
657 673 issue.save!
658 674 assert_nil issue.due_date
659 675 end
660 676 journal = Journal.order('id DESC').first
661 677 assert_equal 1, journal.details.count
662 678 end
663 679
664 680 def test_reload_should_reload_custom_field_values
665 681 issue = Issue.generate!
666 682 issue.custom_field_values = {'2' => 'Foo'}
667 683 issue.save!
668 684
669 685 issue = Issue.order('id desc').first
670 686 assert_equal 'Foo', issue.custom_field_value(2)
671 687
672 688 issue.custom_field_values = {'2' => 'Bar'}
673 689 assert_equal 'Bar', issue.custom_field_value(2)
674 690
675 691 issue.reload
676 692 assert_equal 'Foo', issue.custom_field_value(2)
677 693 end
678 694
679 695 def test_should_update_issue_with_disabled_tracker
680 696 p = Project.find(1)
681 697 issue = Issue.find(1)
682 698
683 699 p.trackers.delete(issue.tracker)
684 700 assert !p.trackers.include?(issue.tracker)
685 701
686 702 issue.reload
687 703 issue.subject = 'New subject'
688 704 assert issue.save
689 705 end
690 706
691 707 def test_should_not_set_a_disabled_tracker
692 708 p = Project.find(1)
693 709 p.trackers.delete(Tracker.find(2))
694 710
695 711 issue = Issue.find(1)
696 712 issue.tracker_id = 2
697 713 issue.subject = 'New subject'
698 714 assert !issue.save
699 715 assert_not_equal [], issue.errors[:tracker_id]
700 716 end
701 717
702 718 def test_category_based_assignment
703 719 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
704 720 :status_id => 1, :priority => IssuePriority.all.first,
705 721 :subject => 'Assignment test',
706 722 :description => 'Assignment test', :category_id => 1)
707 723 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
708 724 end
709 725
710 726 def test_new_statuses_allowed_to
711 727 WorkflowTransition.delete_all
712 728 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
713 729 :old_status_id => 1, :new_status_id => 2,
714 730 :author => false, :assignee => false)
715 731 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
716 732 :old_status_id => 1, :new_status_id => 3,
717 733 :author => true, :assignee => false)
718 734 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
719 735 :old_status_id => 1, :new_status_id => 4,
720 736 :author => false, :assignee => true)
721 737 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
722 738 :old_status_id => 1, :new_status_id => 5,
723 739 :author => true, :assignee => true)
724 740 status = IssueStatus.find(1)
725 741 role = Role.find(1)
726 742 tracker = Tracker.find(1)
727 743 user = User.find(2)
728 744
729 745 issue = Issue.generate!(:tracker => tracker, :status => status,
730 746 :project_id => 1, :author_id => 1)
731 747 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
732 748
733 749 issue = Issue.generate!(:tracker => tracker, :status => status,
734 750 :project_id => 1, :author => user)
735 751 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
736 752
737 753 issue = Issue.generate!(:tracker => tracker, :status => status,
738 754 :project_id => 1, :author_id => 1,
739 755 :assigned_to => user)
740 756 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
741 757
742 758 issue = Issue.generate!(:tracker => tracker, :status => status,
743 759 :project_id => 1, :author => user,
744 760 :assigned_to => user)
745 761 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
746 762
747 763 group = Group.generate!
748 764 group.users << user
749 765 issue = Issue.generate!(:tracker => tracker, :status => status,
750 766 :project_id => 1, :author => user,
751 767 :assigned_to => group)
752 768 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
753 769 end
754 770
755 771 def test_new_statuses_allowed_to_should_consider_group_assignment
756 772 WorkflowTransition.delete_all
757 773 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
758 774 :old_status_id => 1, :new_status_id => 4,
759 775 :author => false, :assignee => true)
760 776 user = User.find(2)
761 777 group = Group.generate!
762 778 group.users << user
763 779
764 780 issue = Issue.generate!(:author_id => 1, :assigned_to => group)
765 781 assert_include 4, issue.new_statuses_allowed_to(user).map(&:id)
766 782 end
767 783
768 784 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
769 785 admin = User.find(1)
770 786 issue = Issue.find(1)
771 787 assert !admin.member_of?(issue.project)
772 788 expected_statuses = [issue.status] +
773 789 WorkflowTransition.where(:old_status_id => issue.status_id).
774 790 map(&:new_status).uniq.sort
775 791 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
776 792 end
777 793
778 794 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
779 795 issue = Issue.find(1).copy
780 796 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
781 797
782 798 issue = Issue.find(2).copy
783 799 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
784 800 end
785 801
786 802 def test_safe_attributes_names_should_not_include_disabled_field
787 803 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
788 804
789 805 issue = Issue.new(:tracker => tracker)
790 806 assert_include 'tracker_id', issue.safe_attribute_names
791 807 assert_include 'status_id', issue.safe_attribute_names
792 808 assert_include 'subject', issue.safe_attribute_names
793 809 assert_include 'description', issue.safe_attribute_names
794 810 assert_include 'custom_field_values', issue.safe_attribute_names
795 811 assert_include 'custom_fields', issue.safe_attribute_names
796 812 assert_include 'lock_version', issue.safe_attribute_names
797 813
798 814 tracker.core_fields.each do |field|
799 815 assert_include field, issue.safe_attribute_names
800 816 end
801 817
802 818 tracker.disabled_core_fields.each do |field|
803 819 assert_not_include field, issue.safe_attribute_names
804 820 end
805 821 end
806 822
807 823 def test_safe_attributes_should_ignore_disabled_fields
808 824 tracker = Tracker.find(1)
809 825 tracker.core_fields = %w(assigned_to_id due_date)
810 826 tracker.save!
811 827
812 828 issue = Issue.new(:tracker => tracker)
813 829 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
814 830 assert_nil issue.start_date
815 831 assert_equal Date.parse('2012-07-14'), issue.due_date
816 832 end
817 833
818 834 def test_safe_attributes_should_accept_target_tracker_enabled_fields
819 835 source = Tracker.find(1)
820 836 source.core_fields = []
821 837 source.save!
822 838 target = Tracker.find(2)
823 839 target.core_fields = %w(assigned_to_id due_date)
824 840 target.save!
825 841 user = User.find(2)
826 842
827 843 issue = Issue.new(:project => Project.find(1), :tracker => source)
828 844 issue.send :safe_attributes=, {'tracker_id' => 2, 'due_date' => '2012-07-14'}, user
829 845 assert_equal target, issue.tracker
830 846 assert_equal Date.parse('2012-07-14'), issue.due_date
831 847 end
832 848
833 849 def test_safe_attributes_should_not_include_readonly_fields
834 850 WorkflowPermission.delete_all
835 851 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
836 852 :role_id => 1, :field_name => 'due_date',
837 853 :rule => 'readonly')
838 854 user = User.find(2)
839 855
840 856 issue = Issue.new(:project_id => 1, :tracker_id => 1)
841 857 assert_equal %w(due_date), issue.read_only_attribute_names(user)
842 858 assert_not_include 'due_date', issue.safe_attribute_names(user)
843 859
844 860 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
845 861 assert_equal Date.parse('2012-07-14'), issue.start_date
846 862 assert_nil issue.due_date
847 863 end
848 864
849 865 def test_safe_attributes_should_not_include_readonly_custom_fields
850 866 cf1 = IssueCustomField.create!(:name => 'Writable field',
851 867 :field_format => 'string',
852 868 :is_for_all => true, :tracker_ids => [1])
853 869 cf2 = IssueCustomField.create!(:name => 'Readonly field',
854 870 :field_format => 'string',
855 871 :is_for_all => true, :tracker_ids => [1])
856 872 WorkflowPermission.delete_all
857 873 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
858 874 :role_id => 1, :field_name => cf2.id.to_s,
859 875 :rule => 'readonly')
860 876 user = User.find(2)
861 877 issue = Issue.new(:project_id => 1, :tracker_id => 1)
862 878 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
863 879 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
864 880
865 881 issue.send :safe_attributes=, {'custom_field_values' => {
866 882 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
867 883 }}, user
868 884 assert_equal 'value1', issue.custom_field_value(cf1)
869 885 assert_nil issue.custom_field_value(cf2)
870 886
871 887 issue.send :safe_attributes=, {'custom_fields' => [
872 888 {'id' => cf1.id.to_s, 'value' => 'valuea'},
873 889 {'id' => cf2.id.to_s, 'value' => 'valueb'}
874 890 ]}, user
875 891 assert_equal 'valuea', issue.custom_field_value(cf1)
876 892 assert_nil issue.custom_field_value(cf2)
877 893 end
878 894
879 895 def test_safe_attributes_should_ignore_unassignable_assignee
880 896 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
881 897 :status_id => 1, :priority => IssuePriority.all.first,
882 898 :subject => 'test_create')
883 899 assert issue.valid?
884 900
885 901 # locked user, not allowed
886 902 issue.safe_attributes=({'assigned_to_id' => '5'})
887 903 assert_nil issue.assigned_to_id
888 904 # no member
889 905 issue.safe_attributes=({'assigned_to_id' => '1'})
890 906 assert_nil issue.assigned_to_id
891 907 # user 2 is ok
892 908 issue.safe_attributes=({'assigned_to_id' => '2'})
893 909 assert_equal 2, issue.assigned_to_id
894 910 assert issue.save
895 911
896 912 issue.reload
897 913 assert_equal 2, issue.assigned_to_id
898 914 issue.safe_attributes=({'assigned_to_id' => '5'})
899 915 assert_equal 2, issue.assigned_to_id
900 916 issue.safe_attributes=({'assigned_to_id' => '1'})
901 917 assert_equal 2, issue.assigned_to_id
902 918 # user 3 is also ok
903 919 issue.safe_attributes=({'assigned_to_id' => '3'})
904 920 assert_equal 3, issue.assigned_to_id
905 921 assert issue.save
906 922
907 923 # removal of assignee
908 924 issue.safe_attributes=({'assigned_to_id' => ''})
909 925 assert_nil issue.assigned_to_id
910 926 assert issue.save
911 927 end
912 928
913 929 def test_editable_custom_field_values_should_return_non_readonly_custom_values
914 930 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
915 931 :is_for_all => true, :tracker_ids => [1, 2])
916 932 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
917 933 :is_for_all => true, :tracker_ids => [1, 2])
918 934 WorkflowPermission.delete_all
919 935 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
920 936 :field_name => cf2.id.to_s, :rule => 'readonly')
921 937 user = User.find(2)
922 938
923 939 issue = Issue.new(:project_id => 1, :tracker_id => 1)
924 940 values = issue.editable_custom_field_values(user)
925 941 assert values.detect {|value| value.custom_field == cf1}
926 942 assert_nil values.detect {|value| value.custom_field == cf2}
927 943
928 944 issue.tracker_id = 2
929 945 values = issue.editable_custom_field_values(user)
930 946 assert values.detect {|value| value.custom_field == cf1}
931 947 assert values.detect {|value| value.custom_field == cf2}
932 948 end
933 949
934 950 def test_editable_custom_fields_should_return_custom_field_that_is_enabled_for_the_role_only
935 951 enabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [1,2])
936 952 disabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [2])
937 953 user = User.find(2)
938 954 issue = Issue.new(:project_id => 1, :tracker_id => 1)
939 955
940 956 assert_include enabled_cf, issue.editable_custom_fields(user)
941 957 assert_not_include disabled_cf, issue.editable_custom_fields(user)
942 958 end
943 959
944 960 def test_safe_attributes_should_accept_target_tracker_writable_fields
945 961 WorkflowPermission.delete_all
946 962 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
947 963 :role_id => 1, :field_name => 'due_date',
948 964 :rule => 'readonly')
949 965 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
950 966 :role_id => 1, :field_name => 'start_date',
951 967 :rule => 'readonly')
952 968 user = User.find(2)
953 969
954 970 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
955 971
956 972 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
957 973 'due_date' => '2012-07-14'}, user
958 974 assert_equal Date.parse('2012-07-12'), issue.start_date
959 975 assert_nil issue.due_date
960 976
961 977 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
962 978 'due_date' => '2012-07-16',
963 979 'tracker_id' => 2}, user
964 980 assert_equal Date.parse('2012-07-12'), issue.start_date
965 981 assert_equal Date.parse('2012-07-16'), issue.due_date
966 982 end
967 983
968 984 def test_safe_attributes_should_accept_target_status_writable_fields
969 985 WorkflowPermission.delete_all
970 986 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
971 987 :role_id => 1, :field_name => 'due_date',
972 988 :rule => 'readonly')
973 989 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
974 990 :role_id => 1, :field_name => 'start_date',
975 991 :rule => 'readonly')
976 992 user = User.find(2)
977 993
978 994 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
979 995
980 996 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
981 997 'due_date' => '2012-07-14'},
982 998 user
983 999 assert_equal Date.parse('2012-07-12'), issue.start_date
984 1000 assert_nil issue.due_date
985 1001
986 1002 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
987 1003 'due_date' => '2012-07-16',
988 1004 'status_id' => 2},
989 1005 user
990 1006 assert_equal Date.parse('2012-07-12'), issue.start_date
991 1007 assert_equal Date.parse('2012-07-16'), issue.due_date
992 1008 end
993 1009
994 1010 def test_required_attributes_should_be_validated
995 1011 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
996 1012 :is_for_all => true, :tracker_ids => [1, 2])
997 1013
998 1014 WorkflowPermission.delete_all
999 1015 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1000 1016 :role_id => 1, :field_name => 'due_date',
1001 1017 :rule => 'required')
1002 1018 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1003 1019 :role_id => 1, :field_name => 'category_id',
1004 1020 :rule => 'required')
1005 1021 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1006 1022 :role_id => 1, :field_name => cf.id.to_s,
1007 1023 :rule => 'required')
1008 1024
1009 1025 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
1010 1026 :role_id => 1, :field_name => 'start_date',
1011 1027 :rule => 'required')
1012 1028 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
1013 1029 :role_id => 1, :field_name => cf.id.to_s,
1014 1030 :rule => 'required')
1015 1031 user = User.find(2)
1016 1032
1017 1033 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1018 1034 :status_id => 1, :subject => 'Required fields',
1019 1035 :author => user)
1020 1036 assert_equal [cf.id.to_s, "category_id", "due_date"],
1021 1037 issue.required_attribute_names(user).sort
1022 1038 assert !issue.save, "Issue was saved"
1023 1039 assert_equal ["Category cannot be blank", "Due date cannot be blank", "Foo cannot be blank"],
1024 1040 issue.errors.full_messages.sort
1025 1041
1026 1042 issue.tracker_id = 2
1027 1043 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
1028 1044 assert !issue.save, "Issue was saved"
1029 1045 assert_equal ["Foo cannot be blank", "Start date cannot be blank"],
1030 1046 issue.errors.full_messages.sort
1031 1047
1032 1048 issue.start_date = Date.today
1033 1049 issue.custom_field_values = {cf.id.to_s => 'bar'}
1034 1050 assert issue.save
1035 1051 end
1036 1052
1037 1053 def test_required_attribute_that_is_disabled_for_the_tracker_should_not_be_required
1038 1054 WorkflowPermission.delete_all
1039 1055 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1040 1056 :role_id => 1, :field_name => 'start_date',
1041 1057 :rule => 'required')
1042 1058 user = User.find(2)
1043 1059
1044 1060 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1045 1061 :subject => 'Required fields', :author => user)
1046 1062 assert !issue.save
1047 1063 assert_include "Start date cannot be blank", issue.errors.full_messages
1048 1064
1049 1065 tracker = Tracker.find(1)
1050 1066 tracker.core_fields -= %w(start_date)
1051 1067 tracker.save!
1052 1068 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1053 1069 :subject => 'Required fields', :author => user)
1054 1070 assert issue.save
1055 1071 end
1056 1072
1057 1073 def test_category_should_not_be_required_if_project_has_no_categories
1058 1074 Project.find(1).issue_categories.delete_all
1059 1075 WorkflowPermission.delete_all
1060 1076 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1061 1077 :role_id => 1, :field_name => 'category_id',:rule => 'required')
1062 1078 user = User.find(2)
1063 1079
1064 1080 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1065 1081 :subject => 'Required fields', :author => user)
1066 1082 assert_save issue
1067 1083 end
1068 1084
1069 1085 def test_fixed_version_should_not_be_required_no_assignable_versions
1070 1086 Version.delete_all
1071 1087 WorkflowPermission.delete_all
1072 1088 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1073 1089 :role_id => 1, :field_name => 'fixed_version_id',:rule => 'required')
1074 1090 user = User.find(2)
1075 1091
1076 1092 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1077 1093 :subject => 'Required fields', :author => user)
1078 1094 assert_save issue
1079 1095 end
1080 1096
1081 1097 def test_required_custom_field_that_is_not_visible_for_the_user_should_not_be_required
1082 1098 CustomField.delete_all
1083 1099 field = IssueCustomField.generate!(:is_required => true, :visible => false, :role_ids => [1], :trackers => Tracker.all, :is_for_all => true)
1084 1100 user = User.generate!
1085 1101 User.add_to_project(user, Project.find(1), Role.find(2))
1086 1102
1087 1103 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1088 1104 :subject => 'Required fields', :author => user)
1089 1105 assert_save issue
1090 1106 end
1091 1107
1092 1108 def test_required_custom_field_that_is_visible_for_the_user_should_be_required
1093 1109 CustomField.delete_all
1094 1110 field = IssueCustomField.generate!(:is_required => true, :visible => false, :role_ids => [1], :trackers => Tracker.all, :is_for_all => true)
1095 1111 user = User.generate!
1096 1112 User.add_to_project(user, Project.find(1), Role.find(1))
1097 1113
1098 1114 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1099 1115 :subject => 'Required fields', :author => user)
1100 1116 assert !issue.save
1101 1117 assert_include "#{field.name} cannot be blank", issue.errors.full_messages
1102 1118 end
1103 1119
1104 1120 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
1105 1121 WorkflowPermission.delete_all
1106 1122 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1107 1123 :role_id => 1, :field_name => 'due_date',
1108 1124 :rule => 'required')
1109 1125 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1110 1126 :role_id => 1, :field_name => 'start_date',
1111 1127 :rule => 'required')
1112 1128 user = User.find(2)
1113 1129 member = Member.find(1)
1114 1130 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1115 1131
1116 1132 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
1117 1133
1118 1134 member.role_ids = [1, 2]
1119 1135 member.save!
1120 1136 assert_equal [], issue.required_attribute_names(user.reload)
1121 1137
1122 1138 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1123 1139 :role_id => 2, :field_name => 'due_date',
1124 1140 :rule => 'required')
1125 1141 assert_equal %w(due_date), issue.required_attribute_names(user)
1126 1142
1127 1143 member.role_ids = [1, 2, 3]
1128 1144 member.save!
1129 1145 assert_equal [], issue.required_attribute_names(user.reload)
1130 1146
1131 1147 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1132 1148 :role_id => 3, :field_name => 'due_date',
1133 1149 :rule => 'readonly')
1134 1150 # required + readonly => required
1135 1151 assert_equal %w(due_date), issue.required_attribute_names(user)
1136 1152 end
1137 1153
1138 1154 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
1139 1155 WorkflowPermission.delete_all
1140 1156 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1141 1157 :role_id => 1, :field_name => 'due_date',
1142 1158 :rule => 'readonly')
1143 1159 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1144 1160 :role_id => 1, :field_name => 'start_date',
1145 1161 :rule => 'readonly')
1146 1162 user = User.find(2)
1147 1163 member = Member.find(1)
1148 1164 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1149 1165
1150 1166 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
1151 1167
1152 1168 member.role_ids = [1, 2]
1153 1169 member.save!
1154 1170 assert_equal [], issue.read_only_attribute_names(user.reload)
1155 1171
1156 1172 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1157 1173 :role_id => 2, :field_name => 'due_date',
1158 1174 :rule => 'readonly')
1159 1175 assert_equal %w(due_date), issue.read_only_attribute_names(user)
1160 1176 end
1161 1177
1162 1178 # A field that is not visible by role 2 and readonly by role 1 should be readonly for user with role 1 and 2
1163 1179 def test_read_only_attribute_names_should_include_custom_fields_that_combine_readonly_and_not_visible_for_roles
1164 1180 field = IssueCustomField.generate!(
1165 1181 :is_for_all => true, :trackers => Tracker.all, :visible => false, :role_ids => [1]
1166 1182 )
1167 1183 WorkflowPermission.delete_all
1168 1184 WorkflowPermission.create!(
1169 1185 :old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => field.id, :rule => 'readonly'
1170 1186 )
1171 1187 user = User.generate!
1172 1188 project = Project.find(1)
1173 1189 User.add_to_project(user, project, Role.where(:id => [1, 2]))
1174 1190
1175 1191 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1176 1192 assert_equal [field.id.to_s], issue.read_only_attribute_names(user)
1177 1193 end
1178 1194
1179 1195 def test_workflow_rules_should_ignore_roles_without_issue_permissions
1180 1196 role = Role.generate! :permissions => [:view_issues, :edit_issues]
1181 1197 ignored_role = Role.generate! :permissions => [:view_issues]
1182 1198
1183 1199 WorkflowPermission.delete_all
1184 1200 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1185 1201 :role => role, :field_name => 'due_date',
1186 1202 :rule => 'required')
1187 1203 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1188 1204 :role => role, :field_name => 'start_date',
1189 1205 :rule => 'readonly')
1190 1206 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1191 1207 :role => role, :field_name => 'done_ratio',
1192 1208 :rule => 'readonly')
1193 1209 user = User.generate!
1194 1210 User.add_to_project user, Project.find(1), [role, ignored_role]
1195 1211
1196 1212 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1197 1213
1198 1214 assert_equal %w(due_date), issue.required_attribute_names(user)
1199 1215 assert_equal %w(done_ratio start_date), issue.read_only_attribute_names(user).sort
1200 1216 end
1201 1217
1202 1218 def test_workflow_rules_should_work_for_member_with_duplicate_role
1203 1219 WorkflowPermission.delete_all
1204 1220 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1205 1221 :role_id => 1, :field_name => 'due_date',
1206 1222 :rule => 'required')
1207 1223 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1208 1224 :role_id => 1, :field_name => 'start_date',
1209 1225 :rule => 'readonly')
1210 1226
1211 1227 user = User.generate!
1212 1228 m = Member.new(:user_id => user.id, :project_id => 1)
1213 1229 m.member_roles.build(:role_id => 1)
1214 1230 m.member_roles.build(:role_id => 1)
1215 1231 m.save!
1216 1232
1217 1233 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1218 1234
1219 1235 assert_equal %w(due_date), issue.required_attribute_names(user)
1220 1236 assert_equal %w(start_date), issue.read_only_attribute_names(user)
1221 1237 end
1222 1238
1223 1239 def test_copy
1224 1240 issue = Issue.new.copy_from(1)
1225 1241 assert issue.copy?
1226 1242 assert issue.save
1227 1243 issue.reload
1228 1244 orig = Issue.find(1)
1229 1245 assert_equal orig.subject, issue.subject
1230 1246 assert_equal orig.tracker, issue.tracker
1231 1247 assert_equal "125", issue.custom_value_for(2).value
1232 1248 end
1233 1249
1234 1250 def test_copy_should_copy_status
1235 1251 orig = Issue.find(8)
1236 1252 assert orig.status != orig.default_status
1237 1253
1238 1254 issue = Issue.new.copy_from(orig)
1239 1255 assert issue.save
1240 1256 issue.reload
1241 1257 assert_equal orig.status, issue.status
1242 1258 end
1243 1259
1244 1260 def test_copy_should_add_relation_with_copied_issue
1245 1261 copied = Issue.find(1)
1246 1262 issue = Issue.new.copy_from(copied)
1247 1263 assert issue.save
1248 1264 issue.reload
1249 1265
1250 1266 assert_equal 1, issue.relations.size
1251 1267 relation = issue.relations.first
1252 1268 assert_equal 'copied_to', relation.relation_type
1253 1269 assert_equal copied, relation.issue_from
1254 1270 assert_equal issue, relation.issue_to
1255 1271 end
1256 1272
1257 1273 def test_copy_should_copy_subtasks
1258 1274 issue = Issue.generate_with_descendants!
1259 1275
1260 1276 copy = issue.reload.copy
1261 1277 copy.author = User.find(7)
1262 1278 assert_difference 'Issue.count', 1+issue.descendants.count do
1263 1279 assert copy.save
1264 1280 end
1265 1281 copy.reload
1266 1282 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
1267 1283 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
1268 1284 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
1269 1285 assert_equal copy.author, child_copy.author
1270 1286 end
1271 1287
1272 1288 def test_copy_as_a_child_of_copied_issue_should_not_copy_itself
1273 1289 parent = Issue.generate!
1274 1290 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
1275 1291 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
1276 1292
1277 1293 copy = parent.reload.copy
1278 1294 copy.parent_issue_id = parent.id
1279 1295 copy.author = User.find(7)
1280 1296 assert_difference 'Issue.count', 3 do
1281 1297 assert copy.save
1282 1298 end
1283 1299 parent.reload
1284 1300 copy.reload
1285 1301 assert_equal parent, copy.parent
1286 1302 assert_equal 3, parent.children.count
1287 1303 assert_equal 5, parent.descendants.count
1288 1304 assert_equal 2, copy.children.count
1289 1305 assert_equal 2, copy.descendants.count
1290 1306 end
1291 1307
1292 1308 def test_copy_as_a_descendant_of_copied_issue_should_not_copy_itself
1293 1309 parent = Issue.generate!
1294 1310 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
1295 1311 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
1296 1312
1297 1313 copy = parent.reload.copy
1298 1314 copy.parent_issue_id = child1.id
1299 1315 copy.author = User.find(7)
1300 1316 assert_difference 'Issue.count', 3 do
1301 1317 assert copy.save
1302 1318 end
1303 1319 parent.reload
1304 1320 child1.reload
1305 1321 copy.reload
1306 1322 assert_equal child1, copy.parent
1307 1323 assert_equal 2, parent.children.count
1308 1324 assert_equal 5, parent.descendants.count
1309 1325 assert_equal 1, child1.children.count
1310 1326 assert_equal 3, child1.descendants.count
1311 1327 assert_equal 2, copy.children.count
1312 1328 assert_equal 2, copy.descendants.count
1313 1329 end
1314 1330
1315 1331 def test_copy_should_copy_subtasks_to_target_project
1316 1332 issue = Issue.generate_with_descendants!
1317 1333
1318 1334 copy = issue.copy(:project_id => 3)
1319 1335 assert_difference 'Issue.count', 1+issue.descendants.count do
1320 1336 assert copy.save
1321 1337 end
1322 1338 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
1323 1339 end
1324 1340
1325 1341 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
1326 1342 issue = Issue.generate_with_descendants!
1327 1343
1328 1344 copy = issue.reload.copy
1329 1345 assert_difference 'Issue.count', 1+issue.descendants.count do
1330 1346 assert copy.save
1331 1347 assert copy.save
1332 1348 end
1333 1349 end
1334 1350
1335 1351 def test_copy_should_clear_closed_on
1336 1352 copied_open = Issue.find(8).copy(:status_id => 1)
1337 1353 assert copied_open.save
1338 1354 assert_nil copied_open.closed_on
1339 1355
1340 1356 copied_closed = Issue.find(8).copy
1341 1357 assert copied_closed.save
1342 1358 assert_not_nil copied_closed.closed_on
1343 1359 end
1344 1360
1345 1361 def test_should_not_call_after_project_change_on_creation
1346 1362 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1347 1363 :subject => 'Test', :author_id => 1)
1348 1364 issue.expects(:after_project_change).never
1349 1365 issue.save!
1350 1366 end
1351 1367
1352 1368 def test_should_not_call_after_project_change_on_update
1353 1369 issue = Issue.find(1)
1354 1370 issue.project = Project.find(1)
1355 1371 issue.subject = 'No project change'
1356 1372 issue.expects(:after_project_change).never
1357 1373 issue.save!
1358 1374 end
1359 1375
1360 1376 def test_should_call_after_project_change_on_project_change
1361 1377 issue = Issue.find(1)
1362 1378 issue.project = Project.find(2)
1363 1379 issue.expects(:after_project_change).once
1364 1380 issue.save!
1365 1381 end
1366 1382
1367 1383 def test_adding_journal_should_update_timestamp
1368 1384 issue = Issue.find(1)
1369 1385 updated_on_was = issue.updated_on
1370 1386
1371 1387 issue.init_journal(User.first, "Adding notes")
1372 1388 assert_difference 'Journal.count' do
1373 1389 assert issue.save
1374 1390 end
1375 1391 issue.reload
1376 1392
1377 1393 assert_not_equal updated_on_was, issue.updated_on
1378 1394 end
1379 1395
1380 1396 def test_should_close_duplicates
1381 1397 # Create 3 issues
1382 1398 issue1 = Issue.generate!
1383 1399 issue2 = Issue.generate!
1384 1400 issue3 = Issue.generate!
1385 1401
1386 1402 # 2 is a dupe of 1
1387 1403 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
1388 1404 :relation_type => IssueRelation::TYPE_DUPLICATES)
1389 1405 # And 3 is a dupe of 2
1390 1406 # IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1391 1407 # :relation_type => IssueRelation::TYPE_DUPLICATES)
1392 1408 # And 3 is a dupe of 1 (circular duplicates)
1393 1409 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
1394 1410 :relation_type => IssueRelation::TYPE_DUPLICATES)
1395 1411
1396 1412 assert issue1.reload.duplicates.include?(issue2)
1397 1413
1398 1414 # Closing issue 1
1399 1415 issue1.init_journal(User.first, "Closing issue1")
1400 1416 issue1.status = IssueStatus.where(:is_closed => true).first
1401 1417 assert issue1.save
1402 1418 # 2 and 3 should be also closed
1403 1419 assert issue2.reload.closed?
1404 1420 assert issue3.reload.closed?
1405 1421 end
1406 1422
1407 1423 def test_should_close_duplicates_with_private_notes
1408 1424 issue = Issue.generate!
1409 1425 duplicate = Issue.generate!
1410 1426 IssueRelation.create!(:issue_from => duplicate, :issue_to => issue,
1411 1427 :relation_type => IssueRelation::TYPE_DUPLICATES)
1412 1428 assert issue.reload.duplicates.include?(duplicate)
1413 1429
1414 1430 # Closing issue with private notes
1415 1431 issue.init_journal(User.first, "Private notes")
1416 1432 issue.private_notes = true
1417 1433 issue.status = IssueStatus.where(:is_closed => true).first
1418 1434 assert_save issue
1419 1435
1420 1436 duplicate.reload
1421 1437 assert journal = duplicate.journals.detect {|journal| journal.notes == "Private notes"}
1422 1438 assert_equal true, journal.private_notes
1423 1439 end
1424 1440
1425 1441 def test_should_not_close_duplicated_issue
1426 1442 issue1 = Issue.generate!
1427 1443 issue2 = Issue.generate!
1428 1444
1429 1445 # 2 is a dupe of 1
1430 1446 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
1431 1447 :relation_type => IssueRelation::TYPE_DUPLICATES)
1432 1448 # 2 is a dup of 1 but 1 is not a duplicate of 2
1433 1449 assert !issue2.reload.duplicates.include?(issue1)
1434 1450
1435 1451 # Closing issue 2
1436 1452 issue2.init_journal(User.first, "Closing issue2")
1437 1453 issue2.status = IssueStatus.where(:is_closed => true).first
1438 1454 assert issue2.save
1439 1455 # 1 should not be also closed
1440 1456 assert !issue1.reload.closed?
1441 1457 end
1442 1458
1443 1459 def test_assignable_versions
1444 1460 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1445 1461 :status_id => 1, :fixed_version_id => 1,
1446 1462 :subject => 'New issue')
1447 1463 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
1448 1464 end
1449 1465
1450 1466 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
1451 1467 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1452 1468 :status_id => 1, :fixed_version_id => 1,
1453 1469 :subject => 'New issue')
1454 1470 assert !issue.save
1455 1471 assert_not_equal [], issue.errors[:fixed_version_id]
1456 1472 end
1457 1473
1458 1474 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
1459 1475 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1460 1476 :status_id => 1, :fixed_version_id => 2,
1461 1477 :subject => 'New issue')
1462 1478 assert !issue.save
1463 1479 assert_not_equal [], issue.errors[:fixed_version_id]
1464 1480 end
1465 1481
1466 1482 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
1467 1483 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1468 1484 :status_id => 1, :fixed_version_id => 3,
1469 1485 :subject => 'New issue')
1470 1486 assert issue.save
1471 1487 end
1472 1488
1473 1489 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
1474 1490 issue = Issue.find(11)
1475 1491 assert_equal 'closed', issue.fixed_version.status
1476 1492 issue.subject = 'Subject changed'
1477 1493 assert issue.save
1478 1494 end
1479 1495
1480 1496 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
1481 1497 issue = Issue.find(11)
1482 1498 issue.status_id = 1
1483 1499 assert !issue.save
1484 1500 assert_not_equal [], issue.errors[:base]
1485 1501 end
1486 1502
1487 1503 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
1488 1504 issue = Issue.find(11)
1489 1505 issue.status_id = 1
1490 1506 issue.fixed_version_id = 3
1491 1507 assert issue.save
1492 1508 end
1493 1509
1494 1510 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
1495 1511 issue = Issue.find(12)
1496 1512 assert_equal 'locked', issue.fixed_version.status
1497 1513 issue.status_id = 1
1498 1514 assert issue.save
1499 1515 end
1500 1516
1501 1517 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
1502 1518 issue = Issue.find(2)
1503 1519 assert_equal 2, issue.fixed_version_id
1504 1520 issue.project_id = 3
1505 1521 assert_nil issue.fixed_version_id
1506 1522 issue.fixed_version_id = 2
1507 1523 assert !issue.save
1508 1524 assert_include 'Target version is not included in the list', issue.errors.full_messages
1509 1525 end
1510 1526
1511 1527 def test_should_keep_shared_version_when_changing_project
1512 1528 Version.find(2).update_attribute :sharing, 'tree'
1513 1529
1514 1530 issue = Issue.find(2)
1515 1531 assert_equal 2, issue.fixed_version_id
1516 1532 issue.project_id = 3
1517 1533 assert_equal 2, issue.fixed_version_id
1518 1534 assert issue.save
1519 1535 end
1520 1536
1521 1537 def test_allowed_target_projects_should_include_projects_with_issue_tracking_enabled
1522 1538 assert_include Project.find(2), Issue.allowed_target_projects(User.find(2))
1523 1539 end
1524 1540
1525 1541 def test_allowed_target_projects_should_not_include_projects_with_issue_tracking_disabled
1526 1542 Project.find(2).disable_module! :issue_tracking
1527 1543 assert_not_include Project.find(2), Issue.allowed_target_projects(User.find(2))
1528 1544 end
1529 1545
1530 1546 def test_allowed_target_projects_should_not_include_projects_without_trackers
1531 1547 project = Project.generate!(:tracker_ids => [])
1532 1548 assert project.trackers.empty?
1533 1549 assert_not_include project, Issue.allowed_target_projects(User.find(1))
1534 1550 end
1535 1551
1536 1552 def test_allowed_target_trackers_with_one_role_allowed_on_all_trackers
1537 1553 user = User.generate!
1538 1554 role = Role.generate!
1539 1555 role.add_permission! :add_issues
1540 1556 role.set_permission_trackers :add_issues, :all
1541 1557 role.save!
1542 1558 User.add_to_project(user, Project.find(1), role)
1543 1559
1544 1560 assert_equal [1, 2, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1545 1561 end
1546 1562
1547 1563 def test_allowed_target_trackers_with_one_role_allowed_on_some_trackers
1548 1564 user = User.generate!
1549 1565 role = Role.generate!
1550 1566 role.add_permission! :add_issues
1551 1567 role.set_permission_trackers :add_issues, [1, 3]
1552 1568 role.save!
1553 1569 User.add_to_project(user, Project.find(1), role)
1554 1570
1555 1571 assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1556 1572 end
1557 1573
1558 1574 def test_allowed_target_trackers_with_two_roles_allowed_on_some_trackers
1559 1575 user = User.generate!
1560 1576 role1 = Role.generate!
1561 1577 role1.add_permission! :add_issues
1562 1578 role1.set_permission_trackers :add_issues, [1]
1563 1579 role1.save!
1564 1580 role2 = Role.generate!
1565 1581 role2.add_permission! :add_issues
1566 1582 role2.set_permission_trackers :add_issues, [3]
1567 1583 role2.save!
1568 1584 User.add_to_project(user, Project.find(1), [role1, role2])
1569 1585
1570 1586 assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1571 1587 end
1572 1588
1573 1589 def test_allowed_target_trackers_with_two_roles_allowed_on_all_trackers_and_some_trackers
1574 1590 user = User.generate!
1575 1591 role1 = Role.generate!
1576 1592 role1.add_permission! :add_issues
1577 1593 role1.set_permission_trackers :add_issues, :all
1578 1594 role1.save!
1579 1595 role2 = Role.generate!
1580 1596 role2.add_permission! :add_issues
1581 1597 role2.set_permission_trackers :add_issues, [1, 3]
1582 1598 role2.save!
1583 1599 User.add_to_project(user, Project.find(1), [role1, role2])
1584 1600
1585 1601 assert_equal [1, 2, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1586 1602 end
1587 1603
1588 1604 def test_allowed_target_trackers_should_not_consider_roles_without_add_issues_permission
1589 1605 user = User.generate!
1590 1606 role1 = Role.generate!
1591 1607 role1.remove_permission! :add_issues
1592 1608 role1.set_permission_trackers :add_issues, :all
1593 1609 role1.save!
1594 1610 role2 = Role.generate!
1595 1611 role2.add_permission! :add_issues
1596 1612 role2.set_permission_trackers :add_issues, [1, 3]
1597 1613 role2.save!
1598 1614 User.add_to_project(user, Project.find(1), [role1, role2])
1599 1615
1600 1616 assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1601 1617 end
1602 1618
1603 1619 def test_allowed_target_trackers_without_project_should_be_empty
1604 1620 issue = Issue.new
1605 1621 assert_nil issue.project
1606 1622 assert_equal [], issue.allowed_target_trackers(User.find(2)).ids
1607 1623 end
1608 1624
1609 1625 def test_allowed_target_trackers_should_include_current_tracker
1610 1626 user = User.generate!
1611 1627 role = Role.generate!
1612 1628 role.add_permission! :add_issues
1613 1629 role.set_permission_trackers :add_issues, [3]
1614 1630 role.save!
1615 1631 User.add_to_project(user, Project.find(1), role)
1616 1632
1617 1633 issue = Issue.generate!(:project => Project.find(1), :tracker => Tracker.find(1))
1618 1634 assert_equal [1, 3], issue.allowed_target_trackers(user).ids.sort
1619 1635 end
1620 1636
1621 1637 def test_move_to_another_project_with_same_category
1622 1638 issue = Issue.find(1)
1623 1639 issue.project = Project.find(2)
1624 1640 assert issue.save
1625 1641 issue.reload
1626 1642 assert_equal 2, issue.project_id
1627 1643 # Category changes
1628 1644 assert_equal 4, issue.category_id
1629 1645 # Make sure time entries were move to the target project
1630 1646 assert_equal 2, issue.time_entries.first.project_id
1631 1647 end
1632 1648
1633 1649 def test_move_to_another_project_without_same_category
1634 1650 issue = Issue.find(2)
1635 1651 issue.project = Project.find(2)
1636 1652 assert issue.save
1637 1653 issue.reload
1638 1654 assert_equal 2, issue.project_id
1639 1655 # Category cleared
1640 1656 assert_nil issue.category_id
1641 1657 end
1642 1658
1643 1659 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1644 1660 issue = Issue.find(1)
1645 1661 issue.update_attribute(:fixed_version_id, 1)
1646 1662 issue.project = Project.find(2)
1647 1663 assert issue.save
1648 1664 issue.reload
1649 1665 assert_equal 2, issue.project_id
1650 1666 # Cleared fixed_version
1651 1667 assert_equal nil, issue.fixed_version
1652 1668 end
1653 1669
1654 1670 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1655 1671 issue = Issue.find(1)
1656 1672 issue.update_attribute(:fixed_version_id, 4)
1657 1673 issue.project = Project.find(5)
1658 1674 assert issue.save
1659 1675 issue.reload
1660 1676 assert_equal 5, issue.project_id
1661 1677 # Keep fixed_version
1662 1678 assert_equal 4, issue.fixed_version_id
1663 1679 end
1664 1680
1665 1681 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1666 1682 issue = Issue.find(1)
1667 1683 issue.update_attribute(:fixed_version_id, 1)
1668 1684 issue.project = Project.find(5)
1669 1685 assert issue.save
1670 1686 issue.reload
1671 1687 assert_equal 5, issue.project_id
1672 1688 # Cleared fixed_version
1673 1689 assert_equal nil, issue.fixed_version
1674 1690 end
1675 1691
1676 1692 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1677 1693 issue = Issue.find(1)
1678 1694 issue.update_attribute(:fixed_version_id, 7)
1679 1695 issue.project = Project.find(2)
1680 1696 assert issue.save
1681 1697 issue.reload
1682 1698 assert_equal 2, issue.project_id
1683 1699 # Keep fixed_version
1684 1700 assert_equal 7, issue.fixed_version_id
1685 1701 end
1686 1702
1687 1703 def test_move_to_another_project_should_keep_parent_if_valid
1688 1704 issue = Issue.find(1)
1689 1705 issue.update_attribute(:parent_issue_id, 2)
1690 1706 issue.project = Project.find(3)
1691 1707 assert issue.save
1692 1708 issue.reload
1693 1709 assert_equal 2, issue.parent_id
1694 1710 end
1695 1711
1696 1712 def test_move_to_another_project_should_clear_parent_if_not_valid
1697 1713 issue = Issue.find(1)
1698 1714 issue.update_attribute(:parent_issue_id, 2)
1699 1715 issue.project = Project.find(2)
1700 1716 assert issue.save
1701 1717 issue.reload
1702 1718 assert_nil issue.parent_id
1703 1719 end
1704 1720
1705 1721 def test_move_to_another_project_with_disabled_tracker
1706 1722 issue = Issue.find(1)
1707 1723 target = Project.find(2)
1708 1724 target.tracker_ids = [3]
1709 1725 target.save
1710 1726 issue.project = target
1711 1727 assert issue.save
1712 1728 issue.reload
1713 1729 assert_equal 2, issue.project_id
1714 1730 assert_equal 3, issue.tracker_id
1715 1731 end
1716 1732
1717 1733 def test_copy_to_the_same_project
1718 1734 issue = Issue.find(1)
1719 1735 copy = issue.copy
1720 1736 assert_difference 'Issue.count' do
1721 1737 copy.save!
1722 1738 end
1723 1739 assert_kind_of Issue, copy
1724 1740 assert_equal issue.project, copy.project
1725 1741 assert_equal "125", copy.custom_value_for(2).value
1726 1742 end
1727 1743
1728 1744 def test_copy_to_another_project_and_tracker
1729 1745 issue = Issue.find(1)
1730 1746 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1731 1747 assert_difference 'Issue.count' do
1732 1748 copy.save!
1733 1749 end
1734 1750 copy.reload
1735 1751 assert_kind_of Issue, copy
1736 1752 assert_equal Project.find(3), copy.project
1737 1753 assert_equal Tracker.find(2), copy.tracker
1738 1754 # Custom field #2 is not associated with target tracker
1739 1755 assert_nil copy.custom_value_for(2)
1740 1756 end
1741 1757
1742 1758 test "#copy should not create a journal" do
1743 1759 copy = Issue.find(1).copy({:project_id => 3, :tracker_id => 2, :assigned_to_id => 3}, :link => false)
1744 1760 copy.save!
1745 1761 assert_equal 0, copy.reload.journals.size
1746 1762 end
1747 1763
1748 1764 test "#copy should allow assigned_to changes" do
1749 1765 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1750 1766 assert_equal 3, copy.assigned_to_id
1751 1767 end
1752 1768
1753 1769 test "#copy should allow status changes" do
1754 1770 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1755 1771 assert_equal 2, copy.status_id
1756 1772 end
1757 1773
1758 1774 test "#copy should allow start date changes" do
1759 1775 date = Date.today
1760 1776 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1761 1777 assert_equal date, copy.start_date
1762 1778 end
1763 1779
1764 1780 test "#copy should allow due date changes" do
1765 1781 date = Date.today
1766 1782 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1767 1783 assert_equal date, copy.due_date
1768 1784 end
1769 1785
1770 1786 test "#copy should set current user as author" do
1771 1787 User.current = User.find(9)
1772 1788 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2)
1773 1789 assert_equal User.current, copy.author
1774 1790 end
1775 1791
1776 1792 test "#copy should create a journal with notes" do
1777 1793 date = Date.today
1778 1794 notes = "Notes added when copying"
1779 1795 copy = Issue.find(1).copy({:project_id => 3, :tracker_id => 2, :start_date => date}, :link => false)
1780 1796 copy.init_journal(User.current, notes)
1781 1797 copy.save!
1782 1798
1783 1799 assert_equal 1, copy.journals.size
1784 1800 journal = copy.journals.first
1785 1801 assert_equal 0, journal.details.size
1786 1802 assert_equal notes, journal.notes
1787 1803 end
1788 1804
1789 1805 def test_valid_parent_project
1790 1806 issue = Issue.find(1)
1791 1807 issue_in_same_project = Issue.find(2)
1792 1808 issue_in_child_project = Issue.find(5)
1793 1809 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1794 1810 issue_in_other_child_project = Issue.find(6)
1795 1811 issue_in_different_tree = Issue.find(4)
1796 1812
1797 1813 with_settings :cross_project_subtasks => '' do
1798 1814 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1799 1815 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1800 1816 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1801 1817 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1802 1818 end
1803 1819
1804 1820 with_settings :cross_project_subtasks => 'system' do
1805 1821 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1806 1822 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1807 1823 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1808 1824 end
1809 1825
1810 1826 with_settings :cross_project_subtasks => 'tree' do
1811 1827 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1812 1828 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1813 1829 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1814 1830 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1815 1831
1816 1832 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1817 1833 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1818 1834 end
1819 1835
1820 1836 with_settings :cross_project_subtasks => 'descendants' do
1821 1837 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1822 1838 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1823 1839 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1824 1840 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1825 1841
1826 1842 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1827 1843 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1828 1844 end
1829 1845 end
1830 1846
1831 1847 def test_recipients_should_include_previous_assignee
1832 1848 user = User.find(3)
1833 1849 user.members.update_all ["mail_notification = ?", false]
1834 1850 user.update_attribute :mail_notification, 'only_assigned'
1835 1851
1836 1852 issue = Issue.find(2)
1837 1853 issue.assigned_to = nil
1838 1854 assert_include user.mail, issue.recipients
1839 1855 issue.save!
1840 1856 assert !issue.recipients.include?(user.mail)
1841 1857 end
1842 1858
1843 1859 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1844 1860 issue = Issue.find(12)
1845 1861 assert issue.recipients.include?(issue.author.mail)
1846 1862 # copy the issue to a private project
1847 1863 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1848 1864 # author is not a member of project anymore
1849 1865 assert !copy.recipients.include?(copy.author.mail)
1850 1866 end
1851 1867
1852 1868 def test_recipients_should_include_the_assigned_group_members
1853 1869 group_member = User.generate!
1854 1870 group = Group.generate!
1855 1871 group.users << group_member
1856 1872
1857 1873 issue = Issue.find(12)
1858 1874 issue.assigned_to = group
1859 1875 assert issue.recipients.include?(group_member.mail)
1860 1876 end
1861 1877
1862 1878 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1863 1879 user = User.find(3)
1864 1880 issue = Issue.find(9)
1865 1881 Watcher.create!(:user => user, :watchable => issue)
1866 1882 assert issue.watched_by?(user)
1867 1883 assert !issue.watcher_recipients.include?(user.mail)
1868 1884 end
1869 1885
1870 1886 def test_issue_destroy
1871 1887 Issue.find(1).destroy
1872 1888 assert_nil Issue.find_by_id(1)
1873 1889 assert_nil TimeEntry.find_by_issue_id(1)
1874 1890 end
1875 1891
1876 1892 def test_destroy_should_delete_time_entries_custom_values
1877 1893 issue = Issue.generate!
1878 1894 time_entry = TimeEntry.generate!(:issue => issue, :custom_field_values => {10 => '1'})
1879 1895
1880 1896 assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do
1881 1897 assert issue.destroy
1882 1898 end
1883 1899 end
1884 1900
1885 1901 def test_destroying_a_deleted_issue_should_not_raise_an_error
1886 1902 issue = Issue.find(1)
1887 1903 Issue.find(1).destroy
1888 1904
1889 1905 assert_nothing_raised do
1890 1906 assert_no_difference 'Issue.count' do
1891 1907 issue.destroy
1892 1908 end
1893 1909 assert issue.destroyed?
1894 1910 end
1895 1911 end
1896 1912
1897 1913 def test_destroying_a_stale_issue_should_not_raise_an_error
1898 1914 issue = Issue.find(1)
1899 1915 Issue.find(1).update_attribute :subject, "Updated"
1900 1916
1901 1917 assert_nothing_raised do
1902 1918 assert_difference 'Issue.count', -1 do
1903 1919 issue.destroy
1904 1920 end
1905 1921 assert issue.destroyed?
1906 1922 end
1907 1923 end
1908 1924
1909 1925 def test_blocked
1910 1926 blocked_issue = Issue.find(9)
1911 1927 blocking_issue = Issue.find(10)
1912 1928
1913 1929 assert blocked_issue.blocked?
1914 1930 assert !blocking_issue.blocked?
1915 1931 end
1916 1932
1917 1933 def test_blocked_issues_dont_allow_closed_statuses
1918 1934 blocked_issue = Issue.find(9)
1919 1935
1920 1936 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1921 1937 assert !allowed_statuses.empty?
1922 1938 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1923 1939 assert closed_statuses.empty?
1924 1940 end
1925 1941
1926 1942 def test_unblocked_issues_allow_closed_statuses
1927 1943 blocking_issue = Issue.find(10)
1928 1944
1929 1945 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1930 1946 assert !allowed_statuses.empty?
1931 1947 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1932 1948 assert !closed_statuses.empty?
1933 1949 end
1934 1950
1935 1951 def test_reschedule_an_issue_without_dates
1936 1952 with_settings :non_working_week_days => [] do
1937 1953 issue = Issue.new(:start_date => nil, :due_date => nil)
1938 1954 issue.reschedule_on '2012-10-09'.to_date
1939 1955 assert_equal '2012-10-09'.to_date, issue.start_date
1940 1956 assert_equal '2012-10-09'.to_date, issue.due_date
1941 1957 end
1942 1958
1943 1959 with_settings :non_working_week_days => %w(6 7) do
1944 1960 issue = Issue.new(:start_date => nil, :due_date => nil)
1945 1961 issue.reschedule_on '2012-10-09'.to_date
1946 1962 assert_equal '2012-10-09'.to_date, issue.start_date
1947 1963 assert_equal '2012-10-09'.to_date, issue.due_date
1948 1964
1949 1965 issue = Issue.new(:start_date => nil, :due_date => nil)
1950 1966 issue.reschedule_on '2012-10-13'.to_date
1951 1967 assert_equal '2012-10-15'.to_date, issue.start_date
1952 1968 assert_equal '2012-10-15'.to_date, issue.due_date
1953 1969 end
1954 1970 end
1955 1971
1956 1972 def test_reschedule_an_issue_with_start_date
1957 1973 with_settings :non_working_week_days => [] do
1958 1974 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1959 1975 issue.reschedule_on '2012-10-13'.to_date
1960 1976 assert_equal '2012-10-13'.to_date, issue.start_date
1961 1977 assert_equal '2012-10-13'.to_date, issue.due_date
1962 1978 end
1963 1979
1964 1980 with_settings :non_working_week_days => %w(6 7) do
1965 1981 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1966 1982 issue.reschedule_on '2012-10-11'.to_date
1967 1983 assert_equal '2012-10-11'.to_date, issue.start_date
1968 1984 assert_equal '2012-10-11'.to_date, issue.due_date
1969 1985
1970 1986 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1971 1987 issue.reschedule_on '2012-10-13'.to_date
1972 1988 assert_equal '2012-10-15'.to_date, issue.start_date
1973 1989 assert_equal '2012-10-15'.to_date, issue.due_date
1974 1990 end
1975 1991 end
1976 1992
1977 1993 def test_reschedule_an_issue_with_start_and_due_dates
1978 1994 with_settings :non_working_week_days => [] do
1979 1995 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1980 1996 issue.reschedule_on '2012-10-13'.to_date
1981 1997 assert_equal '2012-10-13'.to_date, issue.start_date
1982 1998 assert_equal '2012-10-19'.to_date, issue.due_date
1983 1999 end
1984 2000
1985 2001 with_settings :non_working_week_days => %w(6 7) do
1986 2002 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1987 2003 issue.reschedule_on '2012-10-11'.to_date
1988 2004 assert_equal '2012-10-11'.to_date, issue.start_date
1989 2005 assert_equal '2012-10-23'.to_date, issue.due_date
1990 2006
1991 2007 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1992 2008 issue.reschedule_on '2012-10-13'.to_date
1993 2009 assert_equal '2012-10-15'.to_date, issue.start_date
1994 2010 assert_equal '2012-10-25'.to_date, issue.due_date
1995 2011 end
1996 2012 end
1997 2013
1998 2014 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1999 2015 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2000 2016 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2001 2017 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
2002 2018 :relation_type => IssueRelation::TYPE_PRECEDES)
2003 2019 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
2004 2020
2005 2021 issue1.reload
2006 2022 issue1.due_date = '2012-10-23'
2007 2023 issue1.save!
2008 2024 issue2.reload
2009 2025 assert_equal Date.parse('2012-10-24'), issue2.start_date
2010 2026 assert_equal Date.parse('2012-10-26'), issue2.due_date
2011 2027 end
2012 2028
2013 2029 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
2014 2030 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2015 2031 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2016 2032 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
2017 2033 :relation_type => IssueRelation::TYPE_PRECEDES)
2018 2034 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
2019 2035
2020 2036 issue1.reload
2021 2037 issue1.start_date = '2012-09-17'
2022 2038 issue1.due_date = '2012-09-18'
2023 2039 issue1.save!
2024 2040 issue2.reload
2025 2041 assert_equal Date.parse('2012-09-19'), issue2.start_date
2026 2042 assert_equal Date.parse('2012-09-21'), issue2.due_date
2027 2043 end
2028 2044
2029 2045 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
2030 2046 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2031 2047 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2032 2048 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
2033 2049 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
2034 2050 :relation_type => IssueRelation::TYPE_PRECEDES)
2035 2051 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
2036 2052 :relation_type => IssueRelation::TYPE_PRECEDES)
2037 2053 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
2038 2054
2039 2055 issue1.reload
2040 2056 issue1.start_date = '2012-09-17'
2041 2057 issue1.due_date = '2012-09-18'
2042 2058 issue1.save!
2043 2059 issue2.reload
2044 2060 # Issue 2 must start after Issue 3
2045 2061 assert_equal Date.parse('2012-10-03'), issue2.start_date
2046 2062 assert_equal Date.parse('2012-10-05'), issue2.due_date
2047 2063 end
2048 2064
2049 2065 def test_rescheduling_a_stale_issue_should_not_raise_an_error
2050 2066 with_settings :non_working_week_days => [] do
2051 2067 stale = Issue.find(1)
2052 2068 issue = Issue.find(1)
2053 2069 issue.subject = "Updated"
2054 2070 issue.save!
2055 2071 date = 10.days.from_now.to_date
2056 2072 assert_nothing_raised do
2057 2073 stale.reschedule_on!(date)
2058 2074 end
2059 2075 assert_equal date, stale.reload.start_date
2060 2076 end
2061 2077 end
2062 2078
2063 2079 def test_child_issue_should_consider_parent_soonest_start_on_create
2064 2080 set_language_if_valid 'en'
2065 2081 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2066 2082 issue2 = Issue.generate!(:start_date => '2012-10-18', :due_date => '2012-10-20')
2067 2083 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
2068 2084 :relation_type => IssueRelation::TYPE_PRECEDES)
2069 2085 issue1.reload
2070 2086 issue2.reload
2071 2087 assert_equal Date.parse('2012-10-18'), issue2.start_date
2072 2088
2073 2089 with_settings :date_format => '%m/%d/%Y' do
2074 2090 child = Issue.new(:parent_issue_id => issue2.id, :start_date => '2012-10-16',
2075 2091 :project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Child', :author_id => 1)
2076 2092 assert !child.valid?
2077 2093 assert_include 'Start date cannot be earlier than 10/18/2012 because of preceding issues', child.errors.full_messages
2078 2094 assert_equal Date.parse('2012-10-18'), child.soonest_start
2079 2095 child.start_date = '2012-10-18'
2080 2096 assert child.save
2081 2097 end
2082 2098 end
2083 2099
2084 2100 def test_setting_parent_to_a_an_issue_that_precedes_should_not_validate
2085 2101 # tests that 3 cannot have 1 as parent:
2086 2102 #
2087 2103 # 1 -> 2 -> 3
2088 2104 #
2089 2105 set_language_if_valid 'en'
2090 2106 issue1 = Issue.generate!
2091 2107 issue2 = Issue.generate!
2092 2108 issue3 = Issue.generate!
2093 2109 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
2094 2110 IssueRelation.create!(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
2095 2111 issue3.reload
2096 2112 issue3.parent_issue_id = issue1.id
2097 2113 assert !issue3.valid?
2098 2114 assert_include 'Parent task is invalid', issue3.errors.full_messages
2099 2115 end
2100 2116
2101 2117 def test_setting_parent_to_a_an_issue_that_follows_should_not_validate
2102 2118 # tests that 1 cannot have 3 as parent:
2103 2119 #
2104 2120 # 1 -> 2 -> 3
2105 2121 #
2106 2122 set_language_if_valid 'en'
2107 2123 issue1 = Issue.generate!
2108 2124 issue2 = Issue.generate!
2109 2125 issue3 = Issue.generate!
2110 2126 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
2111 2127 IssueRelation.create!(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
2112 2128 issue1.reload
2113 2129 issue1.parent_issue_id = issue3.id
2114 2130 assert !issue1.valid?
2115 2131 assert_include 'Parent task is invalid', issue1.errors.full_messages
2116 2132 end
2117 2133
2118 2134 def test_setting_parent_to_a_an_issue_that_precedes_through_hierarchy_should_not_validate
2119 2135 # tests that 4 cannot have 1 as parent:
2120 2136 # changing the due date of 4 would update the end date of 1 which would reschedule 2
2121 2137 # which would change the end date of 3 which would reschedule 4 and so on...
2122 2138 #
2123 2139 # 3 -> 4
2124 2140 # ^
2125 2141 # 1 -> 2
2126 2142 #
2127 2143 set_language_if_valid 'en'
2128 2144 issue1 = Issue.generate!
2129 2145 issue2 = Issue.generate!
2130 2146 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
2131 2147 issue3 = Issue.generate!
2132 2148 issue2.reload
2133 2149 issue2.parent_issue_id = issue3.id
2134 2150 issue2.save!
2135 2151 issue4 = Issue.generate!
2136 2152 IssueRelation.create!(:issue_from => issue3, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
2137 2153 issue4.reload
2138 2154 issue4.parent_issue_id = issue1.id
2139 2155 assert !issue4.valid?
2140 2156 assert_include 'Parent task is invalid', issue4.errors.full_messages
2141 2157 end
2142 2158
2143 2159 def test_issue_and_following_issue_should_be_able_to_be_moved_to_the_same_parent
2144 2160 set_language_if_valid 'en'
2145 2161 issue1 = Issue.generate!
2146 2162 issue2 = Issue.generate!
2147 2163 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_FOLLOWS)
2148 2164 parent = Issue.generate!
2149 2165 issue1.reload.parent_issue_id = parent.id
2150 2166 assert_save issue1
2151 2167 parent.reload
2152 2168 issue2.reload.parent_issue_id = parent.id
2153 2169 assert_save issue2
2154 2170 assert IssueRelation.exists?(relation.id)
2155 2171 end
2156 2172
2157 2173 def test_issue_and_preceding_issue_should_be_able_to_be_moved_to_the_same_parent
2158 2174 set_language_if_valid 'en'
2159 2175 issue1 = Issue.generate!
2160 2176 issue2 = Issue.generate!
2161 2177 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_PRECEDES)
2162 2178 parent = Issue.generate!
2163 2179 issue1.reload.parent_issue_id = parent.id
2164 2180 assert_save issue1
2165 2181 parent.reload
2166 2182 issue2.reload.parent_issue_id = parent.id
2167 2183 assert_save issue2
2168 2184 assert IssueRelation.exists?(relation.id)
2169 2185 end
2170 2186
2171 2187 def test_issue_and_blocked_issue_should_be_able_to_be_moved_to_the_same_parent
2172 2188 set_language_if_valid 'en'
2173 2189 issue1 = Issue.generate!
2174 2190 issue2 = Issue.generate!
2175 2191 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_BLOCKED)
2176 2192 parent = Issue.generate!
2177 2193 issue1.reload.parent_issue_id = parent.id
2178 2194 assert_save issue1
2179 2195 parent.reload
2180 2196 issue2.reload.parent_issue_id = parent.id
2181 2197 assert_save issue2
2182 2198 assert IssueRelation.exists?(relation.id)
2183 2199 end
2184 2200
2185 2201 def test_issue_and_blocking_issue_should_be_able_to_be_moved_to_the_same_parent
2186 2202 set_language_if_valid 'en'
2187 2203 issue1 = Issue.generate!
2188 2204 issue2 = Issue.generate!
2189 2205 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_BLOCKS)
2190 2206 parent = Issue.generate!
2191 2207 issue1.reload.parent_issue_id = parent.id
2192 2208 assert_save issue1
2193 2209 parent.reload
2194 2210 issue2.reload.parent_issue_id = parent.id
2195 2211 assert_save issue2
2196 2212 assert IssueRelation.exists?(relation.id)
2197 2213 end
2198 2214
2199 2215 def test_issue_copy_should_be_able_to_be_moved_to_the_same_parent_as_copied_issue
2200 2216 issue = Issue.generate!
2201 2217 parent = Issue.generate!
2202 2218 issue.parent_issue_id = parent.id
2203 2219 issue.save!
2204 2220 issue.reload
2205 2221
2206 2222 copy = Issue.new.copy_from(issue, :link => true)
2207 2223 relation = new_record(IssueRelation) do
2208 2224 copy.save!
2209 2225 end
2210 2226
2211 2227 copy.parent_issue_id = parent.id
2212 2228 assert_save copy
2213 2229 assert IssueRelation.exists?(relation.id)
2214 2230 end
2215 2231
2216 2232 def test_overdue
2217 2233 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
2218 2234 assert !Issue.new(:due_date => Date.today).overdue?
2219 2235 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
2220 2236 assert !Issue.new(:due_date => nil).overdue?
2221 2237 assert !Issue.new(:due_date => 1.day.ago.to_date,
2222 2238 :status => IssueStatus.where(:is_closed => true).first
2223 2239 ).overdue?
2224 2240 end
2225 2241
2226 2242 test "#behind_schedule? should be false if the issue has no start_date" do
2227 2243 assert !Issue.new(:start_date => nil,
2228 2244 :due_date => 1.day.from_now.to_date,
2229 2245 :done_ratio => 0).behind_schedule?
2230 2246 end
2231 2247
2232 2248 test "#behind_schedule? should be false if the issue has no end_date" do
2233 2249 assert !Issue.new(:start_date => 1.day.from_now.to_date,
2234 2250 :due_date => nil,
2235 2251 :done_ratio => 0).behind_schedule?
2236 2252 end
2237 2253
2238 2254 test "#behind_schedule? should be false if the issue has more done than it's calendar time" do
2239 2255 assert !Issue.new(:start_date => 50.days.ago.to_date,
2240 2256 :due_date => 50.days.from_now.to_date,
2241 2257 :done_ratio => 90).behind_schedule?
2242 2258 end
2243 2259
2244 2260 test "#behind_schedule? should be true if the issue hasn't been started at all" do
2245 2261 assert Issue.new(:start_date => 1.day.ago.to_date,
2246 2262 :due_date => 1.day.from_now.to_date,
2247 2263 :done_ratio => 0).behind_schedule?
2248 2264 end
2249 2265
2250 2266 test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do
2251 2267 assert Issue.new(:start_date => 100.days.ago.to_date,
2252 2268 :due_date => Date.today,
2253 2269 :done_ratio => 90).behind_schedule?
2254 2270 end
2255 2271
2256 2272 test "#assignable_users should be Users" do
2257 2273 assert_kind_of User, Issue.find(1).assignable_users.first
2258 2274 end
2259 2275
2260 2276 test "#assignable_users should include the issue author" do
2261 2277 non_project_member = User.generate!
2262 2278 issue = Issue.generate!(:author => non_project_member)
2263 2279
2264 2280 assert issue.assignable_users.include?(non_project_member)
2265 2281 end
2266 2282
2267 2283 def test_assignable_users_should_not_include_anonymous_user
2268 2284 issue = Issue.generate!(:author => User.anonymous)
2269 2285
2270 2286 assert !issue.assignable_users.include?(User.anonymous)
2271 2287 end
2272 2288
2273 2289 def test_assignable_users_should_not_include_locked_user
2274 2290 user = User.generate!
2275 2291 issue = Issue.generate!(:author => user)
2276 2292 user.lock!
2277 2293
2278 2294 assert !issue.assignable_users.include?(user)
2279 2295 end
2280 2296
2281 2297 test "#assignable_users should include the current assignee" do
2282 2298 user = User.generate!
2283 2299 issue = Issue.generate!(:assigned_to => user)
2284 2300 user.lock!
2285 2301
2286 2302 assert Issue.find(issue.id).assignable_users.include?(user)
2287 2303 end
2288 2304
2289 2305 test "#assignable_users should not show the issue author twice" do
2290 2306 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
2291 2307 assert_equal 2, assignable_user_ids.length
2292 2308
2293 2309 assignable_user_ids.each do |user_id|
2294 2310 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
2295 2311 "User #{user_id} appears more or less than once"
2296 2312 end
2297 2313 end
2298 2314
2299 2315 test "#assignable_users with issue_group_assignment should include groups" do
2300 2316 issue = Issue.new(:project => Project.find(2))
2301 2317
2302 2318 with_settings :issue_group_assignment => '1' do
2303 2319 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
2304 2320 assert issue.assignable_users.include?(Group.find(11))
2305 2321 end
2306 2322 end
2307 2323
2308 2324 test "#assignable_users without issue_group_assignment should not include groups" do
2309 2325 issue = Issue.new(:project => Project.find(2))
2310 2326
2311 2327 with_settings :issue_group_assignment => '0' do
2312 2328 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
2313 2329 assert !issue.assignable_users.include?(Group.find(11))
2314 2330 end
2315 2331 end
2316 2332
2317 2333 def test_assignable_users_should_not_include_builtin_groups
2318 2334 Member.create!(:project_id => 1, :principal => Group.non_member, :role_ids => [1])
2319 2335 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [1])
2320 2336 issue = Issue.new(:project => Project.find(1))
2321 2337
2322 2338 with_settings :issue_group_assignment => '1' do
2323 2339 assert_nil issue.assignable_users.detect {|u| u.is_a?(GroupBuiltin)}
2324 2340 end
2325 2341 end
2326 2342
2327 2343 def test_assignable_users_should_not_include_users_that_cannot_view_the_tracker
2328 2344 user = User.find(3)
2329 2345 role = Role.find(2)
2330 2346 role.set_permission_trackers :view_issues, [1, 3]
2331 2347 role.save!
2332 2348
2333 2349 issue1 = Issue.new(:project_id => 1, :tracker_id => 1)
2334 2350 issue2 = Issue.new(:project_id => 1, :tracker_id => 2)
2335 2351
2336 2352 assert_include user, issue1.assignable_users
2337 2353 assert_not_include user, issue2.assignable_users
2338 2354 end
2339 2355
2340 2356 def test_create_should_send_email_notification
2341 2357 ActionMailer::Base.deliveries.clear
2342 2358 issue = Issue.new(:project_id => 1, :tracker_id => 1,
2343 2359 :author_id => 3, :status_id => 1,
2344 2360 :priority => IssuePriority.all.first,
2345 2361 :subject => 'test_create', :estimated_hours => '1:30')
2346 2362 with_settings :notified_events => %w(issue_added) do
2347 2363 assert issue.save
2348 2364 assert_equal 1, ActionMailer::Base.deliveries.size
2349 2365 end
2350 2366 end
2351 2367
2352 2368 def test_create_should_send_one_email_notification_with_both_settings
2353 2369 ActionMailer::Base.deliveries.clear
2354 2370 issue = Issue.new(:project_id => 1, :tracker_id => 1,
2355 2371 :author_id => 3, :status_id => 1,
2356 2372 :priority => IssuePriority.all.first,
2357 2373 :subject => 'test_create', :estimated_hours => '1:30')
2358 2374 with_settings :notified_events => %w(issue_added issue_updated) do
2359 2375 assert issue.save
2360 2376 assert_equal 1, ActionMailer::Base.deliveries.size
2361 2377 end
2362 2378 end
2363 2379
2364 2380 def test_create_should_not_send_email_notification_with_no_setting
2365 2381 ActionMailer::Base.deliveries.clear
2366 2382 issue = Issue.new(:project_id => 1, :tracker_id => 1,
2367 2383 :author_id => 3, :status_id => 1,
2368 2384 :priority => IssuePriority.all.first,
2369 2385 :subject => 'test_create', :estimated_hours => '1:30')
2370 2386 with_settings :notified_events => [] do
2371 2387 assert issue.save
2372 2388 assert_equal 0, ActionMailer::Base.deliveries.size
2373 2389 end
2374 2390 end
2375 2391
2376 2392 def test_update_should_notify_previous_assignee
2377 2393 ActionMailer::Base.deliveries.clear
2378 2394 user = User.find(3)
2379 2395 user.members.update_all ["mail_notification = ?", false]
2380 2396 user.update_attribute :mail_notification, 'only_assigned'
2381 2397
2382 2398 with_settings :notified_events => %w(issue_updated) do
2383 2399 issue = Issue.find(2)
2384 2400 issue.init_journal User.find(1)
2385 2401 issue.assigned_to = nil
2386 2402 issue.save!
2387 2403 assert_include user.mail, ActionMailer::Base.deliveries.last.bcc
2388 2404 end
2389 2405 end
2390 2406
2391 2407 def test_stale_issue_should_not_send_email_notification
2392 2408 ActionMailer::Base.deliveries.clear
2393 2409 issue = Issue.find(1)
2394 2410 stale = Issue.find(1)
2395 2411
2396 2412 issue.init_journal(User.find(1))
2397 2413 issue.subject = 'Subjet update'
2398 2414 with_settings :notified_events => %w(issue_updated) do
2399 2415 assert issue.save
2400 2416 assert_equal 1, ActionMailer::Base.deliveries.size
2401 2417 ActionMailer::Base.deliveries.clear
2402 2418
2403 2419 stale.init_journal(User.find(1))
2404 2420 stale.subject = 'Another subjet update'
2405 2421 assert_raise ActiveRecord::StaleObjectError do
2406 2422 stale.save
2407 2423 end
2408 2424 assert ActionMailer::Base.deliveries.empty?
2409 2425 end
2410 2426 end
2411 2427
2412 2428 def test_journalized_description
2413 2429 IssueCustomField.delete_all
2414 2430
2415 2431 i = Issue.first
2416 2432 old_description = i.description
2417 2433 new_description = "This is the new description"
2418 2434
2419 2435 i.init_journal(User.find(2))
2420 2436 i.description = new_description
2421 2437 assert_difference 'Journal.count', 1 do
2422 2438 assert_difference 'JournalDetail.count', 1 do
2423 2439 i.save!
2424 2440 end
2425 2441 end
2426 2442
2427 2443 detail = JournalDetail.order('id DESC').first
2428 2444 assert_equal i, detail.journal.journalized
2429 2445 assert_equal 'attr', detail.property
2430 2446 assert_equal 'description', detail.prop_key
2431 2447 assert_equal old_description, detail.old_value
2432 2448 assert_equal new_description, detail.value
2433 2449 end
2434 2450
2435 2451 def test_blank_descriptions_should_not_be_journalized
2436 2452 IssueCustomField.delete_all
2437 2453 Issue.where(:id => 1).update_all("description = NULL")
2438 2454
2439 2455 i = Issue.find(1)
2440 2456 i.init_journal(User.find(2))
2441 2457 i.subject = "blank description"
2442 2458 i.description = "\r\n"
2443 2459
2444 2460 assert_difference 'Journal.count', 1 do
2445 2461 assert_difference 'JournalDetail.count', 1 do
2446 2462 i.save!
2447 2463 end
2448 2464 end
2449 2465 end
2450 2466
2451 2467 def test_journalized_multi_custom_field
2452 2468 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
2453 2469 :is_filter => true, :is_for_all => true,
2454 2470 :tracker_ids => [1],
2455 2471 :possible_values => ['value1', 'value2', 'value3'],
2456 2472 :multiple => true)
2457 2473
2458 2474 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
2459 2475 :subject => 'Test', :author_id => 1)
2460 2476
2461 2477 assert_difference 'Journal.count' do
2462 2478 assert_difference 'JournalDetail.count' do
2463 2479 issue.init_journal(User.first)
2464 2480 issue.custom_field_values = {field.id => ['value1']}
2465 2481 issue.save!
2466 2482 end
2467 2483 assert_difference 'JournalDetail.count' do
2468 2484 issue.init_journal(User.first)
2469 2485 issue.custom_field_values = {field.id => ['value1', 'value2']}
2470 2486 issue.save!
2471 2487 end
2472 2488 assert_difference 'JournalDetail.count', 2 do
2473 2489 issue.init_journal(User.first)
2474 2490 issue.custom_field_values = {field.id => ['value3', 'value2']}
2475 2491 issue.save!
2476 2492 end
2477 2493 assert_difference 'JournalDetail.count', 2 do
2478 2494 issue.init_journal(User.first)
2479 2495 issue.custom_field_values = {field.id => nil}
2480 2496 issue.save!
2481 2497 end
2482 2498 end
2483 2499 end
2484 2500
2485 2501 def test_description_eol_should_be_normalized
2486 2502 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
2487 2503 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
2488 2504 end
2489 2505
2490 2506 def test_saving_twice_should_not_duplicate_journal_details
2491 2507 i = Issue.first
2492 2508 i.init_journal(User.find(2), 'Some notes')
2493 2509 # initial changes
2494 2510 i.subject = 'New subject'
2495 2511 i.done_ratio = i.done_ratio + 10
2496 2512 assert_difference 'Journal.count' do
2497 2513 assert i.save
2498 2514 end
2499 2515 # 1 more change
2500 2516 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
2501 2517 assert_no_difference 'Journal.count' do
2502 2518 assert_difference 'JournalDetail.count', 1 do
2503 2519 i.save
2504 2520 end
2505 2521 end
2506 2522 # no more change
2507 2523 assert_no_difference 'Journal.count' do
2508 2524 assert_no_difference 'JournalDetail.count' do
2509 2525 i.save
2510 2526 end
2511 2527 end
2512 2528 end
2513 2529
2514 2530 test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do
2515 2531 @issue = Issue.find(1)
2516 2532 @issue_status = IssueStatus.find(1)
2517 2533 @issue_status.update_attribute(:default_done_ratio, 50)
2518 2534 @issue2 = Issue.find(2)
2519 2535 @issue_status2 = IssueStatus.find(2)
2520 2536 @issue_status2.update_attribute(:default_done_ratio, 0)
2521 2537
2522 2538 with_settings :issue_done_ratio => 'issue_field' do
2523 2539 assert_equal 0, @issue.done_ratio
2524 2540 assert_equal 30, @issue2.done_ratio
2525 2541 end
2526 2542
2527 2543 with_settings :issue_done_ratio => 'issue_status' do
2528 2544 assert_equal 50, @issue.done_ratio
2529 2545 assert_equal 0, @issue2.done_ratio
2530 2546 end
2531 2547 end
2532 2548
2533 2549 test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
2534 2550 @issue = Issue.find(1)
2535 2551 @issue_status = IssueStatus.find(1)
2536 2552 @issue_status.update_attribute(:default_done_ratio, 50)
2537 2553 @issue2 = Issue.find(2)
2538 2554 @issue_status2 = IssueStatus.find(2)
2539 2555 @issue_status2.update_attribute(:default_done_ratio, 0)
2540 2556
2541 2557 with_settings :issue_done_ratio => 'issue_field' do
2542 2558 @issue.update_done_ratio_from_issue_status
2543 2559 @issue2.update_done_ratio_from_issue_status
2544 2560
2545 2561 assert_equal 0, @issue.read_attribute(:done_ratio)
2546 2562 assert_equal 30, @issue2.read_attribute(:done_ratio)
2547 2563 end
2548 2564
2549 2565 with_settings :issue_done_ratio => 'issue_status' do
2550 2566 @issue.update_done_ratio_from_issue_status
2551 2567 @issue2.update_done_ratio_from_issue_status
2552 2568
2553 2569 assert_equal 50, @issue.read_attribute(:done_ratio)
2554 2570 assert_equal 0, @issue2.read_attribute(:done_ratio)
2555 2571 end
2556 2572 end
2557 2573
2558 2574 test "#by_tracker" do
2559 2575 User.current = User.anonymous
2560 2576 groups = Issue.by_tracker(Project.find(1))
2561 2577 assert_equal 3, groups.count
2562 2578 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2563 2579 end
2564 2580
2565 2581 test "#by_version" do
2566 2582 User.current = User.anonymous
2567 2583 groups = Issue.by_version(Project.find(1))
2568 2584 assert_equal 3, groups.count
2569 2585 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2570 2586 end
2571 2587
2572 2588 test "#by_priority" do
2573 2589 User.current = User.anonymous
2574 2590 groups = Issue.by_priority(Project.find(1))
2575 2591 assert_equal 4, groups.count
2576 2592 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2577 2593 end
2578 2594
2579 2595 test "#by_category" do
2580 2596 User.current = User.anonymous
2581 2597 groups = Issue.by_category(Project.find(1))
2582 2598 assert_equal 2, groups.count
2583 2599 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2584 2600 end
2585 2601
2586 2602 test "#by_assigned_to" do
2587 2603 User.current = User.anonymous
2588 2604 groups = Issue.by_assigned_to(Project.find(1))
2589 2605 assert_equal 2, groups.count
2590 2606 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2591 2607 end
2592 2608
2593 2609 test "#by_author" do
2594 2610 User.current = User.anonymous
2595 2611 groups = Issue.by_author(Project.find(1))
2596 2612 assert_equal 4, groups.count
2597 2613 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2598 2614 end
2599 2615
2600 2616 test "#by_subproject" do
2601 2617 User.current = User.anonymous
2602 2618 groups = Issue.by_subproject(Project.find(1))
2603 2619 # Private descendant not visible
2604 2620 assert_equal 1, groups.count
2605 2621 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2606 2622 end
2607 2623
2608 2624 def test_recently_updated_scope
2609 2625 #should return the last updated issue
2610 2626 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
2611 2627 end
2612 2628
2613 2629 def test_on_active_projects_scope
2614 2630 assert Project.find(2).archive
2615 2631
2616 2632 before = Issue.on_active_project.length
2617 2633 # test inclusion to results
2618 2634 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
2619 2635 assert_equal before + 1, Issue.on_active_project.length
2620 2636
2621 2637 # Move to an archived project
2622 2638 issue.project = Project.find(2)
2623 2639 assert issue.save
2624 2640 assert_equal before, Issue.on_active_project.length
2625 2641 end
2626 2642
2627 2643 test "Issue#recipients should include project recipients" do
2628 2644 issue = Issue.generate!
2629 2645 assert issue.project.recipients.present?
2630 2646 issue.project.recipients.each do |project_recipient|
2631 2647 assert issue.recipients.include?(project_recipient)
2632 2648 end
2633 2649 end
2634 2650
2635 2651 test "Issue#recipients should include the author if the author is active" do
2636 2652 issue = Issue.generate!(:author => User.generate!)
2637 2653 assert issue.author, "No author set for Issue"
2638 2654 assert issue.recipients.include?(issue.author.mail)
2639 2655 end
2640 2656
2641 2657 test "Issue#recipients should include the assigned to user if the assigned to user is active" do
2642 2658 issue = Issue.generate!(:assigned_to => User.generate!)
2643 2659 assert issue.assigned_to, "No assigned_to set for Issue"
2644 2660 assert issue.recipients.include?(issue.assigned_to.mail)
2645 2661 end
2646 2662
2647 2663 test "Issue#recipients should not include users who opt out of all email" do
2648 2664 issue = Issue.generate!(:author => User.generate!)
2649 2665 issue.author.update_attribute(:mail_notification, :none)
2650 2666 assert !issue.recipients.include?(issue.author.mail)
2651 2667 end
2652 2668
2653 2669 test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do
2654 2670 issue = Issue.generate!(:author => User.generate!)
2655 2671 issue.author.update_attribute(:mail_notification, :only_assigned)
2656 2672 assert !issue.recipients.include?(issue.author.mail)
2657 2673 end
2658 2674
2659 2675 test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do
2660 2676 issue = Issue.generate!(:assigned_to => User.generate!)
2661 2677 issue.assigned_to.update_attribute(:mail_notification, :only_owner)
2662 2678 assert !issue.recipients.include?(issue.assigned_to.mail)
2663 2679 end
2664 2680
2665 2681 def test_last_journal_id_with_journals_should_return_the_journal_id
2666 2682 assert_equal 2, Issue.find(1).last_journal_id
2667 2683 end
2668 2684
2669 2685 def test_last_journal_id_without_journals_should_return_nil
2670 2686 assert_nil Issue.find(3).last_journal_id
2671 2687 end
2672 2688
2673 2689 def test_journals_after_should_return_journals_with_greater_id
2674 2690 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
2675 2691 assert_equal [], Issue.find(1).journals_after('2')
2676 2692 end
2677 2693
2678 2694 def test_journals_after_with_blank_arg_should_return_all_journals
2679 2695 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
2680 2696 end
2681 2697
2682 2698 def test_css_classes_should_include_tracker
2683 2699 issue = Issue.new(:tracker => Tracker.find(2))
2684 2700 classes = issue.css_classes.split(' ')
2685 2701 assert_include 'tracker-2', classes
2686 2702 end
2687 2703
2688 2704 def test_css_classes_should_include_priority
2689 2705 issue = Issue.new(:priority => IssuePriority.find(8))
2690 2706 classes = issue.css_classes.split(' ')
2691 2707 assert_include 'priority-8', classes
2692 2708 assert_include 'priority-highest', classes
2693 2709 end
2694 2710
2695 2711 def test_css_classes_should_include_user_and_group_assignment
2696 2712 project = Project.first
2697 2713 user = User.generate!
2698 2714 group = Group.generate!
2699 2715 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
2700 2716 group.users << user
2701 2717 assert user.member_of?(project)
2702 2718 issue1 = Issue.generate(:assigned_to_id => group.id)
2703 2719 assert_include 'assigned-to-my-group', issue1.css_classes(user)
2704 2720 assert_not_include 'assigned-to-me', issue1.css_classes(user)
2705 2721 issue2 = Issue.generate(:assigned_to_id => user.id)
2706 2722 assert_not_include 'assigned-to-my-group', issue2.css_classes(user)
2707 2723 assert_include 'assigned-to-me', issue2.css_classes(user)
2708 2724 end
2709 2725
2710 2726 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
2711 2727 set_tmp_attachments_directory
2712 2728 issue = Issue.generate!
2713 2729 issue.save_attachments({
2714 2730 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
2715 2731 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
2716 2732 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
2717 2733 })
2718 2734 issue.attach_saved_attachments
2719 2735
2720 2736 assert_equal 3, issue.reload.attachments.count
2721 2737 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
2722 2738 end
2723 2739
2724 2740 def test_save_attachments_with_array_should_warn_about_missing_tokens
2725 2741 set_tmp_attachments_directory
2726 2742 issue = Issue.generate!
2727 2743 issue.save_attachments([
2728 2744 {'token' => 'missing'}
2729 2745 ])
2730 2746 assert !issue.save
2731 2747 assert issue.errors[:base].present?
2732 2748 assert_equal 0, issue.reload.attachments.count
2733 2749 end
2734 2750
2735 2751 def test_closed_on_should_be_nil_when_creating_an_open_issue
2736 2752 issue = Issue.generate!(:status_id => 1).reload
2737 2753 assert !issue.closed?
2738 2754 assert_nil issue.closed_on
2739 2755 end
2740 2756
2741 2757 def test_closed_on_should_be_set_when_creating_a_closed_issue
2742 2758 issue = Issue.generate!(:status_id => 5).reload
2743 2759 assert issue.closed?
2744 2760 assert_not_nil issue.closed_on
2745 2761 assert_equal issue.updated_on, issue.closed_on
2746 2762 assert_equal issue.created_on, issue.closed_on
2747 2763 end
2748 2764
2749 2765 def test_closed_on_should_be_nil_when_updating_an_open_issue
2750 2766 issue = Issue.find(1)
2751 2767 issue.subject = 'Not closed yet'
2752 2768 issue.save!
2753 2769 issue.reload
2754 2770 assert_nil issue.closed_on
2755 2771 end
2756 2772
2757 2773 def test_closed_on_should_be_set_when_closing_an_open_issue
2758 2774 issue = Issue.find(1)
2759 2775 issue.subject = 'Now closed'
2760 2776 issue.status_id = 5
2761 2777 issue.save!
2762 2778 issue.reload
2763 2779 assert_not_nil issue.closed_on
2764 2780 assert_equal issue.updated_on, issue.closed_on
2765 2781 end
2766 2782
2767 2783 def test_closed_on_should_not_be_updated_when_updating_a_closed_issue
2768 2784 issue = Issue.open(false).first
2769 2785 was_closed_on = issue.closed_on
2770 2786 assert_not_nil was_closed_on
2771 2787 issue.subject = 'Updating a closed issue'
2772 2788 issue.save!
2773 2789 issue.reload
2774 2790 assert_equal was_closed_on, issue.closed_on
2775 2791 end
2776 2792
2777 2793 def test_closed_on_should_be_preserved_when_reopening_a_closed_issue
2778 2794 issue = Issue.open(false).first
2779 2795 was_closed_on = issue.closed_on
2780 2796 assert_not_nil was_closed_on
2781 2797 issue.subject = 'Reopening a closed issue'
2782 2798 issue.status_id = 1
2783 2799 issue.save!
2784 2800 issue.reload
2785 2801 assert !issue.closed?
2786 2802 assert_equal was_closed_on, issue.closed_on
2787 2803 end
2788 2804
2789 2805 def test_status_was_should_return_nil_for_new_issue
2790 2806 issue = Issue.new
2791 2807 assert_nil issue.status_was
2792 2808 end
2793 2809
2794 2810 def test_status_was_should_return_status_before_change
2795 2811 issue = Issue.find(1)
2796 2812 issue.status = IssueStatus.find(2)
2797 2813 assert_equal IssueStatus.find(1), issue.status_was
2798 2814 end
2799 2815
2800 2816 def test_status_was_should_return_status_before_change_with_status_id
2801 2817 issue = Issue.find(1)
2802 2818 assert_equal IssueStatus.find(1), issue.status
2803 2819 issue.status_id = 2
2804 2820 assert_equal IssueStatus.find(1), issue.status_was
2805 2821 end
2806 2822
2807 2823 def test_status_was_should_be_reset_on_save
2808 2824 issue = Issue.find(1)
2809 2825 issue.status = IssueStatus.find(2)
2810 2826 assert_equal IssueStatus.find(1), issue.status_was
2811 2827 assert issue.save!
2812 2828 assert_equal IssueStatus.find(2), issue.status_was
2813 2829 end
2814 2830
2815 2831 def test_closing_should_return_true_when_closing_an_issue
2816 2832 issue = Issue.find(1)
2817 2833 issue.status = IssueStatus.find(2)
2818 2834 assert_equal false, issue.closing?
2819 2835 issue.status = IssueStatus.find(5)
2820 2836 assert_equal true, issue.closing?
2821 2837 end
2822 2838
2823 2839 def test_closing_should_return_true_when_closing_an_issue_with_status_id
2824 2840 issue = Issue.find(1)
2825 2841 issue.status_id = 2
2826 2842 assert_equal false, issue.closing?
2827 2843 issue.status_id = 5
2828 2844 assert_equal true, issue.closing?
2829 2845 end
2830 2846
2831 2847 def test_closing_should_return_true_for_new_closed_issue
2832 2848 issue = Issue.new
2833 2849 assert_equal false, issue.closing?
2834 2850 issue.status = IssueStatus.find(5)
2835 2851 assert_equal true, issue.closing?
2836 2852 end
2837 2853
2838 2854 def test_closing_should_return_true_for_new_closed_issue_with_status_id
2839 2855 issue = Issue.new
2840 2856 assert_equal false, issue.closing?
2841 2857 issue.status_id = 5
2842 2858 assert_equal true, issue.closing?
2843 2859 end
2844 2860
2845 2861 def test_closing_should_be_reset_after_save
2846 2862 issue = Issue.find(1)
2847 2863 issue.status_id = 5
2848 2864 assert_equal true, issue.closing?
2849 2865 issue.save!
2850 2866 assert_equal false, issue.closing?
2851 2867 end
2852 2868
2853 2869 def test_reopening_should_return_true_when_reopening_an_issue
2854 2870 issue = Issue.find(8)
2855 2871 issue.status = IssueStatus.find(6)
2856 2872 assert_equal false, issue.reopening?
2857 2873 issue.status = IssueStatus.find(2)
2858 2874 assert_equal true, issue.reopening?
2859 2875 end
2860 2876
2861 2877 def test_reopening_should_return_true_when_reopening_an_issue_with_status_id
2862 2878 issue = Issue.find(8)
2863 2879 issue.status_id = 6
2864 2880 assert_equal false, issue.reopening?
2865 2881 issue.status_id = 2
2866 2882 assert_equal true, issue.reopening?
2867 2883 end
2868 2884
2869 2885 def test_reopening_should_return_false_for_new_open_issue
2870 2886 issue = Issue.new
2871 2887 issue.status = IssueStatus.find(1)
2872 2888 assert_equal false, issue.reopening?
2873 2889 end
2874 2890
2875 2891 def test_reopening_should_be_reset_after_save
2876 2892 issue = Issue.find(8)
2877 2893 issue.status_id = 2
2878 2894 assert_equal true, issue.reopening?
2879 2895 issue.save!
2880 2896 assert_equal false, issue.reopening?
2881 2897 end
2882 2898
2883 2899 def test_default_status_without_tracker_should_be_nil
2884 2900 issue = Issue.new
2885 2901 assert_nil issue.tracker
2886 2902 assert_nil issue.default_status
2887 2903 end
2888 2904
2889 2905 def test_default_status_should_be_tracker_default_status
2890 2906 issue = Issue.new(:tracker_id => 1)
2891 2907 assert_not_nil issue.status
2892 2908 assert_equal issue.tracker.default_status, issue.default_status
2893 2909 end
2894 2910
2895 2911 def test_initializing_with_tracker_should_set_default_status
2896 2912 issue = Issue.new(:tracker => Tracker.find(1))
2897 2913 assert_not_nil issue.status
2898 2914 assert_equal issue.default_status, issue.status
2899 2915 end
2900 2916
2901 2917 def test_initializing_with_tracker_id_should_set_default_status
2902 2918 issue = Issue.new(:tracker_id => 1)
2903 2919 assert_not_nil issue.status
2904 2920 assert_equal issue.default_status, issue.status
2905 2921 end
2906 2922
2907 2923 def test_setting_tracker_should_set_default_status
2908 2924 issue = Issue.new
2909 2925 issue.tracker = Tracker.find(1)
2910 2926 assert_not_nil issue.status
2911 2927 assert_equal issue.default_status, issue.status
2912 2928 end
2913 2929
2914 2930 def test_changing_tracker_should_set_default_status_if_status_was_default
2915 2931 WorkflowTransition.delete_all
2916 2932 WorkflowTransition.create! :role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1
2917 2933 Tracker.find(2).update! :default_status_id => 2
2918 2934
2919 2935 issue = Issue.new(:tracker_id => 1, :status_id => 1)
2920 2936 assert_equal IssueStatus.find(1), issue.status
2921 2937 issue.tracker = Tracker.find(2)
2922 2938 assert_equal IssueStatus.find(2), issue.status
2923 2939 end
2924 2940
2925 2941 def test_changing_tracker_should_set_default_status_if_status_is_not_used_by_tracker
2926 2942 WorkflowTransition.delete_all
2927 2943 Tracker.find(2).update! :default_status_id => 2
2928 2944
2929 2945 issue = Issue.new(:tracker_id => 1, :status_id => 3)
2930 2946 assert_equal IssueStatus.find(3), issue.status
2931 2947 issue.tracker = Tracker.find(2)
2932 2948 assert_equal IssueStatus.find(2), issue.status
2933 2949 end
2934 2950
2935 2951 def test_changing_tracker_should_keep_status_if_status_was_not_default_and_is_used_by_tracker
2936 2952 WorkflowTransition.delete_all
2937 2953 WorkflowTransition.create! :role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3
2938 2954 Tracker.find(2).update! :default_status_id => 2
2939 2955
2940 2956 issue = Issue.new(:tracker_id => 1, :status_id => 3)
2941 2957 assert_equal IssueStatus.find(3), issue.status
2942 2958 issue.tracker = Tracker.find(2)
2943 2959 assert_equal IssueStatus.find(3), issue.status
2944 2960 end
2945 2961
2946 2962 def test_assigned_to_was_with_a_group
2947 2963 group = Group.find(10)
2948 2964
2949 2965 issue = Issue.generate!(:assigned_to => group)
2950 2966 issue.reload.assigned_to = nil
2951 2967 assert_equal group, issue.assigned_to_was
2952 2968 end
2953 2969
2954 2970 def test_issue_overdue_should_respect_user_timezone
2955 2971 user_in_europe = users(:users_001)
2956 2972 user_in_europe.pref.update_attribute :time_zone, 'UTC'
2957 2973
2958 2974 user_in_asia = users(:users_002)
2959 2975 user_in_asia.pref.update_attribute :time_zone, 'Hongkong'
2960 2976
2961 2977 issue = Issue.generate! :due_date => Date.parse('2016-03-20')
2962 2978
2963 2979 # server time is UTC
2964 2980 time = Time.parse '2016-03-20 20:00 UTC'
2965 2981 Time.stubs(:now).returns(time)
2966 2982 Date.stubs(:today).returns(time.to_date)
2967 2983
2968 2984 # for a user in the same time zone as the server the issue is not overdue
2969 2985 # yet
2970 2986 User.current = user_in_europe
2971 2987 assert !issue.overdue?
2972 2988
2973 2989 # at the same time, a user in East Asia looks at the issue - it's already
2974 2990 # March 21st and the issue should be marked overdue
2975 2991 User.current = user_in_asia
2976 2992 assert issue.overdue?
2977 2993
2978 2994 end
2979 2995 end
General Comments 0
You need to be logged in to leave comments. Login now