##// END OF EJS Templates
Edit/delete links displayed on issue even if project is closed (#23969)....
Jean-Philippe Lang -
r15497:d4f57cc2fa82
parent child
Show More

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

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