##// END OF EJS Templates
Fixed that deleting a project with subtasks may fail (#11185)....
Jean-Philippe Lang -
r9675:f62507dae53a
parent child
Show More
@@ -1,1084 +1,1106
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 belongs_to :project
22 22 belongs_to :tracker
23 23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29 29
30 30 has_many :journals, :as => :journalized, :dependent => :destroy
31 31 has_many :time_entries, :dependent => :delete_all
32 32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33 33
34 34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 36
37 37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 39 acts_as_customizable
40 40 acts_as_watchable
41 41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 42 :include => [:project, :journals],
43 43 # sort by id so that limited eager loading doesn't break with postgresql
44 44 :order_column => "#{table_name}.id"
45 45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48 48
49 49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 50 :author_key => :author_id
51 51
52 52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53 53
54 54 attr_reader :current_journal
55 55
56 56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57 57
58 58 validates_length_of :subject, :maximum => 255
59 59 validates_inclusion_of :done_ratio, :in => 0..100
60 60 validates_numericality_of :estimated_hours, :allow_nil => true
61 61 validate :validate_issue
62 62
63 63 scope :visible,
64 64 lambda {|*args| { :include => :project,
65 65 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
66 66
67 67 scope :open, lambda {|*args|
68 68 is_closed = args.size > 0 ? !args.first : false
69 69 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
70 70 }
71 71
72 72 scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
73 73 scope :with_limit, lambda { |limit| { :limit => limit} }
74 74 scope :on_active_project, :include => [:status, :project, :tracker],
75 75 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
76 76
77 77 before_create :default_assign
78 78 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
79 79 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
80 80 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
81 81 after_destroy :update_parent_attributes
82 82
83 83 # Returns a SQL conditions string used to find all issues visible by the specified user
84 84 def self.visible_condition(user, options={})
85 85 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
86 86 case role.issues_visibility
87 87 when 'all'
88 88 nil
89 89 when 'default'
90 90 user_ids = [user.id] + user.groups.map(&:id)
91 91 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
92 92 when 'own'
93 93 user_ids = [user.id] + user.groups.map(&:id)
94 94 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
95 95 else
96 96 '1=0'
97 97 end
98 98 end
99 99 end
100 100
101 101 # Returns true if usr or current user is allowed to view the issue
102 102 def visible?(usr=nil)
103 103 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
104 104 case role.issues_visibility
105 105 when 'all'
106 106 true
107 107 when 'default'
108 108 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
109 109 when 'own'
110 110 self.author == user || user.is_or_belongs_to?(assigned_to)
111 111 else
112 112 false
113 113 end
114 114 end
115 115 end
116 116
117 117 def initialize(attributes=nil, *args)
118 118 super
119 119 if new_record?
120 120 # set default values for new records only
121 121 self.status ||= IssueStatus.default
122 122 self.priority ||= IssuePriority.default
123 123 self.watcher_user_ids = []
124 124 end
125 125 end
126 126
127 # AR#Persistence#destroy would raise and RecordNotFound exception
128 # if the issue was already deleted or updated (non matching lock_version).
129 # This is a problem when bulk deleting issues or deleting a project
130 # (because an issue may already be deleted if its parent was deleted
131 # first).
132 # The issue is reloaded by the nested_set before being deleted so
133 # the lock_version condition should not be an issue but we handle it.
134 def destroy
135 super
136 rescue ActiveRecord::RecordNotFound
137 # Stale or already deleted
138 begin
139 reload
140 rescue ActiveRecord::RecordNotFound
141 # The issue was actually already deleted
142 @destroyed = true
143 return freeze
144 end
145 # The issue was stale, retry to destroy
146 super
147 end
148
127 149 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
128 150 def available_custom_fields
129 151 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
130 152 end
131 153
132 154 # Copies attributes from another issue, arg can be an id or an Issue
133 155 def copy_from(arg, options={})
134 156 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
135 157 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
136 158 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
137 159 self.status = issue.status
138 160 self.author = User.current
139 161 unless options[:attachments] == false
140 162 self.attachments = issue.attachments.map do |attachement|
141 163 attachement.copy(:container => self)
142 164 end
143 165 end
144 166 @copied_from = issue
145 167 self
146 168 end
147 169
148 170 # Returns an unsaved copy of the issue
149 171 def copy(attributes=nil, copy_options={})
150 172 copy = self.class.new.copy_from(self, copy_options)
151 173 copy.attributes = attributes if attributes
152 174 copy
153 175 end
154 176
155 177 # Returns true if the issue is a copy
156 178 def copy?
157 179 @copied_from.present?
158 180 end
159 181
160 182 # Moves/copies an issue to a new project and tracker
161 183 # Returns the moved/copied issue on success, false on failure
162 184 def move_to_project(new_project, new_tracker=nil, options={})
163 185 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
164 186
165 187 if options[:copy]
166 188 issue = self.copy
167 189 else
168 190 issue = self
169 191 end
170 192
171 193 issue.init_journal(User.current, options[:notes])
172 194
173 195 # Preserve previous behaviour
174 196 # #move_to_project doesn't change tracker automatically
175 197 issue.send :project=, new_project, true
176 198 if new_tracker
177 199 issue.tracker = new_tracker
178 200 end
179 201 # Allow bulk setting of attributes on the issue
180 202 if options[:attributes]
181 203 issue.attributes = options[:attributes]
182 204 end
183 205
184 206 issue.save ? issue : false
185 207 end
186 208
187 209 def status_id=(sid)
188 210 self.status = nil
189 211 write_attribute(:status_id, sid)
190 212 end
191 213
192 214 def priority_id=(pid)
193 215 self.priority = nil
194 216 write_attribute(:priority_id, pid)
195 217 end
196 218
197 219 def category_id=(cid)
198 220 self.category = nil
199 221 write_attribute(:category_id, cid)
200 222 end
201 223
202 224 def fixed_version_id=(vid)
203 225 self.fixed_version = nil
204 226 write_attribute(:fixed_version_id, vid)
205 227 end
206 228
207 229 def tracker_id=(tid)
208 230 self.tracker = nil
209 231 result = write_attribute(:tracker_id, tid)
210 232 @custom_field_values = nil
211 233 result
212 234 end
213 235
214 236 def project_id=(project_id)
215 237 if project_id.to_s != self.project_id.to_s
216 238 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
217 239 end
218 240 end
219 241
220 242 def project=(project, keep_tracker=false)
221 243 project_was = self.project
222 244 write_attribute(:project_id, project ? project.id : nil)
223 245 association_instance_set('project', project)
224 246 if project_was && project && project_was != project
225 247 unless keep_tracker || project.trackers.include?(tracker)
226 248 self.tracker = project.trackers.first
227 249 end
228 250 # Reassign to the category with same name if any
229 251 if category
230 252 self.category = project.issue_categories.find_by_name(category.name)
231 253 end
232 254 # Keep the fixed_version if it's still valid in the new_project
233 255 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
234 256 self.fixed_version = nil
235 257 end
236 258 if parent && parent.project_id != project_id
237 259 self.parent_issue_id = nil
238 260 end
239 261 @custom_field_values = nil
240 262 end
241 263 end
242 264
243 265 def description=(arg)
244 266 if arg.is_a?(String)
245 267 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
246 268 end
247 269 write_attribute(:description, arg)
248 270 end
249 271
250 272 # Overrides assign_attributes so that project and tracker get assigned first
251 273 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
252 274 return if new_attributes.nil?
253 275 attrs = new_attributes.dup
254 276 attrs.stringify_keys!
255 277
256 278 %w(project project_id tracker tracker_id).each do |attr|
257 279 if attrs.has_key?(attr)
258 280 send "#{attr}=", attrs.delete(attr)
259 281 end
260 282 end
261 283 send :assign_attributes_without_project_and_tracker_first, attrs, *args
262 284 end
263 285 # Do not redefine alias chain on reload (see #4838)
264 286 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
265 287
266 288 def estimated_hours=(h)
267 289 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
268 290 end
269 291
270 292 safe_attributes 'project_id',
271 293 :if => lambda {|issue, user|
272 294 if issue.new_record?
273 295 issue.copy?
274 296 elsif user.allowed_to?(:move_issues, issue.project)
275 297 projects = Issue.allowed_target_projects_on_move(user)
276 298 projects.include?(issue.project) && projects.size > 1
277 299 end
278 300 }
279 301
280 302 safe_attributes 'tracker_id',
281 303 'status_id',
282 304 'category_id',
283 305 'assigned_to_id',
284 306 'priority_id',
285 307 'fixed_version_id',
286 308 'subject',
287 309 'description',
288 310 'start_date',
289 311 'due_date',
290 312 'done_ratio',
291 313 'estimated_hours',
292 314 'custom_field_values',
293 315 'custom_fields',
294 316 'lock_version',
295 317 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
296 318
297 319 safe_attributes 'status_id',
298 320 'assigned_to_id',
299 321 'fixed_version_id',
300 322 'done_ratio',
301 323 'lock_version',
302 324 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
303 325
304 326 safe_attributes 'watcher_user_ids',
305 327 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
306 328
307 329 safe_attributes 'is_private',
308 330 :if => lambda {|issue, user|
309 331 user.allowed_to?(:set_issues_private, issue.project) ||
310 332 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
311 333 }
312 334
313 335 safe_attributes 'parent_issue_id',
314 336 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
315 337 user.allowed_to?(:manage_subtasks, issue.project)}
316 338
317 339 # Safely sets attributes
318 340 # Should be called from controllers instead of #attributes=
319 341 # attr_accessible is too rough because we still want things like
320 342 # Issue.new(:project => foo) to work
321 343 def safe_attributes=(attrs, user=User.current)
322 344 return unless attrs.is_a?(Hash)
323 345
324 346 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
325 347 attrs = delete_unsafe_attributes(attrs, user)
326 348 return if attrs.empty?
327 349
328 350 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
329 351 if p = attrs.delete('project_id')
330 352 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
331 353 self.project_id = p
332 354 end
333 355 end
334 356
335 357 if t = attrs.delete('tracker_id')
336 358 self.tracker_id = t
337 359 end
338 360
339 361 if attrs['status_id']
340 362 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
341 363 attrs.delete('status_id')
342 364 end
343 365 end
344 366
345 367 unless leaf?
346 368 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
347 369 end
348 370
349 371 if attrs['parent_issue_id'].present?
350 372 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
351 373 end
352 374
353 375 # mass-assignment security bypass
354 376 assign_attributes attrs, :without_protection => true
355 377 end
356 378
357 379 def done_ratio
358 380 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
359 381 status.default_done_ratio
360 382 else
361 383 read_attribute(:done_ratio)
362 384 end
363 385 end
364 386
365 387 def self.use_status_for_done_ratio?
366 388 Setting.issue_done_ratio == 'issue_status'
367 389 end
368 390
369 391 def self.use_field_for_done_ratio?
370 392 Setting.issue_done_ratio == 'issue_field'
371 393 end
372 394
373 395 def validate_issue
374 396 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
375 397 errors.add :due_date, :not_a_date
376 398 end
377 399
378 400 if self.due_date and self.start_date and self.due_date < self.start_date
379 401 errors.add :due_date, :greater_than_start_date
380 402 end
381 403
382 404 if start_date && soonest_start && start_date < soonest_start
383 405 errors.add :start_date, :invalid
384 406 end
385 407
386 408 if fixed_version
387 409 if !assignable_versions.include?(fixed_version)
388 410 errors.add :fixed_version_id, :inclusion
389 411 elsif reopened? && fixed_version.closed?
390 412 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
391 413 end
392 414 end
393 415
394 416 # Checks that the issue can not be added/moved to a disabled tracker
395 417 if project && (tracker_id_changed? || project_id_changed?)
396 418 unless project.trackers.include?(tracker)
397 419 errors.add :tracker_id, :inclusion
398 420 end
399 421 end
400 422
401 423 # Checks parent issue assignment
402 424 if @parent_issue
403 425 if @parent_issue.project_id != project_id
404 426 errors.add :parent_issue_id, :not_same_project
405 427 elsif !new_record?
406 428 # moving an existing issue
407 429 if @parent_issue.root_id != root_id
408 430 # we can always move to another tree
409 431 elsif move_possible?(@parent_issue)
410 432 # move accepted inside tree
411 433 else
412 434 errors.add :parent_issue_id, :not_a_valid_parent
413 435 end
414 436 end
415 437 end
416 438 end
417 439
418 440 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
419 441 # even if the user turns off the setting later
420 442 def update_done_ratio_from_issue_status
421 443 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
422 444 self.done_ratio = status.default_done_ratio
423 445 end
424 446 end
425 447
426 448 def init_journal(user, notes = "")
427 449 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
428 450 if new_record?
429 451 @current_journal.notify = false
430 452 else
431 453 @attributes_before_change = attributes.dup
432 454 @custom_values_before_change = {}
433 455 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
434 456 end
435 457 @current_journal
436 458 end
437 459
438 460 # Returns the id of the last journal or nil
439 461 def last_journal_id
440 462 if new_record?
441 463 nil
442 464 else
443 465 journals.first(:order => "#{Journal.table_name}.id DESC").try(:id)
444 466 end
445 467 end
446 468
447 469 # Return true if the issue is closed, otherwise false
448 470 def closed?
449 471 self.status.is_closed?
450 472 end
451 473
452 474 # Return true if the issue is being reopened
453 475 def reopened?
454 476 if !new_record? && status_id_changed?
455 477 status_was = IssueStatus.find_by_id(status_id_was)
456 478 status_new = IssueStatus.find_by_id(status_id)
457 479 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
458 480 return true
459 481 end
460 482 end
461 483 false
462 484 end
463 485
464 486 # Return true if the issue is being closed
465 487 def closing?
466 488 if !new_record? && status_id_changed?
467 489 status_was = IssueStatus.find_by_id(status_id_was)
468 490 status_new = IssueStatus.find_by_id(status_id)
469 491 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
470 492 return true
471 493 end
472 494 end
473 495 false
474 496 end
475 497
476 498 # Returns true if the issue is overdue
477 499 def overdue?
478 500 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
479 501 end
480 502
481 503 # Is the amount of work done less than it should for the due date
482 504 def behind_schedule?
483 505 return false if start_date.nil? || due_date.nil?
484 506 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
485 507 return done_date <= Date.today
486 508 end
487 509
488 510 # Does this issue have children?
489 511 def children?
490 512 !leaf?
491 513 end
492 514
493 515 # Users the issue can be assigned to
494 516 def assignable_users
495 517 users = project.assignable_users
496 518 users << author if author
497 519 users << assigned_to if assigned_to
498 520 users.uniq.sort
499 521 end
500 522
501 523 # Versions that the issue can be assigned to
502 524 def assignable_versions
503 525 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
504 526 end
505 527
506 528 # Returns true if this issue is blocked by another issue that is still open
507 529 def blocked?
508 530 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
509 531 end
510 532
511 533 # Returns an array of statuses that user is able to apply
512 534 def new_statuses_allowed_to(user=User.current, include_default=false)
513 535 if new_record? && @copied_from
514 536 [IssueStatus.default, @copied_from.status].compact.uniq.sort
515 537 else
516 538 initial_status = nil
517 539 if new_record?
518 540 initial_status = IssueStatus.default
519 541 elsif status_id_was
520 542 initial_status = IssueStatus.find_by_id(status_id_was)
521 543 end
522 544 initial_status ||= status
523 545
524 546 statuses = initial_status.find_new_statuses_allowed_to(
525 547 user.admin ? Role.all : user.roles_for_project(project),
526 548 tracker,
527 549 author == user,
528 550 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
529 551 )
530 552 statuses << initial_status unless statuses.empty?
531 553 statuses << IssueStatus.default if include_default
532 554 statuses = statuses.compact.uniq.sort
533 555 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
534 556 end
535 557 end
536 558
537 559 def assigned_to_was
538 560 if assigned_to_id_changed? && assigned_to_id_was.present?
539 561 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
540 562 end
541 563 end
542 564
543 565 # Returns the mail adresses of users that should be notified
544 566 def recipients
545 567 notified = []
546 568 # Author and assignee are always notified unless they have been
547 569 # locked or don't want to be notified
548 570 notified << author if author
549 571 if assigned_to
550 572 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
551 573 end
552 574 if assigned_to_was
553 575 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
554 576 end
555 577 notified = notified.select {|u| u.active? && u.notify_about?(self)}
556 578
557 579 notified += project.notified_users
558 580 notified.uniq!
559 581 # Remove users that can not view the issue
560 582 notified.reject! {|user| !visible?(user)}
561 583 notified.collect(&:mail)
562 584 end
563 585
564 586 # Returns the number of hours spent on this issue
565 587 def spent_hours
566 588 @spent_hours ||= time_entries.sum(:hours) || 0
567 589 end
568 590
569 591 # Returns the total number of hours spent on this issue and its descendants
570 592 #
571 593 # Example:
572 594 # spent_hours => 0.0
573 595 # spent_hours => 50.2
574 596 def total_spent_hours
575 597 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
576 598 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
577 599 end
578 600
579 601 def relations
580 602 @relations ||= (relations_from + relations_to).sort
581 603 end
582 604
583 605 # Preloads relations for a collection of issues
584 606 def self.load_relations(issues)
585 607 if issues.any?
586 608 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
587 609 issues.each do |issue|
588 610 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
589 611 end
590 612 end
591 613 end
592 614
593 615 # Preloads visible spent time for a collection of issues
594 616 def self.load_visible_spent_hours(issues, user=User.current)
595 617 if issues.any?
596 618 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
597 619 issues.each do |issue|
598 620 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
599 621 end
600 622 end
601 623 end
602 624
603 625 # Finds an issue relation given its id.
604 626 def find_relation(relation_id)
605 627 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
606 628 end
607 629
608 630 def all_dependent_issues(except=[])
609 631 except << self
610 632 dependencies = []
611 633 relations_from.each do |relation|
612 634 if relation.issue_to && !except.include?(relation.issue_to)
613 635 dependencies << relation.issue_to
614 636 dependencies += relation.issue_to.all_dependent_issues(except)
615 637 end
616 638 end
617 639 dependencies
618 640 end
619 641
620 642 # Returns an array of issues that duplicate this one
621 643 def duplicates
622 644 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
623 645 end
624 646
625 647 # Returns the due date or the target due date if any
626 648 # Used on gantt chart
627 649 def due_before
628 650 due_date || (fixed_version ? fixed_version.effective_date : nil)
629 651 end
630 652
631 653 # Returns the time scheduled for this issue.
632 654 #
633 655 # Example:
634 656 # Start Date: 2/26/09, End Date: 3/04/09
635 657 # duration => 6
636 658 def duration
637 659 (start_date && due_date) ? due_date - start_date : 0
638 660 end
639 661
640 662 def soonest_start
641 663 @soonest_start ||= (
642 664 relations_to.collect{|relation| relation.successor_soonest_start} +
643 665 ancestors.collect(&:soonest_start)
644 666 ).compact.max
645 667 end
646 668
647 669 def reschedule_after(date)
648 670 return if date.nil?
649 671 if leaf?
650 672 if start_date.nil? || start_date < date
651 673 self.start_date, self.due_date = date, date + duration
652 674 begin
653 675 save
654 676 rescue ActiveRecord::StaleObjectError
655 677 reload
656 678 self.start_date, self.due_date = date, date + duration
657 679 save
658 680 end
659 681 end
660 682 else
661 683 leaves.each do |leaf|
662 684 leaf.reschedule_after(date)
663 685 end
664 686 end
665 687 end
666 688
667 689 def <=>(issue)
668 690 if issue.nil?
669 691 -1
670 692 elsif root_id != issue.root_id
671 693 (root_id || 0) <=> (issue.root_id || 0)
672 694 else
673 695 (lft || 0) <=> (issue.lft || 0)
674 696 end
675 697 end
676 698
677 699 def to_s
678 700 "#{tracker} ##{id}: #{subject}"
679 701 end
680 702
681 703 # Returns a string of css classes that apply to the issue
682 704 def css_classes
683 705 s = "issue status-#{status.position} priority-#{priority.position}"
684 706 s << ' closed' if closed?
685 707 s << ' overdue' if overdue?
686 708 s << ' child' if child?
687 709 s << ' parent' unless leaf?
688 710 s << ' private' if is_private?
689 711 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
690 712 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
691 713 s
692 714 end
693 715
694 716 # Saves an issue and a time_entry from the parameters
695 717 def save_issue_with_child_records(params, existing_time_entry=nil)
696 718 Issue.transaction do
697 719 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
698 720 @time_entry = existing_time_entry || TimeEntry.new
699 721 @time_entry.project = project
700 722 @time_entry.issue = self
701 723 @time_entry.user = User.current
702 724 @time_entry.spent_on = User.current.today
703 725 @time_entry.attributes = params[:time_entry]
704 726 self.time_entries << @time_entry
705 727 end
706 728
707 729 # TODO: Rename hook
708 730 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
709 731 if save
710 732 # TODO: Rename hook
711 733 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
712 734 else
713 735 raise ActiveRecord::Rollback
714 736 end
715 737 end
716 738 end
717 739
718 740 # Unassigns issues from +version+ if it's no longer shared with issue's project
719 741 def self.update_versions_from_sharing_change(version)
720 742 # Update issues assigned to the version
721 743 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
722 744 end
723 745
724 746 # Unassigns issues from versions that are no longer shared
725 747 # after +project+ was moved
726 748 def self.update_versions_from_hierarchy_change(project)
727 749 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
728 750 # Update issues of the moved projects and issues assigned to a version of a moved project
729 751 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
730 752 end
731 753
732 754 def parent_issue_id=(arg)
733 755 parent_issue_id = arg.blank? ? nil : arg.to_i
734 756 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
735 757 @parent_issue.id
736 758 else
737 759 @parent_issue = nil
738 760 nil
739 761 end
740 762 end
741 763
742 764 def parent_issue_id
743 765 if instance_variable_defined? :@parent_issue
744 766 @parent_issue.nil? ? nil : @parent_issue.id
745 767 else
746 768 parent_id
747 769 end
748 770 end
749 771
750 772 # Extracted from the ReportsController.
751 773 def self.by_tracker(project)
752 774 count_and_group_by(:project => project,
753 775 :field => 'tracker_id',
754 776 :joins => Tracker.table_name)
755 777 end
756 778
757 779 def self.by_version(project)
758 780 count_and_group_by(:project => project,
759 781 :field => 'fixed_version_id',
760 782 :joins => Version.table_name)
761 783 end
762 784
763 785 def self.by_priority(project)
764 786 count_and_group_by(:project => project,
765 787 :field => 'priority_id',
766 788 :joins => IssuePriority.table_name)
767 789 end
768 790
769 791 def self.by_category(project)
770 792 count_and_group_by(:project => project,
771 793 :field => 'category_id',
772 794 :joins => IssueCategory.table_name)
773 795 end
774 796
775 797 def self.by_assigned_to(project)
776 798 count_and_group_by(:project => project,
777 799 :field => 'assigned_to_id',
778 800 :joins => User.table_name)
779 801 end
780 802
781 803 def self.by_author(project)
782 804 count_and_group_by(:project => project,
783 805 :field => 'author_id',
784 806 :joins => User.table_name)
785 807 end
786 808
787 809 def self.by_subproject(project)
788 810 ActiveRecord::Base.connection.select_all("select s.id as status_id,
789 811 s.is_closed as closed,
790 812 #{Issue.table_name}.project_id as project_id,
791 813 count(#{Issue.table_name}.id) as total
792 814 from
793 815 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
794 816 where
795 817 #{Issue.table_name}.status_id=s.id
796 818 and #{Issue.table_name}.project_id = #{Project.table_name}.id
797 819 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
798 820 and #{Issue.table_name}.project_id <> #{project.id}
799 821 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
800 822 end
801 823 # End ReportsController extraction
802 824
803 825 # Returns an array of projects that user can assign the issue to
804 826 def allowed_target_projects(user=User.current)
805 827 if new_record?
806 828 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
807 829 else
808 830 self.class.allowed_target_projects_on_move(user)
809 831 end
810 832 end
811 833
812 834 # Returns an array of projects that user can move issues to
813 835 def self.allowed_target_projects_on_move(user=User.current)
814 836 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
815 837 end
816 838
817 839 private
818 840
819 841 def after_project_change
820 842 # Update project_id on related time entries
821 843 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
822 844
823 845 # Delete issue relations
824 846 unless Setting.cross_project_issue_relations?
825 847 relations_from.clear
826 848 relations_to.clear
827 849 end
828 850
829 851 # Move subtasks
830 852 children.each do |child|
831 853 # Change project and keep project
832 854 child.send :project=, project, true
833 855 unless child.save
834 856 raise ActiveRecord::Rollback
835 857 end
836 858 end
837 859 end
838 860
839 861 def update_nested_set_attributes
840 862 if root_id.nil?
841 863 # issue was just created
842 864 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
843 865 set_default_left_and_right
844 866 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
845 867 if @parent_issue
846 868 move_to_child_of(@parent_issue)
847 869 end
848 870 reload
849 871 elsif parent_issue_id != parent_id
850 872 former_parent_id = parent_id
851 873 # moving an existing issue
852 874 if @parent_issue && @parent_issue.root_id == root_id
853 875 # inside the same tree
854 876 move_to_child_of(@parent_issue)
855 877 else
856 878 # to another tree
857 879 unless root?
858 880 move_to_right_of(root)
859 881 reload
860 882 end
861 883 old_root_id = root_id
862 884 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
863 885 target_maxright = nested_set_scope.maximum(right_column_name) || 0
864 886 offset = target_maxright + 1 - lft
865 887 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
866 888 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
867 889 self[left_column_name] = lft + offset
868 890 self[right_column_name] = rgt + offset
869 891 if @parent_issue
870 892 move_to_child_of(@parent_issue)
871 893 end
872 894 end
873 895 reload
874 896 # delete invalid relations of all descendants
875 897 self_and_descendants.each do |issue|
876 898 issue.relations.each do |relation|
877 899 relation.destroy unless relation.valid?
878 900 end
879 901 end
880 902 # update former parent
881 903 recalculate_attributes_for(former_parent_id) if former_parent_id
882 904 end
883 905 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
884 906 end
885 907
886 908 def update_parent_attributes
887 909 recalculate_attributes_for(parent_id) if parent_id
888 910 end
889 911
890 912 def recalculate_attributes_for(issue_id)
891 913 if issue_id && p = Issue.find_by_id(issue_id)
892 914 # priority = highest priority of children
893 915 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
894 916 p.priority = IssuePriority.find_by_position(priority_position)
895 917 end
896 918
897 919 # start/due dates = lowest/highest dates of children
898 920 p.start_date = p.children.minimum(:start_date)
899 921 p.due_date = p.children.maximum(:due_date)
900 922 if p.start_date && p.due_date && p.due_date < p.start_date
901 923 p.start_date, p.due_date = p.due_date, p.start_date
902 924 end
903 925
904 926 # done ratio = weighted average ratio of leaves
905 927 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
906 928 leaves_count = p.leaves.count
907 929 if leaves_count > 0
908 930 average = p.leaves.average(:estimated_hours).to_f
909 931 if average == 0
910 932 average = 1
911 933 end
912 934 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
913 935 progress = done / (average * leaves_count)
914 936 p.done_ratio = progress.round
915 937 end
916 938 end
917 939
918 940 # estimate = sum of leaves estimates
919 941 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
920 942 p.estimated_hours = nil if p.estimated_hours == 0.0
921 943
922 944 # ancestors will be recursively updated
923 945 p.save(:validate => false)
924 946 end
925 947 end
926 948
927 949 # Update issues so their versions are not pointing to a
928 950 # fixed_version that is not shared with the issue's project
929 951 def self.update_versions(conditions=nil)
930 952 # Only need to update issues with a fixed_version from
931 953 # a different project and that is not systemwide shared
932 954 Issue.scoped(:conditions => conditions).all(
933 955 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
934 956 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
935 957 " AND #{Version.table_name}.sharing <> 'system'",
936 958 :include => [:project, :fixed_version]
937 959 ).each do |issue|
938 960 next if issue.project.nil? || issue.fixed_version.nil?
939 961 unless issue.project.shared_versions.include?(issue.fixed_version)
940 962 issue.init_journal(User.current)
941 963 issue.fixed_version = nil
942 964 issue.save
943 965 end
944 966 end
945 967 end
946 968
947 969 # Callback on attachment deletion
948 970 def attachment_added(obj)
949 971 if @current_journal && !obj.new_record?
950 972 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
951 973 end
952 974 end
953 975
954 976 # Callback on attachment deletion
955 977 def attachment_removed(obj)
956 978 if @current_journal && !obj.new_record?
957 979 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
958 980 @current_journal.save
959 981 end
960 982 end
961 983
962 984 # Default assignment based on category
963 985 def default_assign
964 986 if assigned_to.nil? && category && category.assigned_to
965 987 self.assigned_to = category.assigned_to
966 988 end
967 989 end
968 990
969 991 # Updates start/due dates of following issues
970 992 def reschedule_following_issues
971 993 if start_date_changed? || due_date_changed?
972 994 relations_from.each do |relation|
973 995 relation.set_issue_to_dates
974 996 end
975 997 end
976 998 end
977 999
978 1000 # Closes duplicates if the issue is being closed
979 1001 def close_duplicates
980 1002 if closing?
981 1003 duplicates.each do |duplicate|
982 1004 # Reload is need in case the duplicate was updated by a previous duplicate
983 1005 duplicate.reload
984 1006 # Don't re-close it if it's already closed
985 1007 next if duplicate.closed?
986 1008 # Same user and notes
987 1009 if @current_journal
988 1010 duplicate.init_journal(@current_journal.user, @current_journal.notes)
989 1011 end
990 1012 duplicate.update_attribute :status, self.status
991 1013 end
992 1014 end
993 1015 end
994 1016
995 1017 # Make sure updated_on is updated when adding a note
996 1018 def force_updated_on_change
997 1019 if @current_journal
998 1020 self.updated_on = current_time_from_proper_timezone
999 1021 end
1000 1022 end
1001 1023
1002 1024 # Saves the changes in a Journal
1003 1025 # Called after_save
1004 1026 def create_journal
1005 1027 if @current_journal
1006 1028 # attributes changes
1007 1029 if @attributes_before_change
1008 1030 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1009 1031 before = @attributes_before_change[c]
1010 1032 after = send(c)
1011 1033 next if before == after || (before.blank? && after.blank?)
1012 1034 @current_journal.details << JournalDetail.new(:property => 'attr',
1013 1035 :prop_key => c,
1014 1036 :old_value => before,
1015 1037 :value => after)
1016 1038 }
1017 1039 end
1018 1040 if @custom_values_before_change
1019 1041 # custom fields changes
1020 1042 custom_field_values.each {|c|
1021 1043 before = @custom_values_before_change[c.custom_field_id]
1022 1044 after = c.value
1023 1045 next if before == after || (before.blank? && after.blank?)
1024 1046
1025 1047 if before.is_a?(Array) || after.is_a?(Array)
1026 1048 before = [before] unless before.is_a?(Array)
1027 1049 after = [after] unless after.is_a?(Array)
1028 1050
1029 1051 # values removed
1030 1052 (before - after).reject(&:blank?).each do |value|
1031 1053 @current_journal.details << JournalDetail.new(:property => 'cf',
1032 1054 :prop_key => c.custom_field_id,
1033 1055 :old_value => value,
1034 1056 :value => nil)
1035 1057 end
1036 1058 # values added
1037 1059 (after - before).reject(&:blank?).each do |value|
1038 1060 @current_journal.details << JournalDetail.new(:property => 'cf',
1039 1061 :prop_key => c.custom_field_id,
1040 1062 :old_value => nil,
1041 1063 :value => value)
1042 1064 end
1043 1065 else
1044 1066 @current_journal.details << JournalDetail.new(:property => 'cf',
1045 1067 :prop_key => c.custom_field_id,
1046 1068 :old_value => before,
1047 1069 :value => after)
1048 1070 end
1049 1071 }
1050 1072 end
1051 1073 @current_journal.save
1052 1074 # reset current journal
1053 1075 init_journal @current_journal.user, @current_journal.notes
1054 1076 end
1055 1077 end
1056 1078
1057 1079 # Query generator for selecting groups of issue counts for a project
1058 1080 # based on specific criteria
1059 1081 #
1060 1082 # Options
1061 1083 # * project - Project to search in.
1062 1084 # * field - String. Issue field to key off of in the grouping.
1063 1085 # * joins - String. The table name to join against.
1064 1086 def self.count_and_group_by(options)
1065 1087 project = options.delete(:project)
1066 1088 select_field = options.delete(:field)
1067 1089 joins = options.delete(:joins)
1068 1090
1069 1091 where = "#{Issue.table_name}.#{select_field}=j.id"
1070 1092
1071 1093 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1072 1094 s.is_closed as closed,
1073 1095 j.id as #{select_field},
1074 1096 count(#{Issue.table_name}.id) as total
1075 1097 from
1076 1098 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1077 1099 where
1078 1100 #{Issue.table_name}.status_id=s.id
1079 1101 and #{where}
1080 1102 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1081 1103 and #{visible_condition(User.current, :project => project)}
1082 1104 group by s.id, s.is_closed, j.id")
1083 1105 end
1084 1106 end
@@ -1,1280 +1,1304
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class IssueTest < ActiveSupport::TestCase
21 21 fixtures :projects, :users, :members, :member_roles, :roles,
22 22 :groups_users,
23 23 :trackers, :projects_trackers,
24 24 :enabled_modules,
25 25 :versions,
26 26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 27 :enumerations,
28 28 :issues,
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 test_create
35 35 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
36 36 :status_id => 1, :priority => IssuePriority.all.first,
37 37 :subject => 'test_create',
38 38 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
39 39 assert issue.save
40 40 issue.reload
41 41 assert_equal 1.5, issue.estimated_hours
42 42 end
43 43
44 44 def test_create_minimal
45 45 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
46 46 :status_id => 1, :priority => IssuePriority.all.first,
47 47 :subject => 'test_create')
48 48 assert issue.save
49 49 assert issue.description.nil?
50 50 end
51 51
52 52 def test_create_with_required_custom_field
53 53 set_language_if_valid 'en'
54 54 field = IssueCustomField.find_by_name('Database')
55 55 field.update_attribute(:is_required, true)
56 56
57 57 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
58 58 :status_id => 1, :subject => 'test_create',
59 59 :description => 'IssueTest#test_create_with_required_custom_field')
60 60 assert issue.available_custom_fields.include?(field)
61 61 # No value for the custom field
62 62 assert !issue.save
63 63 assert_equal ["Database can't be blank"], issue.errors.full_messages
64 64 # Blank value
65 65 issue.custom_field_values = { field.id => '' }
66 66 assert !issue.save
67 67 assert_equal ["Database can't be blank"], issue.errors.full_messages
68 68 # Invalid value
69 69 issue.custom_field_values = { field.id => 'SQLServer' }
70 70 assert !issue.save
71 71 assert_equal ["Database is not included in the list"], issue.errors.full_messages
72 72 # Valid value
73 73 issue.custom_field_values = { field.id => 'PostgreSQL' }
74 74 assert issue.save
75 75 issue.reload
76 76 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
77 77 end
78 78
79 79 def test_create_with_group_assignment
80 80 with_settings :issue_group_assignment => '1' do
81 81 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
82 82 :subject => 'Group assignment',
83 83 :assigned_to_id => 11).save
84 84 issue = Issue.first(:order => 'id DESC')
85 85 assert_kind_of Group, issue.assigned_to
86 86 assert_equal Group.find(11), issue.assigned_to
87 87 end
88 88 end
89 89
90 90 def assert_visibility_match(user, issues)
91 91 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
92 92 end
93 93
94 94 def test_visible_scope_for_anonymous
95 95 # Anonymous user should see issues of public projects only
96 96 issues = Issue.visible(User.anonymous).all
97 97 assert issues.any?
98 98 assert_nil issues.detect {|issue| !issue.project.is_public?}
99 99 assert_nil issues.detect {|issue| issue.is_private?}
100 100 assert_visibility_match User.anonymous, issues
101 101 end
102 102
103 103 def test_visible_scope_for_anonymous_with_own_issues_visibility
104 104 Role.anonymous.update_attribute :issues_visibility, 'own'
105 105 Issue.create!(:project_id => 1, :tracker_id => 1,
106 106 :author_id => User.anonymous.id,
107 107 :subject => 'Issue by anonymous')
108 108
109 109 issues = Issue.visible(User.anonymous).all
110 110 assert issues.any?
111 111 assert_nil issues.detect {|issue| issue.author != User.anonymous}
112 112 assert_visibility_match User.anonymous, issues
113 113 end
114 114
115 115 def test_visible_scope_for_anonymous_without_view_issues_permissions
116 116 # Anonymous user should not see issues without permission
117 117 Role.anonymous.remove_permission!(:view_issues)
118 118 issues = Issue.visible(User.anonymous).all
119 119 assert issues.empty?
120 120 assert_visibility_match User.anonymous, issues
121 121 end
122 122
123 123 def test_visible_scope_for_non_member
124 124 user = User.find(9)
125 125 assert user.projects.empty?
126 126 # Non member user should see issues of public projects only
127 127 issues = Issue.visible(user).all
128 128 assert issues.any?
129 129 assert_nil issues.detect {|issue| !issue.project.is_public?}
130 130 assert_nil issues.detect {|issue| issue.is_private?}
131 131 assert_visibility_match user, issues
132 132 end
133 133
134 134 def test_visible_scope_for_non_member_with_own_issues_visibility
135 135 Role.non_member.update_attribute :issues_visibility, 'own'
136 136 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
137 137 user = User.find(9)
138 138
139 139 issues = Issue.visible(user).all
140 140 assert issues.any?
141 141 assert_nil issues.detect {|issue| issue.author != user}
142 142 assert_visibility_match user, issues
143 143 end
144 144
145 145 def test_visible_scope_for_non_member_without_view_issues_permissions
146 146 # Non member user should not see issues without permission
147 147 Role.non_member.remove_permission!(:view_issues)
148 148 user = User.find(9)
149 149 assert user.projects.empty?
150 150 issues = Issue.visible(user).all
151 151 assert issues.empty?
152 152 assert_visibility_match user, issues
153 153 end
154 154
155 155 def test_visible_scope_for_member
156 156 user = User.find(9)
157 157 # User should see issues of projects for which he has view_issues permissions only
158 158 Role.non_member.remove_permission!(:view_issues)
159 159 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
160 160 issues = Issue.visible(user).all
161 161 assert issues.any?
162 162 assert_nil issues.detect {|issue| issue.project_id != 3}
163 163 assert_nil issues.detect {|issue| issue.is_private?}
164 164 assert_visibility_match user, issues
165 165 end
166 166
167 167 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
168 168 user = User.find(8)
169 169 assert user.groups.any?
170 170 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
171 171 Role.non_member.remove_permission!(:view_issues)
172 172
173 173 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
174 174 :status_id => 1, :priority => IssuePriority.all.first,
175 175 :subject => 'Assignment test',
176 176 :assigned_to => user.groups.first,
177 177 :is_private => true)
178 178
179 179 Role.find(2).update_attribute :issues_visibility, 'default'
180 180 issues = Issue.visible(User.find(8)).all
181 181 assert issues.any?
182 182 assert issues.include?(issue)
183 183
184 184 Role.find(2).update_attribute :issues_visibility, 'own'
185 185 issues = Issue.visible(User.find(8)).all
186 186 assert issues.any?
187 187 assert issues.include?(issue)
188 188 end
189 189
190 190 def test_visible_scope_for_admin
191 191 user = User.find(1)
192 192 user.members.each(&:destroy)
193 193 assert user.projects.empty?
194 194 issues = Issue.visible(user).all
195 195 assert issues.any?
196 196 # Admin should see issues on private projects that he does not belong to
197 197 assert issues.detect {|issue| !issue.project.is_public?}
198 198 # Admin should see private issues of other users
199 199 assert issues.detect {|issue| issue.is_private? && issue.author != user}
200 200 assert_visibility_match user, issues
201 201 end
202 202
203 203 def test_visible_scope_with_project
204 204 project = Project.find(1)
205 205 issues = Issue.visible(User.find(2), :project => project).all
206 206 projects = issues.collect(&:project).uniq
207 207 assert_equal 1, projects.size
208 208 assert_equal project, projects.first
209 209 end
210 210
211 211 def test_visible_scope_with_project_and_subprojects
212 212 project = Project.find(1)
213 213 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
214 214 projects = issues.collect(&:project).uniq
215 215 assert projects.size > 1
216 216 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
217 217 end
218 218
219 219 def test_visible_and_nested_set_scopes
220 220 assert_equal 0, Issue.find(1).descendants.visible.all.size
221 221 end
222 222
223 223 def test_open_scope
224 224 issues = Issue.open.all
225 225 assert_nil issues.detect(&:closed?)
226 226 end
227 227
228 228 def test_open_scope_with_arg
229 229 issues = Issue.open(false).all
230 230 assert_equal issues, issues.select(&:closed?)
231 231 end
232 232
233 233 def test_errors_full_messages_should_include_custom_fields_errors
234 234 field = IssueCustomField.find_by_name('Database')
235 235
236 236 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
237 237 :status_id => 1, :subject => 'test_create',
238 238 :description => 'IssueTest#test_create_with_required_custom_field')
239 239 assert issue.available_custom_fields.include?(field)
240 240 # Invalid value
241 241 issue.custom_field_values = { field.id => 'SQLServer' }
242 242
243 243 assert !issue.valid?
244 244 assert_equal 1, issue.errors.full_messages.size
245 245 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
246 246 issue.errors.full_messages.first
247 247 end
248 248
249 249 def test_update_issue_with_required_custom_field
250 250 field = IssueCustomField.find_by_name('Database')
251 251 field.update_attribute(:is_required, true)
252 252
253 253 issue = Issue.find(1)
254 254 assert_nil issue.custom_value_for(field)
255 255 assert issue.available_custom_fields.include?(field)
256 256 # No change to custom values, issue can be saved
257 257 assert issue.save
258 258 # Blank value
259 259 issue.custom_field_values = { field.id => '' }
260 260 assert !issue.save
261 261 # Valid value
262 262 issue.custom_field_values = { field.id => 'PostgreSQL' }
263 263 assert issue.save
264 264 issue.reload
265 265 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
266 266 end
267 267
268 268 def test_should_not_update_attributes_if_custom_fields_validation_fails
269 269 issue = Issue.find(1)
270 270 field = IssueCustomField.find_by_name('Database')
271 271 assert issue.available_custom_fields.include?(field)
272 272
273 273 issue.custom_field_values = { field.id => 'Invalid' }
274 274 issue.subject = 'Should be not be saved'
275 275 assert !issue.save
276 276
277 277 issue.reload
278 278 assert_equal "Can't print recipes", issue.subject
279 279 end
280 280
281 281 def test_should_not_recreate_custom_values_objects_on_update
282 282 field = IssueCustomField.find_by_name('Database')
283 283
284 284 issue = Issue.find(1)
285 285 issue.custom_field_values = { field.id => 'PostgreSQL' }
286 286 assert issue.save
287 287 custom_value = issue.custom_value_for(field)
288 288 issue.reload
289 289 issue.custom_field_values = { field.id => 'MySQL' }
290 290 assert issue.save
291 291 issue.reload
292 292 assert_equal custom_value.id, issue.custom_value_for(field).id
293 293 end
294 294
295 295 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
296 296 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'Test', :custom_field_values => {'2' => 'Test'})
297 297 assert !Tracker.find(2).custom_field_ids.include?(2)
298 298
299 299 issue = Issue.find(issue.id)
300 300 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
301 301
302 302 issue = Issue.find(issue.id)
303 303 custom_value = issue.custom_value_for(2)
304 304 assert_not_nil custom_value
305 305 assert_equal 'Test', custom_value.value
306 306 end
307 307
308 308 def test_assigning_tracker_id_should_reload_custom_fields_values
309 309 issue = Issue.new(:project => Project.find(1))
310 310 assert issue.custom_field_values.empty?
311 311 issue.tracker_id = 1
312 312 assert issue.custom_field_values.any?
313 313 end
314 314
315 315 def test_assigning_attributes_should_assign_project_and_tracker_first
316 316 seq = sequence('seq')
317 317 issue = Issue.new
318 318 issue.expects(:project_id=).in_sequence(seq)
319 319 issue.expects(:tracker_id=).in_sequence(seq)
320 320 issue.expects(:subject=).in_sequence(seq)
321 321 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
322 322 end
323 323
324 324 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
325 325 attributes = ActiveSupport::OrderedHash.new
326 326 attributes['custom_field_values'] = { '1' => 'MySQL' }
327 327 attributes['tracker_id'] = '1'
328 328 issue = Issue.new(:project => Project.find(1))
329 329 issue.attributes = attributes
330 330 assert_equal 'MySQL', issue.custom_field_value(1)
331 331 end
332 332
333 333 def test_should_update_issue_with_disabled_tracker
334 334 p = Project.find(1)
335 335 issue = Issue.find(1)
336 336
337 337 p.trackers.delete(issue.tracker)
338 338 assert !p.trackers.include?(issue.tracker)
339 339
340 340 issue.reload
341 341 issue.subject = 'New subject'
342 342 assert issue.save
343 343 end
344 344
345 345 def test_should_not_set_a_disabled_tracker
346 346 p = Project.find(1)
347 347 p.trackers.delete(Tracker.find(2))
348 348
349 349 issue = Issue.find(1)
350 350 issue.tracker_id = 2
351 351 issue.subject = 'New subject'
352 352 assert !issue.save
353 353 assert_not_nil issue.errors[:tracker_id]
354 354 end
355 355
356 356 def test_category_based_assignment
357 357 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
358 358 :status_id => 1, :priority => IssuePriority.all.first,
359 359 :subject => 'Assignment test',
360 360 :description => 'Assignment test', :category_id => 1)
361 361 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
362 362 end
363 363
364 364 def test_new_statuses_allowed_to
365 365 Workflow.delete_all
366 366
367 367 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
368 368 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
369 369 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
370 370 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
371 371 status = IssueStatus.find(1)
372 372 role = Role.find(1)
373 373 tracker = Tracker.find(1)
374 374 user = User.find(2)
375 375
376 376 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author_id => 1)
377 377 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
378 378
379 379 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
380 380 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
381 381
382 382 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author_id => 1, :assigned_to => user)
383 383 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
384 384
385 385 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
386 386 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
387 387 end
388 388
389 389 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
390 390 admin = User.find(1)
391 391 issue = Issue.find(1)
392 392 assert !admin.member_of?(issue.project)
393 393 expected_statuses = [issue.status] + Workflow.find_all_by_old_status_id(issue.status_id).map(&:new_status).uniq.sort
394 394
395 395 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
396 396 end
397 397
398 398 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
399 399 issue = Issue.find(1).copy
400 400 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
401 401
402 402 issue = Issue.find(2).copy
403 403 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
404 404 end
405 405
406 406 def test_copy
407 407 issue = Issue.new.copy_from(1)
408 408 assert issue.copy?
409 409 assert issue.save
410 410 issue.reload
411 411 orig = Issue.find(1)
412 412 assert_equal orig.subject, issue.subject
413 413 assert_equal orig.tracker, issue.tracker
414 414 assert_equal "125", issue.custom_value_for(2).value
415 415 end
416 416
417 417 def test_copy_should_copy_status
418 418 orig = Issue.find(8)
419 419 assert orig.status != IssueStatus.default
420 420
421 421 issue = Issue.new.copy_from(orig)
422 422 assert issue.save
423 423 issue.reload
424 424 assert_equal orig.status, issue.status
425 425 end
426 426
427 427 def test_should_not_call_after_project_change_on_creation
428 428 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Test', :author_id => 1)
429 429 issue.expects(:after_project_change).never
430 430 issue.save!
431 431 end
432 432
433 433 def test_should_not_call_after_project_change_on_update
434 434 issue = Issue.find(1)
435 435 issue.project = Project.find(1)
436 436 issue.subject = 'No project change'
437 437 issue.expects(:after_project_change).never
438 438 issue.save!
439 439 end
440 440
441 441 def test_should_call_after_project_change_on_project_change
442 442 issue = Issue.find(1)
443 443 issue.project = Project.find(2)
444 444 issue.expects(:after_project_change).once
445 445 issue.save!
446 446 end
447 447
448 448 def test_adding_journal_should_update_timestamp
449 449 issue = Issue.find(1)
450 450 updated_on_was = issue.updated_on
451 451
452 452 issue.init_journal(User.first, "Adding notes")
453 453 assert_difference 'Journal.count' do
454 454 assert issue.save
455 455 end
456 456 issue.reload
457 457
458 458 assert_not_equal updated_on_was, issue.updated_on
459 459 end
460 460
461 461 def test_should_close_duplicates
462 462 # Create 3 issues
463 463 project = Project.find(1)
464 464 issue1 = Issue.generate_for_project!(project)
465 465 issue2 = Issue.generate_for_project!(project)
466 466 issue3 = Issue.generate_for_project!(project)
467 467
468 468 # 2 is a dupe of 1
469 469 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
470 470 # And 3 is a dupe of 2
471 471 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
472 472 # And 3 is a dupe of 1 (circular duplicates)
473 473 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
474 474
475 475 assert issue1.reload.duplicates.include?(issue2)
476 476
477 477 # Closing issue 1
478 478 issue1.init_journal(User.find(:first), "Closing issue1")
479 479 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
480 480 assert issue1.save
481 481 # 2 and 3 should be also closed
482 482 assert issue2.reload.closed?
483 483 assert issue3.reload.closed?
484 484 end
485 485
486 486 def test_should_not_close_duplicated_issue
487 487 project = Project.find(1)
488 488 issue1 = Issue.generate_for_project!(project)
489 489 issue2 = Issue.generate_for_project!(project)
490 490
491 491 # 2 is a dupe of 1
492 492 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
493 493 # 2 is a dup of 1 but 1 is not a duplicate of 2
494 494 assert !issue2.reload.duplicates.include?(issue1)
495 495
496 496 # Closing issue 2
497 497 issue2.init_journal(User.find(:first), "Closing issue2")
498 498 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
499 499 assert issue2.save
500 500 # 1 should not be also closed
501 501 assert !issue1.reload.closed?
502 502 end
503 503
504 504 def test_assignable_versions
505 505 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
506 506 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
507 507 end
508 508
509 509 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
510 510 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
511 511 assert !issue.save
512 512 assert_not_nil issue.errors[:fixed_version_id]
513 513 end
514 514
515 515 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
516 516 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
517 517 assert !issue.save
518 518 assert_not_nil issue.errors[:fixed_version_id]
519 519 end
520 520
521 521 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
522 522 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
523 523 assert issue.save
524 524 end
525 525
526 526 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
527 527 issue = Issue.find(11)
528 528 assert_equal 'closed', issue.fixed_version.status
529 529 issue.subject = 'Subject changed'
530 530 assert issue.save
531 531 end
532 532
533 533 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
534 534 issue = Issue.find(11)
535 535 issue.status_id = 1
536 536 assert !issue.save
537 537 assert_not_nil issue.errors[:base]
538 538 end
539 539
540 540 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
541 541 issue = Issue.find(11)
542 542 issue.status_id = 1
543 543 issue.fixed_version_id = 3
544 544 assert issue.save
545 545 end
546 546
547 547 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
548 548 issue = Issue.find(12)
549 549 assert_equal 'locked', issue.fixed_version.status
550 550 issue.status_id = 1
551 551 assert issue.save
552 552 end
553 553
554 554 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
555 555 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
556 556 end
557 557
558 558 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
559 559 Project.find(2).disable_module! :issue_tracking
560 560 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
561 561 end
562 562
563 563 def test_move_to_another_project_with_same_category
564 564 issue = Issue.find(1)
565 565 issue.project = Project.find(2)
566 566 assert issue.save
567 567 issue.reload
568 568 assert_equal 2, issue.project_id
569 569 # Category changes
570 570 assert_equal 4, issue.category_id
571 571 # Make sure time entries were move to the target project
572 572 assert_equal 2, issue.time_entries.first.project_id
573 573 end
574 574
575 575 def test_move_to_another_project_without_same_category
576 576 issue = Issue.find(2)
577 577 issue.project = Project.find(2)
578 578 assert issue.save
579 579 issue.reload
580 580 assert_equal 2, issue.project_id
581 581 # Category cleared
582 582 assert_nil issue.category_id
583 583 end
584 584
585 585 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
586 586 issue = Issue.find(1)
587 587 issue.update_attribute(:fixed_version_id, 1)
588 588 issue.project = Project.find(2)
589 589 assert issue.save
590 590 issue.reload
591 591 assert_equal 2, issue.project_id
592 592 # Cleared fixed_version
593 593 assert_equal nil, issue.fixed_version
594 594 end
595 595
596 596 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
597 597 issue = Issue.find(1)
598 598 issue.update_attribute(:fixed_version_id, 4)
599 599 issue.project = Project.find(5)
600 600 assert issue.save
601 601 issue.reload
602 602 assert_equal 5, issue.project_id
603 603 # Keep fixed_version
604 604 assert_equal 4, issue.fixed_version_id
605 605 end
606 606
607 607 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
608 608 issue = Issue.find(1)
609 609 issue.update_attribute(:fixed_version_id, 1)
610 610 issue.project = Project.find(5)
611 611 assert issue.save
612 612 issue.reload
613 613 assert_equal 5, issue.project_id
614 614 # Cleared fixed_version
615 615 assert_equal nil, issue.fixed_version
616 616 end
617 617
618 618 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
619 619 issue = Issue.find(1)
620 620 issue.update_attribute(:fixed_version_id, 7)
621 621 issue.project = Project.find(2)
622 622 assert issue.save
623 623 issue.reload
624 624 assert_equal 2, issue.project_id
625 625 # Keep fixed_version
626 626 assert_equal 7, issue.fixed_version_id
627 627 end
628 628
629 629 def test_move_to_another_project_with_disabled_tracker
630 630 issue = Issue.find(1)
631 631 target = Project.find(2)
632 632 target.tracker_ids = [3]
633 633 target.save
634 634 issue.project = target
635 635 assert issue.save
636 636 issue.reload
637 637 assert_equal 2, issue.project_id
638 638 assert_equal 3, issue.tracker_id
639 639 end
640 640
641 641 def test_copy_to_the_same_project
642 642 issue = Issue.find(1)
643 643 copy = issue.copy
644 644 assert_difference 'Issue.count' do
645 645 copy.save!
646 646 end
647 647 assert_kind_of Issue, copy
648 648 assert_equal issue.project, copy.project
649 649 assert_equal "125", copy.custom_value_for(2).value
650 650 end
651 651
652 652 def test_copy_to_another_project_and_tracker
653 653 issue = Issue.find(1)
654 654 copy = issue.copy(:project_id => 3, :tracker_id => 2)
655 655 assert_difference 'Issue.count' do
656 656 copy.save!
657 657 end
658 658 copy.reload
659 659 assert_kind_of Issue, copy
660 660 assert_equal Project.find(3), copy.project
661 661 assert_equal Tracker.find(2), copy.tracker
662 662 # Custom field #2 is not associated with target tracker
663 663 assert_nil copy.custom_value_for(2)
664 664 end
665 665
666 666 context "#copy" do
667 667 setup do
668 668 @issue = Issue.find(1)
669 669 end
670 670
671 671 should "not create a journal" do
672 672 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
673 673 copy.save!
674 674 assert_equal 0, copy.reload.journals.size
675 675 end
676 676
677 677 should "allow assigned_to changes" do
678 678 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
679 679 assert_equal 3, copy.assigned_to_id
680 680 end
681 681
682 682 should "allow status changes" do
683 683 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
684 684 assert_equal 2, copy.status_id
685 685 end
686 686
687 687 should "allow start date changes" do
688 688 date = Date.today
689 689 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
690 690 assert_equal date, copy.start_date
691 691 end
692 692
693 693 should "allow due date changes" do
694 694 date = Date.today
695 695 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
696 696 assert_equal date, copy.due_date
697 697 end
698 698
699 699 should "set current user as author" do
700 700 User.current = User.find(9)
701 701 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
702 702 assert_equal User.current, copy.author
703 703 end
704 704
705 705 should "create a journal with notes" do
706 706 date = Date.today
707 707 notes = "Notes added when copying"
708 708 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
709 709 copy.init_journal(User.current, notes)
710 710 copy.save!
711 711
712 712 assert_equal 1, copy.journals.size
713 713 journal = copy.journals.first
714 714 assert_equal 0, journal.details.size
715 715 assert_equal notes, journal.notes
716 716 end
717 717 end
718 718
719 719 def test_recipients_should_include_previous_assignee
720 720 user = User.find(3)
721 721 user.members.update_all ["mail_notification = ?", false]
722 722 user.update_attribute :mail_notification, 'only_assigned'
723 723
724 724 issue = Issue.find(2)
725 725 issue.assigned_to = nil
726 726 assert_include user.mail, issue.recipients
727 727 issue.save!
728 728 assert !issue.recipients.include?(user.mail)
729 729 end
730 730
731 731 def test_recipients_should_not_include_users_that_cannot_view_the_issue
732 732 issue = Issue.find(12)
733 733 assert issue.recipients.include?(issue.author.mail)
734 734 # copy the issue to a private project
735 735 copy = issue.copy(:project_id => 5, :tracker_id => 2)
736 736 # author is not a member of project anymore
737 737 assert !copy.recipients.include?(copy.author.mail)
738 738 end
739 739
740 740 def test_recipients_should_include_the_assigned_group_members
741 741 group_member = User.generate!
742 742 group = Group.generate!
743 743 group.users << group_member
744 744
745 745 issue = Issue.find(12)
746 746 issue.assigned_to = group
747 747 assert issue.recipients.include?(group_member.mail)
748 748 end
749 749
750 750 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
751 751 user = User.find(3)
752 752 issue = Issue.find(9)
753 753 Watcher.create!(:user => user, :watchable => issue)
754 754 assert issue.watched_by?(user)
755 755 assert !issue.watcher_recipients.include?(user.mail)
756 756 end
757 757
758 758 def test_issue_destroy
759 759 Issue.find(1).destroy
760 760 assert_nil Issue.find_by_id(1)
761 761 assert_nil TimeEntry.find_by_issue_id(1)
762 762 end
763 763
764 def test_destroying_a_deleted_issue_should_not_raise_an_error
765 issue = Issue.find(1)
766 Issue.find(1).destroy
767
768 assert_nothing_raised do
769 assert_no_difference 'Issue.count' do
770 issue.destroy
771 end
772 assert issue.destroyed?
773 end
774 end
775
776 def test_destroying_a_stale_issue_should_not_raise_an_error
777 issue = Issue.find(1)
778 Issue.find(1).update_attribute :subject, "Updated"
779
780 assert_nothing_raised do
781 assert_difference 'Issue.count', -1 do
782 issue.destroy
783 end
784 assert issue.destroyed?
785 end
786 end
787
764 788 def test_blocked
765 789 blocked_issue = Issue.find(9)
766 790 blocking_issue = Issue.find(10)
767 791
768 792 assert blocked_issue.blocked?
769 793 assert !blocking_issue.blocked?
770 794 end
771 795
772 796 def test_blocked_issues_dont_allow_closed_statuses
773 797 blocked_issue = Issue.find(9)
774 798
775 799 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
776 800 assert !allowed_statuses.empty?
777 801 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
778 802 assert closed_statuses.empty?
779 803 end
780 804
781 805 def test_unblocked_issues_allow_closed_statuses
782 806 blocking_issue = Issue.find(10)
783 807
784 808 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
785 809 assert !allowed_statuses.empty?
786 810 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
787 811 assert !closed_statuses.empty?
788 812 end
789 813
790 814 def test_rescheduling_an_issue_should_reschedule_following_issue
791 815 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
792 816 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
793 817 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
794 818 assert_equal issue1.due_date + 1, issue2.reload.start_date
795 819
796 820 issue1.due_date = Date.today + 5
797 821 issue1.save!
798 822 assert_equal issue1.due_date + 1, issue2.reload.start_date
799 823 end
800 824
801 825 def test_rescheduling_a_stale_issue_should_not_raise_an_error
802 826 stale = Issue.find(1)
803 827 issue = Issue.find(1)
804 828 issue.subject = "Updated"
805 829 issue.save!
806 830
807 831 date = 10.days.from_now.to_date
808 832 assert_nothing_raised do
809 833 stale.reschedule_after(date)
810 834 end
811 835 assert_equal date, stale.reload.start_date
812 836 end
813 837
814 838 def test_overdue
815 839 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
816 840 assert !Issue.new(:due_date => Date.today).overdue?
817 841 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
818 842 assert !Issue.new(:due_date => nil).overdue?
819 843 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
820 844 end
821 845
822 846 context "#behind_schedule?" do
823 847 should "be false if the issue has no start_date" do
824 848 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
825 849 end
826 850
827 851 should "be false if the issue has no end_date" do
828 852 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
829 853 end
830 854
831 855 should "be false if the issue has more done than it's calendar time" do
832 856 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
833 857 end
834 858
835 859 should "be true if the issue hasn't been started at all" do
836 860 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
837 861 end
838 862
839 863 should "be true if the issue has used more calendar time than it's done ratio" do
840 864 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
841 865 end
842 866 end
843 867
844 868 context "#assignable_users" do
845 869 should "be Users" do
846 870 assert_kind_of User, Issue.find(1).assignable_users.first
847 871 end
848 872
849 873 should "include the issue author" do
850 874 project = Project.find(1)
851 875 non_project_member = User.generate!
852 876 issue = Issue.generate_for_project!(project, :author => non_project_member)
853 877
854 878 assert issue.assignable_users.include?(non_project_member)
855 879 end
856 880
857 881 should "include the current assignee" do
858 882 project = Project.find(1)
859 883 user = User.generate!
860 884 issue = Issue.generate_for_project!(project, :assigned_to => user)
861 885 user.lock!
862 886
863 887 assert Issue.find(issue.id).assignable_users.include?(user)
864 888 end
865 889
866 890 should "not show the issue author twice" do
867 891 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
868 892 assert_equal 2, assignable_user_ids.length
869 893
870 894 assignable_user_ids.each do |user_id|
871 895 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
872 896 end
873 897 end
874 898
875 899 context "with issue_group_assignment" do
876 900 should "include groups" do
877 901 issue = Issue.new(:project => Project.find(2))
878 902
879 903 with_settings :issue_group_assignment => '1' do
880 904 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
881 905 assert issue.assignable_users.include?(Group.find(11))
882 906 end
883 907 end
884 908 end
885 909
886 910 context "without issue_group_assignment" do
887 911 should "not include groups" do
888 912 issue = Issue.new(:project => Project.find(2))
889 913
890 914 with_settings :issue_group_assignment => '0' do
891 915 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
892 916 assert !issue.assignable_users.include?(Group.find(11))
893 917 end
894 918 end
895 919 end
896 920 end
897 921
898 922 def test_create_should_send_email_notification
899 923 ActionMailer::Base.deliveries.clear
900 924 issue = Issue.new(:project_id => 1, :tracker_id => 1,
901 925 :author_id => 3, :status_id => 1,
902 926 :priority => IssuePriority.all.first,
903 927 :subject => 'test_create', :estimated_hours => '1:30')
904 928
905 929 assert issue.save
906 930 assert_equal 1, ActionMailer::Base.deliveries.size
907 931 end
908 932
909 933 def test_stale_issue_should_not_send_email_notification
910 934 ActionMailer::Base.deliveries.clear
911 935 issue = Issue.find(1)
912 936 stale = Issue.find(1)
913 937
914 938 issue.init_journal(User.find(1))
915 939 issue.subject = 'Subjet update'
916 940 assert issue.save
917 941 assert_equal 1, ActionMailer::Base.deliveries.size
918 942 ActionMailer::Base.deliveries.clear
919 943
920 944 stale.init_journal(User.find(1))
921 945 stale.subject = 'Another subjet update'
922 946 assert_raise ActiveRecord::StaleObjectError do
923 947 stale.save
924 948 end
925 949 assert ActionMailer::Base.deliveries.empty?
926 950 end
927 951
928 952 def test_journalized_description
929 953 IssueCustomField.delete_all
930 954
931 955 i = Issue.first
932 956 old_description = i.description
933 957 new_description = "This is the new description"
934 958
935 959 i.init_journal(User.find(2))
936 960 i.description = new_description
937 961 assert_difference 'Journal.count', 1 do
938 962 assert_difference 'JournalDetail.count', 1 do
939 963 i.save!
940 964 end
941 965 end
942 966
943 967 detail = JournalDetail.first(:order => 'id DESC')
944 968 assert_equal i, detail.journal.journalized
945 969 assert_equal 'attr', detail.property
946 970 assert_equal 'description', detail.prop_key
947 971 assert_equal old_description, detail.old_value
948 972 assert_equal new_description, detail.value
949 973 end
950 974
951 975 def test_blank_descriptions_should_not_be_journalized
952 976 IssueCustomField.delete_all
953 977 Issue.update_all("description = NULL", "id=1")
954 978
955 979 i = Issue.find(1)
956 980 i.init_journal(User.find(2))
957 981 i.subject = "blank description"
958 982 i.description = "\r\n"
959 983
960 984 assert_difference 'Journal.count', 1 do
961 985 assert_difference 'JournalDetail.count', 1 do
962 986 i.save!
963 987 end
964 988 end
965 989 end
966 990
967 991 def test_journalized_multi_custom_field
968 992 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
969 993 :tracker_ids => [1], :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
970 994
971 995 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'Test', :author_id => 1)
972 996
973 997 assert_difference 'Journal.count' do
974 998 assert_difference 'JournalDetail.count' do
975 999 issue.init_journal(User.first)
976 1000 issue.custom_field_values = {field.id => ['value1']}
977 1001 issue.save!
978 1002 end
979 1003 assert_difference 'JournalDetail.count' do
980 1004 issue.init_journal(User.first)
981 1005 issue.custom_field_values = {field.id => ['value1', 'value2']}
982 1006 issue.save!
983 1007 end
984 1008 assert_difference 'JournalDetail.count', 2 do
985 1009 issue.init_journal(User.first)
986 1010 issue.custom_field_values = {field.id => ['value3', 'value2']}
987 1011 issue.save!
988 1012 end
989 1013 assert_difference 'JournalDetail.count', 2 do
990 1014 issue.init_journal(User.first)
991 1015 issue.custom_field_values = {field.id => nil}
992 1016 issue.save!
993 1017 end
994 1018 end
995 1019 end
996 1020
997 1021 def test_description_eol_should_be_normalized
998 1022 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
999 1023 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1000 1024 end
1001 1025
1002 1026 def test_saving_twice_should_not_duplicate_journal_details
1003 1027 i = Issue.find(:first)
1004 1028 i.init_journal(User.find(2), 'Some notes')
1005 1029 # initial changes
1006 1030 i.subject = 'New subject'
1007 1031 i.done_ratio = i.done_ratio + 10
1008 1032 assert_difference 'Journal.count' do
1009 1033 assert i.save
1010 1034 end
1011 1035 # 1 more change
1012 1036 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
1013 1037 assert_no_difference 'Journal.count' do
1014 1038 assert_difference 'JournalDetail.count', 1 do
1015 1039 i.save
1016 1040 end
1017 1041 end
1018 1042 # no more change
1019 1043 assert_no_difference 'Journal.count' do
1020 1044 assert_no_difference 'JournalDetail.count' do
1021 1045 i.save
1022 1046 end
1023 1047 end
1024 1048 end
1025 1049
1026 1050 def test_all_dependent_issues
1027 1051 IssueRelation.delete_all
1028 1052 assert IssueRelation.create!(:issue_from => Issue.find(1),
1029 1053 :issue_to => Issue.find(2),
1030 1054 :relation_type => IssueRelation::TYPE_PRECEDES)
1031 1055 assert IssueRelation.create!(:issue_from => Issue.find(2),
1032 1056 :issue_to => Issue.find(3),
1033 1057 :relation_type => IssueRelation::TYPE_PRECEDES)
1034 1058 assert IssueRelation.create!(:issue_from => Issue.find(3),
1035 1059 :issue_to => Issue.find(8),
1036 1060 :relation_type => IssueRelation::TYPE_PRECEDES)
1037 1061
1038 1062 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1039 1063 end
1040 1064
1041 1065 def test_all_dependent_issues_with_persistent_circular_dependency
1042 1066 IssueRelation.delete_all
1043 1067 assert IssueRelation.create!(:issue_from => Issue.find(1),
1044 1068 :issue_to => Issue.find(2),
1045 1069 :relation_type => IssueRelation::TYPE_PRECEDES)
1046 1070 assert IssueRelation.create!(:issue_from => Issue.find(2),
1047 1071 :issue_to => Issue.find(3),
1048 1072 :relation_type => IssueRelation::TYPE_PRECEDES)
1049 1073
1050 1074 r = IssueRelation.create!(:issue_from => Issue.find(3),
1051 1075 :issue_to => Issue.find(7),
1052 1076 :relation_type => IssueRelation::TYPE_PRECEDES)
1053 1077 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1054 1078
1055 1079 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1056 1080 end
1057 1081
1058 1082 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1059 1083 IssueRelation.delete_all
1060 1084 assert IssueRelation.create!(:issue_from => Issue.find(1),
1061 1085 :issue_to => Issue.find(2),
1062 1086 :relation_type => IssueRelation::TYPE_RELATES)
1063 1087 assert IssueRelation.create!(:issue_from => Issue.find(2),
1064 1088 :issue_to => Issue.find(3),
1065 1089 :relation_type => IssueRelation::TYPE_RELATES)
1066 1090 assert IssueRelation.create!(:issue_from => Issue.find(3),
1067 1091 :issue_to => Issue.find(8),
1068 1092 :relation_type => IssueRelation::TYPE_RELATES)
1069 1093
1070 1094 r = IssueRelation.create!(:issue_from => Issue.find(8),
1071 1095 :issue_to => Issue.find(7),
1072 1096 :relation_type => IssueRelation::TYPE_RELATES)
1073 1097 IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id])
1074 1098
1075 1099 r = IssueRelation.create!(:issue_from => Issue.find(3),
1076 1100 :issue_to => Issue.find(7),
1077 1101 :relation_type => IssueRelation::TYPE_RELATES)
1078 1102 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1079 1103
1080 1104 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1081 1105 end
1082 1106
1083 1107 context "#done_ratio" do
1084 1108 setup do
1085 1109 @issue = Issue.find(1)
1086 1110 @issue_status = IssueStatus.find(1)
1087 1111 @issue_status.update_attribute(:default_done_ratio, 50)
1088 1112 @issue2 = Issue.find(2)
1089 1113 @issue_status2 = IssueStatus.find(2)
1090 1114 @issue_status2.update_attribute(:default_done_ratio, 0)
1091 1115 end
1092 1116
1093 1117 teardown do
1094 1118 Setting.issue_done_ratio = 'issue_field'
1095 1119 end
1096 1120
1097 1121 context "with Setting.issue_done_ratio using the issue_field" do
1098 1122 setup do
1099 1123 Setting.issue_done_ratio = 'issue_field'
1100 1124 end
1101 1125
1102 1126 should "read the issue's field" do
1103 1127 assert_equal 0, @issue.done_ratio
1104 1128 assert_equal 30, @issue2.done_ratio
1105 1129 end
1106 1130 end
1107 1131
1108 1132 context "with Setting.issue_done_ratio using the issue_status" do
1109 1133 setup do
1110 1134 Setting.issue_done_ratio = 'issue_status'
1111 1135 end
1112 1136
1113 1137 should "read the Issue Status's default done ratio" do
1114 1138 assert_equal 50, @issue.done_ratio
1115 1139 assert_equal 0, @issue2.done_ratio
1116 1140 end
1117 1141 end
1118 1142 end
1119 1143
1120 1144 context "#update_done_ratio_from_issue_status" do
1121 1145 setup do
1122 1146 @issue = Issue.find(1)
1123 1147 @issue_status = IssueStatus.find(1)
1124 1148 @issue_status.update_attribute(:default_done_ratio, 50)
1125 1149 @issue2 = Issue.find(2)
1126 1150 @issue_status2 = IssueStatus.find(2)
1127 1151 @issue_status2.update_attribute(:default_done_ratio, 0)
1128 1152 end
1129 1153
1130 1154 context "with Setting.issue_done_ratio using the issue_field" do
1131 1155 setup do
1132 1156 Setting.issue_done_ratio = 'issue_field'
1133 1157 end
1134 1158
1135 1159 should "not change the issue" do
1136 1160 @issue.update_done_ratio_from_issue_status
1137 1161 @issue2.update_done_ratio_from_issue_status
1138 1162
1139 1163 assert_equal 0, @issue.read_attribute(:done_ratio)
1140 1164 assert_equal 30, @issue2.read_attribute(:done_ratio)
1141 1165 end
1142 1166 end
1143 1167
1144 1168 context "with Setting.issue_done_ratio using the issue_status" do
1145 1169 setup do
1146 1170 Setting.issue_done_ratio = 'issue_status'
1147 1171 end
1148 1172
1149 1173 should "change the issue's done ratio" do
1150 1174 @issue.update_done_ratio_from_issue_status
1151 1175 @issue2.update_done_ratio_from_issue_status
1152 1176
1153 1177 assert_equal 50, @issue.read_attribute(:done_ratio)
1154 1178 assert_equal 0, @issue2.read_attribute(:done_ratio)
1155 1179 end
1156 1180 end
1157 1181 end
1158 1182
1159 1183 test "#by_tracker" do
1160 1184 User.current = User.anonymous
1161 1185 groups = Issue.by_tracker(Project.find(1))
1162 1186 assert_equal 3, groups.size
1163 1187 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1164 1188 end
1165 1189
1166 1190 test "#by_version" do
1167 1191 User.current = User.anonymous
1168 1192 groups = Issue.by_version(Project.find(1))
1169 1193 assert_equal 3, groups.size
1170 1194 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1171 1195 end
1172 1196
1173 1197 test "#by_priority" do
1174 1198 User.current = User.anonymous
1175 1199 groups = Issue.by_priority(Project.find(1))
1176 1200 assert_equal 4, groups.size
1177 1201 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1178 1202 end
1179 1203
1180 1204 test "#by_category" do
1181 1205 User.current = User.anonymous
1182 1206 groups = Issue.by_category(Project.find(1))
1183 1207 assert_equal 2, groups.size
1184 1208 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1185 1209 end
1186 1210
1187 1211 test "#by_assigned_to" do
1188 1212 User.current = User.anonymous
1189 1213 groups = Issue.by_assigned_to(Project.find(1))
1190 1214 assert_equal 2, groups.size
1191 1215 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1192 1216 end
1193 1217
1194 1218 test "#by_author" do
1195 1219 User.current = User.anonymous
1196 1220 groups = Issue.by_author(Project.find(1))
1197 1221 assert_equal 4, groups.size
1198 1222 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1199 1223 end
1200 1224
1201 1225 test "#by_subproject" do
1202 1226 User.current = User.anonymous
1203 1227 groups = Issue.by_subproject(Project.find(1))
1204 1228 # Private descendant not visible
1205 1229 assert_equal 1, groups.size
1206 1230 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1207 1231 end
1208 1232
1209 1233 def test_recently_updated_with_limit_scopes
1210 1234 #should return the last updated issue
1211 1235 assert_equal 1, Issue.recently_updated.with_limit(1).length
1212 1236 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1213 1237 end
1214 1238
1215 1239 def test_on_active_projects_scope
1216 1240 assert Project.find(2).archive
1217 1241
1218 1242 before = Issue.on_active_project.length
1219 1243 # test inclusion to results
1220 1244 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1221 1245 assert_equal before + 1, Issue.on_active_project.length
1222 1246
1223 1247 # Move to an archived project
1224 1248 issue.project = Project.find(2)
1225 1249 assert issue.save
1226 1250 assert_equal before, Issue.on_active_project.length
1227 1251 end
1228 1252
1229 1253 context "Issue#recipients" do
1230 1254 setup do
1231 1255 @project = Project.find(1)
1232 1256 @author = User.generate!
1233 1257 @assignee = User.generate!
1234 1258 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1235 1259 end
1236 1260
1237 1261 should "include project recipients" do
1238 1262 assert @project.recipients.present?
1239 1263 @project.recipients.each do |project_recipient|
1240 1264 assert @issue.recipients.include?(project_recipient)
1241 1265 end
1242 1266 end
1243 1267
1244 1268 should "include the author if the author is active" do
1245 1269 assert @issue.author, "No author set for Issue"
1246 1270 assert @issue.recipients.include?(@issue.author.mail)
1247 1271 end
1248 1272
1249 1273 should "include the assigned to user if the assigned to user is active" do
1250 1274 assert @issue.assigned_to, "No assigned_to set for Issue"
1251 1275 assert @issue.recipients.include?(@issue.assigned_to.mail)
1252 1276 end
1253 1277
1254 1278 should "not include users who opt out of all email" do
1255 1279 @author.update_attribute(:mail_notification, :none)
1256 1280
1257 1281 assert !@issue.recipients.include?(@issue.author.mail)
1258 1282 end
1259 1283
1260 1284 should "not include the issue author if they are only notified of assigned issues" do
1261 1285 @author.update_attribute(:mail_notification, :only_assigned)
1262 1286
1263 1287 assert !@issue.recipients.include?(@issue.author.mail)
1264 1288 end
1265 1289
1266 1290 should "not include the assigned user if they are only notified of owned issues" do
1267 1291 @assignee.update_attribute(:mail_notification, :only_owner)
1268 1292
1269 1293 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1270 1294 end
1271 1295 end
1272 1296
1273 1297 def test_last_journal_id_with_journals_should_return_the_journal_id
1274 1298 assert_equal 2, Issue.find(1).last_journal_id
1275 1299 end
1276 1300
1277 1301 def test_last_journal_id_without_journals_should_return_nil
1278 1302 assert_nil Issue.find(3).last_journal_id
1279 1303 end
1280 1304 end
@@ -1,1164 +1,1176
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class ProjectTest < ActiveSupport::TestCase
21 21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 22 :journals, :journal_details,
23 23 :enumerations, :users, :issue_categories,
24 24 :projects_trackers,
25 25 :custom_fields,
26 26 :custom_fields_projects,
27 27 :custom_fields_trackers,
28 28 :custom_values,
29 29 :roles,
30 30 :member_roles,
31 31 :members,
32 32 :enabled_modules,
33 33 :workflows,
34 34 :versions,
35 35 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
36 36 :groups_users,
37 37 :boards,
38 38 :repositories
39 39
40 40 def setup
41 41 @ecookbook = Project.find(1)
42 42 @ecookbook_sub1 = Project.find(3)
43 43 set_tmp_attachments_directory
44 44 User.current = nil
45 45 end
46 46
47 47 def test_truth
48 48 assert_kind_of Project, @ecookbook
49 49 assert_equal "eCookbook", @ecookbook.name
50 50 end
51 51
52 52 def test_default_attributes
53 53 with_settings :default_projects_public => '1' do
54 54 assert_equal true, Project.new.is_public
55 55 assert_equal false, Project.new(:is_public => false).is_public
56 56 end
57 57
58 58 with_settings :default_projects_public => '0' do
59 59 assert_equal false, Project.new.is_public
60 60 assert_equal true, Project.new(:is_public => true).is_public
61 61 end
62 62
63 63 with_settings :sequential_project_identifiers => '1' do
64 64 assert !Project.new.identifier.blank?
65 65 assert Project.new(:identifier => '').identifier.blank?
66 66 end
67 67
68 68 with_settings :sequential_project_identifiers => '0' do
69 69 assert Project.new.identifier.blank?
70 70 assert !Project.new(:identifier => 'test').blank?
71 71 end
72 72
73 73 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
74 74 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
75 75 end
76 76
77 77 assert_equal Tracker.all.sort, Project.new.trackers.sort
78 78 assert_equal Tracker.find(1, 3).sort, Project.new(:tracker_ids => [1, 3]).trackers.sort
79 79 end
80 80
81 81 def test_update
82 82 assert_equal "eCookbook", @ecookbook.name
83 83 @ecookbook.name = "eCook"
84 84 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
85 85 @ecookbook.reload
86 86 assert_equal "eCook", @ecookbook.name
87 87 end
88 88
89 89 def test_validate_identifier
90 90 to_test = {"abc" => true,
91 91 "ab12" => true,
92 92 "ab-12" => true,
93 93 "ab_12" => true,
94 94 "12" => false,
95 95 "new" => false}
96 96
97 97 to_test.each do |identifier, valid|
98 98 p = Project.new
99 99 p.identifier = identifier
100 100 p.valid?
101 101 if valid
102 102 assert p.errors['identifier'].blank?, "identifier #{identifier} was not valid"
103 103 else
104 104 assert p.errors['identifier'].present?, "identifier #{identifier} was valid"
105 105 end
106 106 end
107 107 end
108 108
109 109 def test_identifier_should_not_be_frozen_for_a_new_project
110 110 assert_equal false, Project.new.identifier_frozen?
111 111 end
112 112
113 113 def test_identifier_should_not_be_frozen_for_a_saved_project_with_blank_identifier
114 114 Project.update_all(["identifier = ''"], "id = 1")
115 115
116 116 assert_equal false, Project.find(1).identifier_frozen?
117 117 end
118 118
119 119 def test_identifier_should_be_frozen_for_a_saved_project_with_valid_identifier
120 120 assert_equal true, Project.find(1).identifier_frozen?
121 121 end
122 122
123 123 def test_members_should_be_active_users
124 124 Project.all.each do |project|
125 125 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
126 126 end
127 127 end
128 128
129 129 def test_users_should_be_active_users
130 130 Project.all.each do |project|
131 131 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
132 132 end
133 133 end
134 134
135 135 def test_archive
136 136 user = @ecookbook.members.first.user
137 137 @ecookbook.archive
138 138 @ecookbook.reload
139 139
140 140 assert !@ecookbook.active?
141 141 assert @ecookbook.archived?
142 142 assert !user.projects.include?(@ecookbook)
143 143 # Subproject are also archived
144 144 assert !@ecookbook.children.empty?
145 145 assert @ecookbook.descendants.active.empty?
146 146 end
147 147
148 148 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
149 149 # Assign an issue of a project to a version of a child project
150 150 Issue.find(4).update_attribute :fixed_version_id, 4
151 151
152 152 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
153 153 assert_equal false, @ecookbook.archive
154 154 end
155 155 @ecookbook.reload
156 156 assert @ecookbook.active?
157 157 end
158 158
159 159 def test_unarchive
160 160 user = @ecookbook.members.first.user
161 161 @ecookbook.archive
162 162 # A subproject of an archived project can not be unarchived
163 163 assert !@ecookbook_sub1.unarchive
164 164
165 165 # Unarchive project
166 166 assert @ecookbook.unarchive
167 167 @ecookbook.reload
168 168 assert @ecookbook.active?
169 169 assert !@ecookbook.archived?
170 170 assert user.projects.include?(@ecookbook)
171 171 # Subproject can now be unarchived
172 172 @ecookbook_sub1.reload
173 173 assert @ecookbook_sub1.unarchive
174 174 end
175 175
176 176 def test_destroy
177 177 # 2 active members
178 178 assert_equal 2, @ecookbook.members.size
179 179 # and 1 is locked
180 180 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
181 181 # some boards
182 182 assert @ecookbook.boards.any?
183 183
184 184 @ecookbook.destroy
185 185 # make sure that the project non longer exists
186 186 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
187 187 # make sure related data was removed
188 188 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
189 189 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
190 190 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
191 191 end
192 192
193 def test_destroy_should_destroy_subtasks
194 issues = (0..2).to_a.map {Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test')}
195 issues[0].update_attribute :parent_issue_id, issues[1].id
196 issues[2].update_attribute :parent_issue_id, issues[1].id
197 assert_equal 2, issues[1].children.count
198
199 assert_nothing_raised do
200 Project.find(1).destroy
201 end
202 assert Issue.find_all_by_id(issues.map(&:id)).empty?
203 end
204
193 205 def test_destroying_root_projects_should_clear_data
194 206 Project.roots.each do |root|
195 207 root.destroy
196 208 end
197 209
198 210 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
199 211 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
200 212 assert_equal 0, MemberRole.count
201 213 assert_equal 0, Issue.count
202 214 assert_equal 0, Journal.count
203 215 assert_equal 0, JournalDetail.count
204 216 assert_equal 0, Attachment.count
205 217 assert_equal 0, EnabledModule.count
206 218 assert_equal 0, IssueCategory.count
207 219 assert_equal 0, IssueRelation.count
208 220 assert_equal 0, Board.count
209 221 assert_equal 0, Message.count
210 222 assert_equal 0, News.count
211 223 assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL")
212 224 assert_equal 0, Repository.count
213 225 assert_equal 0, Changeset.count
214 226 assert_equal 0, Change.count
215 227 assert_equal 0, Comment.count
216 228 assert_equal 0, TimeEntry.count
217 229 assert_equal 0, Version.count
218 230 assert_equal 0, Watcher.count
219 231 assert_equal 0, Wiki.count
220 232 assert_equal 0, WikiPage.count
221 233 assert_equal 0, WikiContent.count
222 234 assert_equal 0, WikiContent::Version.count
223 235 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
224 236 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
225 237 assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
226 238 end
227 239
228 240 def test_move_an_orphan_project_to_a_root_project
229 241 sub = Project.find(2)
230 242 sub.set_parent! @ecookbook
231 243 assert_equal @ecookbook.id, sub.parent.id
232 244 @ecookbook.reload
233 245 assert_equal 4, @ecookbook.children.size
234 246 end
235 247
236 248 def test_move_an_orphan_project_to_a_subproject
237 249 sub = Project.find(2)
238 250 assert sub.set_parent!(@ecookbook_sub1)
239 251 end
240 252
241 253 def test_move_a_root_project_to_a_project
242 254 sub = @ecookbook
243 255 assert sub.set_parent!(Project.find(2))
244 256 end
245 257
246 258 def test_should_not_move_a_project_to_its_children
247 259 sub = @ecookbook
248 260 assert !(sub.set_parent!(Project.find(3)))
249 261 end
250 262
251 263 def test_set_parent_should_add_roots_in_alphabetical_order
252 264 ProjectCustomField.delete_all
253 265 Project.delete_all
254 266 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
255 267 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
256 268 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
257 269 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
258 270
259 271 assert_equal 4, Project.count
260 272 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
261 273 end
262 274
263 275 def test_set_parent_should_add_children_in_alphabetical_order
264 276 ProjectCustomField.delete_all
265 277 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
266 278 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
267 279 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
268 280 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
269 281 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
270 282
271 283 parent.reload
272 284 assert_equal 4, parent.children.size
273 285 assert_equal parent.children.all.sort_by(&:name), parent.children.all
274 286 end
275 287
276 288 def test_rebuild_should_sort_children_alphabetically
277 289 ProjectCustomField.delete_all
278 290 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
279 291 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
280 292 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
281 293 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
282 294 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
283 295
284 296 Project.update_all("lft = NULL, rgt = NULL")
285 297 Project.rebuild!
286 298
287 299 parent.reload
288 300 assert_equal 4, parent.children.size
289 301 assert_equal parent.children.all.sort_by(&:name), parent.children.all
290 302 end
291 303
292 304
293 305 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
294 306 # Parent issue with a hierarchy project's fixed version
295 307 parent_issue = Issue.find(1)
296 308 parent_issue.update_attribute(:fixed_version_id, 4)
297 309 parent_issue.reload
298 310 assert_equal 4, parent_issue.fixed_version_id
299 311
300 312 # Should keep fixed versions for the issues
301 313 issue_with_local_fixed_version = Issue.find(5)
302 314 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
303 315 issue_with_local_fixed_version.reload
304 316 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
305 317
306 318 # Local issue with hierarchy fixed_version
307 319 issue_with_hierarchy_fixed_version = Issue.find(13)
308 320 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
309 321 issue_with_hierarchy_fixed_version.reload
310 322 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
311 323
312 324 # Move project out of the issue's hierarchy
313 325 moved_project = Project.find(3)
314 326 moved_project.set_parent!(Project.find(2))
315 327 parent_issue.reload
316 328 issue_with_local_fixed_version.reload
317 329 issue_with_hierarchy_fixed_version.reload
318 330
319 331 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
320 332 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
321 333 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
322 334 end
323 335
324 336 def test_parent
325 337 p = Project.find(6).parent
326 338 assert p.is_a?(Project)
327 339 assert_equal 5, p.id
328 340 end
329 341
330 342 def test_ancestors
331 343 a = Project.find(6).ancestors
332 344 assert a.first.is_a?(Project)
333 345 assert_equal [1, 5], a.collect(&:id)
334 346 end
335 347
336 348 def test_root
337 349 r = Project.find(6).root
338 350 assert r.is_a?(Project)
339 351 assert_equal 1, r.id
340 352 end
341 353
342 354 def test_children
343 355 c = Project.find(1).children
344 356 assert c.first.is_a?(Project)
345 357 assert_equal [5, 3, 4], c.collect(&:id)
346 358 end
347 359
348 360 def test_descendants
349 361 d = Project.find(1).descendants
350 362 assert d.first.is_a?(Project)
351 363 assert_equal [5, 6, 3, 4], d.collect(&:id)
352 364 end
353 365
354 366 def test_allowed_parents_should_be_empty_for_non_member_user
355 367 Role.non_member.add_permission!(:add_project)
356 368 user = User.find(9)
357 369 assert user.memberships.empty?
358 370 User.current = user
359 371 assert Project.new.allowed_parents.compact.empty?
360 372 end
361 373
362 374 def test_allowed_parents_with_add_subprojects_permission
363 375 Role.find(1).remove_permission!(:add_project)
364 376 Role.find(1).add_permission!(:add_subprojects)
365 377 User.current = User.find(2)
366 378 # new project
367 379 assert !Project.new.allowed_parents.include?(nil)
368 380 assert Project.new.allowed_parents.include?(Project.find(1))
369 381 # existing root project
370 382 assert Project.find(1).allowed_parents.include?(nil)
371 383 # existing child
372 384 assert Project.find(3).allowed_parents.include?(Project.find(1))
373 385 assert !Project.find(3).allowed_parents.include?(nil)
374 386 end
375 387
376 388 def test_allowed_parents_with_add_project_permission
377 389 Role.find(1).add_permission!(:add_project)
378 390 Role.find(1).remove_permission!(:add_subprojects)
379 391 User.current = User.find(2)
380 392 # new project
381 393 assert Project.new.allowed_parents.include?(nil)
382 394 assert !Project.new.allowed_parents.include?(Project.find(1))
383 395 # existing root project
384 396 assert Project.find(1).allowed_parents.include?(nil)
385 397 # existing child
386 398 assert Project.find(3).allowed_parents.include?(Project.find(1))
387 399 assert Project.find(3).allowed_parents.include?(nil)
388 400 end
389 401
390 402 def test_allowed_parents_with_add_project_and_subprojects_permission
391 403 Role.find(1).add_permission!(:add_project)
392 404 Role.find(1).add_permission!(:add_subprojects)
393 405 User.current = User.find(2)
394 406 # new project
395 407 assert Project.new.allowed_parents.include?(nil)
396 408 assert Project.new.allowed_parents.include?(Project.find(1))
397 409 # existing root project
398 410 assert Project.find(1).allowed_parents.include?(nil)
399 411 # existing child
400 412 assert Project.find(3).allowed_parents.include?(Project.find(1))
401 413 assert Project.find(3).allowed_parents.include?(nil)
402 414 end
403 415
404 416 def test_users_by_role
405 417 users_by_role = Project.find(1).users_by_role
406 418 assert_kind_of Hash, users_by_role
407 419 role = Role.find(1)
408 420 assert_kind_of Array, users_by_role[role]
409 421 assert users_by_role[role].include?(User.find(2))
410 422 end
411 423
412 424 def test_rolled_up_trackers
413 425 parent = Project.find(1)
414 426 parent.trackers = Tracker.find([1,2])
415 427 child = parent.children.find(3)
416 428
417 429 assert_equal [1, 2], parent.tracker_ids
418 430 assert_equal [2, 3], child.trackers.collect(&:id)
419 431
420 432 assert_kind_of Tracker, parent.rolled_up_trackers.first
421 433 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
422 434
423 435 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
424 436 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
425 437 end
426 438
427 439 def test_rolled_up_trackers_should_ignore_archived_subprojects
428 440 parent = Project.find(1)
429 441 parent.trackers = Tracker.find([1,2])
430 442 child = parent.children.find(3)
431 443 child.trackers = Tracker.find([1,3])
432 444 parent.children.each(&:archive)
433 445
434 446 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
435 447 end
436 448
437 449 context "#rolled_up_versions" do
438 450 setup do
439 451 @project = Project.generate!
440 452 @parent_version_1 = Version.generate!(:project => @project)
441 453 @parent_version_2 = Version.generate!(:project => @project)
442 454 end
443 455
444 456 should "include the versions for the current project" do
445 457 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
446 458 end
447 459
448 460 should "include versions for a subproject" do
449 461 @subproject = Project.generate!
450 462 @subproject.set_parent!(@project)
451 463 @subproject_version = Version.generate!(:project => @subproject)
452 464
453 465 assert_same_elements [
454 466 @parent_version_1,
455 467 @parent_version_2,
456 468 @subproject_version
457 469 ], @project.rolled_up_versions
458 470 end
459 471
460 472 should "include versions for a sub-subproject" do
461 473 @subproject = Project.generate!
462 474 @subproject.set_parent!(@project)
463 475 @sub_subproject = Project.generate!
464 476 @sub_subproject.set_parent!(@subproject)
465 477 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
466 478
467 479 @project.reload
468 480
469 481 assert_same_elements [
470 482 @parent_version_1,
471 483 @parent_version_2,
472 484 @sub_subproject_version
473 485 ], @project.rolled_up_versions
474 486 end
475 487
476 488 should "only check active projects" do
477 489 @subproject = Project.generate!
478 490 @subproject.set_parent!(@project)
479 491 @subproject_version = Version.generate!(:project => @subproject)
480 492 assert @subproject.archive
481 493
482 494 @project.reload
483 495
484 496 assert !@subproject.active?
485 497 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
486 498 end
487 499 end
488 500
489 501 def test_shared_versions_none_sharing
490 502 p = Project.find(5)
491 503 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
492 504 assert p.shared_versions.include?(v)
493 505 assert !p.children.first.shared_versions.include?(v)
494 506 assert !p.root.shared_versions.include?(v)
495 507 assert !p.siblings.first.shared_versions.include?(v)
496 508 assert !p.root.siblings.first.shared_versions.include?(v)
497 509 end
498 510
499 511 def test_shared_versions_descendants_sharing
500 512 p = Project.find(5)
501 513 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
502 514 assert p.shared_versions.include?(v)
503 515 assert p.children.first.shared_versions.include?(v)
504 516 assert !p.root.shared_versions.include?(v)
505 517 assert !p.siblings.first.shared_versions.include?(v)
506 518 assert !p.root.siblings.first.shared_versions.include?(v)
507 519 end
508 520
509 521 def test_shared_versions_hierarchy_sharing
510 522 p = Project.find(5)
511 523 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
512 524 assert p.shared_versions.include?(v)
513 525 assert p.children.first.shared_versions.include?(v)
514 526 assert p.root.shared_versions.include?(v)
515 527 assert !p.siblings.first.shared_versions.include?(v)
516 528 assert !p.root.siblings.first.shared_versions.include?(v)
517 529 end
518 530
519 531 def test_shared_versions_tree_sharing
520 532 p = Project.find(5)
521 533 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
522 534 assert p.shared_versions.include?(v)
523 535 assert p.children.first.shared_versions.include?(v)
524 536 assert p.root.shared_versions.include?(v)
525 537 assert p.siblings.first.shared_versions.include?(v)
526 538 assert !p.root.siblings.first.shared_versions.include?(v)
527 539 end
528 540
529 541 def test_shared_versions_system_sharing
530 542 p = Project.find(5)
531 543 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
532 544 assert p.shared_versions.include?(v)
533 545 assert p.children.first.shared_versions.include?(v)
534 546 assert p.root.shared_versions.include?(v)
535 547 assert p.siblings.first.shared_versions.include?(v)
536 548 assert p.root.siblings.first.shared_versions.include?(v)
537 549 end
538 550
539 551 def test_shared_versions
540 552 parent = Project.find(1)
541 553 child = parent.children.find(3)
542 554 private_child = parent.children.find(5)
543 555
544 556 assert_equal [1,2,3], parent.version_ids.sort
545 557 assert_equal [4], child.version_ids
546 558 assert_equal [6], private_child.version_ids
547 559 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
548 560
549 561 assert_equal 6, parent.shared_versions.size
550 562 parent.shared_versions.each do |version|
551 563 assert_kind_of Version, version
552 564 end
553 565
554 566 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
555 567 end
556 568
557 569 def test_shared_versions_should_ignore_archived_subprojects
558 570 parent = Project.find(1)
559 571 child = parent.children.find(3)
560 572 child.archive
561 573 parent.reload
562 574
563 575 assert_equal [1,2,3], parent.version_ids.sort
564 576 assert_equal [4], child.version_ids
565 577 assert !parent.shared_versions.collect(&:id).include?(4)
566 578 end
567 579
568 580 def test_shared_versions_visible_to_user
569 581 user = User.find(3)
570 582 parent = Project.find(1)
571 583 child = parent.children.find(5)
572 584
573 585 assert_equal [1,2,3], parent.version_ids.sort
574 586 assert_equal [6], child.version_ids
575 587
576 588 versions = parent.shared_versions.visible(user)
577 589
578 590 assert_equal 4, versions.size
579 591 versions.each do |version|
580 592 assert_kind_of Version, version
581 593 end
582 594
583 595 assert !versions.collect(&:id).include?(6)
584 596 end
585 597
586 598 def test_shared_versions_for_new_project_should_include_system_shared_versions
587 599 p = Project.find(5)
588 600 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
589 601
590 602 assert_include v, Project.new.shared_versions
591 603 end
592 604
593 605 def test_next_identifier
594 606 ProjectCustomField.delete_all
595 607 Project.create!(:name => 'last', :identifier => 'p2008040')
596 608 assert_equal 'p2008041', Project.next_identifier
597 609 end
598 610
599 611 def test_next_identifier_first_project
600 612 Project.delete_all
601 613 assert_nil Project.next_identifier
602 614 end
603 615
604 616 def test_enabled_module_names
605 617 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
606 618 project = Project.new
607 619
608 620 project.enabled_module_names = %w(issue_tracking news)
609 621 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
610 622 end
611 623 end
612 624
613 625 context "enabled_modules" do
614 626 setup do
615 627 @project = Project.find(1)
616 628 end
617 629
618 630 should "define module by names and preserve ids" do
619 631 # Remove one module
620 632 modules = @project.enabled_modules.slice(0..-2)
621 633 assert modules.any?
622 634 assert_difference 'EnabledModule.count', -1 do
623 635 @project.enabled_module_names = modules.collect(&:name)
624 636 end
625 637 @project.reload
626 638 # Ids should be preserved
627 639 assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort
628 640 end
629 641
630 642 should "enable a module" do
631 643 @project.enabled_module_names = []
632 644 @project.reload
633 645 assert_equal [], @project.enabled_module_names
634 646 #with string
635 647 @project.enable_module!("issue_tracking")
636 648 assert_equal ["issue_tracking"], @project.enabled_module_names
637 649 #with symbol
638 650 @project.enable_module!(:gantt)
639 651 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
640 652 #don't add a module twice
641 653 @project.enable_module!("issue_tracking")
642 654 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
643 655 end
644 656
645 657 should "disable a module" do
646 658 #with string
647 659 assert @project.enabled_module_names.include?("issue_tracking")
648 660 @project.disable_module!("issue_tracking")
649 661 assert ! @project.reload.enabled_module_names.include?("issue_tracking")
650 662 #with symbol
651 663 assert @project.enabled_module_names.include?("gantt")
652 664 @project.disable_module!(:gantt)
653 665 assert ! @project.reload.enabled_module_names.include?("gantt")
654 666 #with EnabledModule object
655 667 first_module = @project.enabled_modules.first
656 668 @project.disable_module!(first_module)
657 669 assert ! @project.reload.enabled_module_names.include?(first_module.name)
658 670 end
659 671 end
660 672
661 673 def test_enabled_module_names_should_not_recreate_enabled_modules
662 674 project = Project.find(1)
663 675 # Remove one module
664 676 modules = project.enabled_modules.slice(0..-2)
665 677 assert modules.any?
666 678 assert_difference 'EnabledModule.count', -1 do
667 679 project.enabled_module_names = modules.collect(&:name)
668 680 end
669 681 project.reload
670 682 # Ids should be preserved
671 683 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
672 684 end
673 685
674 686 def test_copy_from_existing_project
675 687 source_project = Project.find(1)
676 688 copied_project = Project.copy_from(1)
677 689
678 690 assert copied_project
679 691 # Cleared attributes
680 692 assert copied_project.id.blank?
681 693 assert copied_project.name.blank?
682 694 assert copied_project.identifier.blank?
683 695
684 696 # Duplicated attributes
685 697 assert_equal source_project.description, copied_project.description
686 698 assert_equal source_project.enabled_modules, copied_project.enabled_modules
687 699 assert_equal source_project.trackers, copied_project.trackers
688 700
689 701 # Default attributes
690 702 assert_equal 1, copied_project.status
691 703 end
692 704
693 705 def test_activities_should_use_the_system_activities
694 706 project = Project.find(1)
695 707 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
696 708 end
697 709
698 710
699 711 def test_activities_should_use_the_project_specific_activities
700 712 project = Project.find(1)
701 713 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
702 714 assert overridden_activity.save!
703 715
704 716 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
705 717 end
706 718
707 719 def test_activities_should_not_include_the_inactive_project_specific_activities
708 720 project = Project.find(1)
709 721 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
710 722 assert overridden_activity.save!
711 723
712 724 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
713 725 end
714 726
715 727 def test_activities_should_not_include_project_specific_activities_from_other_projects
716 728 project = Project.find(1)
717 729 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
718 730 assert overridden_activity.save!
719 731
720 732 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
721 733 end
722 734
723 735 def test_activities_should_handle_nils
724 736 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
725 737 TimeEntryActivity.delete_all
726 738
727 739 # No activities
728 740 project = Project.find(1)
729 741 assert project.activities.empty?
730 742
731 743 # No system, one overridden
732 744 assert overridden_activity.save!
733 745 project.reload
734 746 assert_equal [overridden_activity], project.activities
735 747 end
736 748
737 749 def test_activities_should_override_system_activities_with_project_activities
738 750 project = Project.find(1)
739 751 parent_activity = TimeEntryActivity.find(:first)
740 752 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
741 753 assert overridden_activity.save!
742 754
743 755 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
744 756 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
745 757 end
746 758
747 759 def test_activities_should_include_inactive_activities_if_specified
748 760 project = Project.find(1)
749 761 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
750 762 assert overridden_activity.save!
751 763
752 764 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
753 765 end
754 766
755 767 test 'activities should not include active System activities if the project has an override that is inactive' do
756 768 project = Project.find(1)
757 769 system_activity = TimeEntryActivity.find_by_name('Design')
758 770 assert system_activity.active?
759 771 overridden_activity = TimeEntryActivity.create!(:name => "Project", :project => project, :parent => system_activity, :active => false)
760 772 assert overridden_activity.save!
761 773
762 774 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
763 775 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
764 776 end
765 777
766 778 def test_close_completed_versions
767 779 Version.update_all("status = 'open'")
768 780 project = Project.find(1)
769 781 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
770 782 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
771 783 project.close_completed_versions
772 784 project.reload
773 785 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
774 786 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
775 787 end
776 788
777 789 context "Project#copy" do
778 790 setup do
779 791 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
780 792 Project.destroy_all :identifier => "copy-test"
781 793 @source_project = Project.find(2)
782 794 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
783 795 @project.trackers = @source_project.trackers
784 796 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
785 797 end
786 798
787 799 should "copy issues" do
788 800 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
789 801 :subject => "copy issue status",
790 802 :tracker_id => 1,
791 803 :assigned_to_id => 2,
792 804 :project_id => @source_project.id)
793 805 assert @project.valid?
794 806 assert @project.issues.empty?
795 807 assert @project.copy(@source_project)
796 808
797 809 assert_equal @source_project.issues.size, @project.issues.size
798 810 @project.issues.each do |issue|
799 811 assert issue.valid?
800 812 assert ! issue.assigned_to.blank?
801 813 assert_equal @project, issue.project
802 814 end
803 815
804 816 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
805 817 assert copied_issue
806 818 assert copied_issue.status
807 819 assert_equal "Closed", copied_issue.status.name
808 820 end
809 821
810 822 should "change the new issues to use the copied version" do
811 823 User.current = User.find(1)
812 824 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
813 825 @source_project.versions << assigned_version
814 826 assert_equal 3, @source_project.versions.size
815 827 Issue.generate_for_project!(@source_project,
816 828 :fixed_version_id => assigned_version.id,
817 829 :subject => "change the new issues to use the copied version",
818 830 :tracker_id => 1,
819 831 :project_id => @source_project.id)
820 832
821 833 assert @project.copy(@source_project)
822 834 @project.reload
823 835 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
824 836
825 837 assert copied_issue
826 838 assert copied_issue.fixed_version
827 839 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
828 840 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
829 841 end
830 842
831 843 should "copy issue relations" do
832 844 Setting.cross_project_issue_relations = '1'
833 845
834 846 second_issue = Issue.generate!(:status_id => 5,
835 847 :subject => "copy issue relation",
836 848 :tracker_id => 1,
837 849 :assigned_to_id => 2,
838 850 :project_id => @source_project.id)
839 851 source_relation = IssueRelation.create!(:issue_from => Issue.find(4),
840 852 :issue_to => second_issue,
841 853 :relation_type => "relates")
842 854 source_relation_cross_project = IssueRelation.create!(:issue_from => Issue.find(1),
843 855 :issue_to => second_issue,
844 856 :relation_type => "duplicates")
845 857
846 858 assert @project.copy(@source_project)
847 859 assert_equal @source_project.issues.count, @project.issues.count
848 860 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
849 861 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
850 862
851 863 # First issue with a relation on project
852 864 assert_equal 1, copied_issue.relations.size, "Relation not copied"
853 865 copied_relation = copied_issue.relations.first
854 866 assert_equal "relates", copied_relation.relation_type
855 867 assert_equal copied_second_issue.id, copied_relation.issue_to_id
856 868 assert_not_equal source_relation.id, copied_relation.id
857 869
858 870 # Second issue with a cross project relation
859 871 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
860 872 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
861 873 assert_equal "duplicates", copied_relation.relation_type
862 874 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
863 875 assert_not_equal source_relation_cross_project.id, copied_relation.id
864 876 end
865 877
866 878 should "copy issue attachments" do
867 879 issue = Issue.generate!(:subject => "copy with attachment", :tracker_id => 1, :project_id => @source_project.id)
868 880 Attachment.create!(:container => issue, :file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 1)
869 881 @source_project.issues << issue
870 882 assert @project.copy(@source_project)
871 883
872 884 copied_issue = @project.issues.first(:conditions => {:subject => "copy with attachment"})
873 885 assert_not_nil copied_issue
874 886 assert_equal 1, copied_issue.attachments.count, "Attachment not copied"
875 887 assert_equal "testfile.txt", copied_issue.attachments.first.filename
876 888 end
877 889
878 890 should "copy memberships" do
879 891 assert @project.valid?
880 892 assert @project.members.empty?
881 893 assert @project.copy(@source_project)
882 894
883 895 assert_equal @source_project.memberships.size, @project.memberships.size
884 896 @project.memberships.each do |membership|
885 897 assert membership
886 898 assert_equal @project, membership.project
887 899 end
888 900 end
889 901
890 902 should "copy memberships with groups and additional roles" do
891 903 group = Group.create!(:lastname => "Copy group")
892 904 user = User.find(7)
893 905 group.users << user
894 906 # group role
895 907 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
896 908 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
897 909 # additional role
898 910 member.role_ids = [1]
899 911
900 912 assert @project.copy(@source_project)
901 913 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
902 914 assert_not_nil member
903 915 assert_equal [1, 2], member.role_ids.sort
904 916 end
905 917
906 918 should "copy project specific queries" do
907 919 assert @project.valid?
908 920 assert @project.queries.empty?
909 921 assert @project.copy(@source_project)
910 922
911 923 assert_equal @source_project.queries.size, @project.queries.size
912 924 @project.queries.each do |query|
913 925 assert query
914 926 assert_equal @project, query.project
915 927 end
916 928 assert_equal @source_project.queries.map(&:user_id).sort, @project.queries.map(&:user_id).sort
917 929 end
918 930
919 931 should "copy versions" do
920 932 @source_project.versions << Version.generate!
921 933 @source_project.versions << Version.generate!
922 934
923 935 assert @project.versions.empty?
924 936 assert @project.copy(@source_project)
925 937
926 938 assert_equal @source_project.versions.size, @project.versions.size
927 939 @project.versions.each do |version|
928 940 assert version
929 941 assert_equal @project, version.project
930 942 end
931 943 end
932 944
933 945 should "copy wiki" do
934 946 assert_difference 'Wiki.count' do
935 947 assert @project.copy(@source_project)
936 948 end
937 949
938 950 assert @project.wiki
939 951 assert_not_equal @source_project.wiki, @project.wiki
940 952 assert_equal "Start page", @project.wiki.start_page
941 953 end
942 954
943 955 should "copy wiki pages and content with hierarchy" do
944 956 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
945 957 assert @project.copy(@source_project)
946 958 end
947 959
948 960 assert @project.wiki
949 961 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
950 962
951 963 @project.wiki.pages.each do |wiki_page|
952 964 assert wiki_page.content
953 965 assert !@source_project.wiki.pages.include?(wiki_page)
954 966 end
955 967
956 968 parent = @project.wiki.find_page('Parent_page')
957 969 child1 = @project.wiki.find_page('Child_page_1')
958 970 child2 = @project.wiki.find_page('Child_page_2')
959 971 assert_equal parent, child1.parent
960 972 assert_equal parent, child2.parent
961 973 end
962 974
963 975 should "copy issue categories" do
964 976 assert @project.copy(@source_project)
965 977
966 978 assert_equal 2, @project.issue_categories.size
967 979 @project.issue_categories.each do |issue_category|
968 980 assert !@source_project.issue_categories.include?(issue_category)
969 981 end
970 982 end
971 983
972 984 should "copy boards" do
973 985 assert @project.copy(@source_project)
974 986
975 987 assert_equal 1, @project.boards.size
976 988 @project.boards.each do |board|
977 989 assert !@source_project.boards.include?(board)
978 990 end
979 991 end
980 992
981 993 should "change the new issues to use the copied issue categories" do
982 994 issue = Issue.find(4)
983 995 issue.update_attribute(:category_id, 3)
984 996
985 997 assert @project.copy(@source_project)
986 998
987 999 @project.issues.each do |issue|
988 1000 assert issue.category
989 1001 assert_equal "Stock management", issue.category.name # Same name
990 1002 assert_not_equal IssueCategory.find(3), issue.category # Different record
991 1003 end
992 1004 end
993 1005
994 1006 should "limit copy with :only option" do
995 1007 assert @project.members.empty?
996 1008 assert @project.issue_categories.empty?
997 1009 assert @source_project.issues.any?
998 1010
999 1011 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
1000 1012
1001 1013 assert @project.members.any?
1002 1014 assert @project.issue_categories.any?
1003 1015 assert @project.issues.empty?
1004 1016 end
1005 1017
1006 1018 end
1007 1019
1008 1020 context "#start_date" do
1009 1021 setup do
1010 1022 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1011 1023 @project = Project.generate!(:identifier => 'test0')
1012 1024 @project.trackers << Tracker.generate!
1013 1025 end
1014 1026
1015 1027 should "be nil if there are no issues on the project" do
1016 1028 assert_nil @project.start_date
1017 1029 end
1018 1030
1019 1031 should "be tested when issues have no start date"
1020 1032
1021 1033 should "be the earliest start date of it's issues" do
1022 1034 early = 7.days.ago.to_date
1023 1035 Issue.generate_for_project!(@project, :start_date => Date.today)
1024 1036 Issue.generate_for_project!(@project, :start_date => early)
1025 1037
1026 1038 assert_equal early, @project.start_date
1027 1039 end
1028 1040
1029 1041 end
1030 1042
1031 1043 context "#due_date" do
1032 1044 setup do
1033 1045 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1034 1046 @project = Project.generate!(:identifier => 'test0')
1035 1047 @project.trackers << Tracker.generate!
1036 1048 end
1037 1049
1038 1050 should "be nil if there are no issues on the project" do
1039 1051 assert_nil @project.due_date
1040 1052 end
1041 1053
1042 1054 should "be tested when issues have no due date"
1043 1055
1044 1056 should "be the latest due date of it's issues" do
1045 1057 future = 7.days.from_now.to_date
1046 1058 Issue.generate_for_project!(@project, :due_date => future)
1047 1059 Issue.generate_for_project!(@project, :due_date => Date.today)
1048 1060
1049 1061 assert_equal future, @project.due_date
1050 1062 end
1051 1063
1052 1064 should "be the latest due date of it's versions" do
1053 1065 future = 7.days.from_now.to_date
1054 1066 @project.versions << Version.generate!(:effective_date => future)
1055 1067 @project.versions << Version.generate!(:effective_date => Date.today)
1056 1068
1057 1069
1058 1070 assert_equal future, @project.due_date
1059 1071
1060 1072 end
1061 1073
1062 1074 should "pick the latest date from it's issues and versions" do
1063 1075 future = 7.days.from_now.to_date
1064 1076 far_future = 14.days.from_now.to_date
1065 1077 Issue.generate_for_project!(@project, :due_date => far_future)
1066 1078 @project.versions << Version.generate!(:effective_date => future)
1067 1079
1068 1080 assert_equal far_future, @project.due_date
1069 1081 end
1070 1082
1071 1083 end
1072 1084
1073 1085 context "Project#completed_percent" do
1074 1086 setup do
1075 1087 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1076 1088 @project = Project.generate!(:identifier => 'test0')
1077 1089 @project.trackers << Tracker.generate!
1078 1090 end
1079 1091
1080 1092 context "no versions" do
1081 1093 should "be 100" do
1082 1094 assert_equal 100, @project.completed_percent
1083 1095 end
1084 1096 end
1085 1097
1086 1098 context "with versions" do
1087 1099 should "return 0 if the versions have no issues" do
1088 1100 Version.generate!(:project => @project)
1089 1101 Version.generate!(:project => @project)
1090 1102
1091 1103 assert_equal 0, @project.completed_percent
1092 1104 end
1093 1105
1094 1106 should "return 100 if the version has only closed issues" do
1095 1107 v1 = Version.generate!(:project => @project)
1096 1108 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
1097 1109 v2 = Version.generate!(:project => @project)
1098 1110 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
1099 1111
1100 1112 assert_equal 100, @project.completed_percent
1101 1113 end
1102 1114
1103 1115 should "return the averaged completed percent of the versions (not weighted)" do
1104 1116 v1 = Version.generate!(:project => @project)
1105 1117 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
1106 1118 v2 = Version.generate!(:project => @project)
1107 1119 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
1108 1120
1109 1121 assert_equal 50, @project.completed_percent
1110 1122 end
1111 1123
1112 1124 end
1113 1125 end
1114 1126
1115 1127 context "#notified_users" do
1116 1128 setup do
1117 1129 @project = Project.generate!
1118 1130 @role = Role.generate!
1119 1131
1120 1132 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1121 1133 Member.create!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1122 1134
1123 1135 @all_events_user = User.generate!(:mail_notification => 'all')
1124 1136 Member.create!(:project => @project, :roles => [@role], :principal => @all_events_user)
1125 1137
1126 1138 @no_events_user = User.generate!(:mail_notification => 'none')
1127 1139 Member.create!(:project => @project, :roles => [@role], :principal => @no_events_user)
1128 1140
1129 1141 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1130 1142 Member.create!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1131 1143
1132 1144 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1133 1145 Member.create!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1134 1146
1135 1147 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1136 1148 Member.create!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1137 1149 end
1138 1150
1139 1151 should "include members with a mail notification" do
1140 1152 assert @project.notified_users.include?(@user_with_membership_notification)
1141 1153 end
1142 1154
1143 1155 should "include users with the 'all' notification option" do
1144 1156 assert @project.notified_users.include?(@all_events_user)
1145 1157 end
1146 1158
1147 1159 should "not include users with the 'none' notification option" do
1148 1160 assert !@project.notified_users.include?(@no_events_user)
1149 1161 end
1150 1162
1151 1163 should "not include users with the 'only_my_events' notification option" do
1152 1164 assert !@project.notified_users.include?(@only_my_events_user)
1153 1165 end
1154 1166
1155 1167 should "not include users with the 'only_assigned' notification option" do
1156 1168 assert !@project.notified_users.include?(@only_assigned_user)
1157 1169 end
1158 1170
1159 1171 should "not include users with the 'only_owner' notification option" do
1160 1172 assert !@project.notified_users.include?(@only_owned_user)
1161 1173 end
1162 1174 end
1163 1175
1164 1176 end
General Comments 0
You need to be logged in to leave comments. Login now