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