##// END OF EJS Templates
Assignee is removed on issue update if assignee account is locked (#8884)....
Jean-Philippe Lang -
r6188:2ce3191639dc
parent child
Show More
@@ -1,943 +1,944
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_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
62 62 named_scope :visible, lambda {|*args| { :include => :project,
63 63 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
64 64
65 65 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
66 66
67 67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
68 68 named_scope :with_limit, lambda { |limit| { :limit => limit} }
69 69 named_scope :on_active_project, :include => [:status, :project, :tracker],
70 70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
71 71
72 72 named_scope :without_version, lambda {
73 73 {
74 74 :conditions => { :fixed_version_id => nil}
75 75 }
76 76 }
77 77
78 78 named_scope :with_query, lambda {|query|
79 79 {
80 80 :conditions => Query.merge_conditions(query.statement)
81 81 }
82 82 }
83 83
84 84 before_create :default_assign
85 85 before_save :close_duplicates, :update_done_ratio_from_issue_status
86 86 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
87 87 after_destroy :update_parent_attributes
88 88
89 89 # Returns a SQL conditions string used to find all issues visible by the specified user
90 90 def self.visible_condition(user, options={})
91 91 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
92 92 case role.issues_visibility
93 93 when 'all'
94 94 nil
95 95 when 'default'
96 96 user_ids = [user.id] + user.groups.map(&:id)
97 97 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids}))"
98 98 when 'own'
99 99 user_ids = [user.id] + user.groups.map(&:id)
100 100 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids}))"
101 101 else
102 102 '1=0'
103 103 end
104 104 end
105 105 end
106 106
107 107 # Returns true if usr or current user is allowed to view the issue
108 108 def visible?(usr=nil)
109 109 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
110 110 case role.issues_visibility
111 111 when 'all'
112 112 true
113 113 when 'default'
114 114 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
115 115 when 'own'
116 116 self.author == user || user.is_or_belongs_to?(assigned_to)
117 117 else
118 118 false
119 119 end
120 120 end
121 121 end
122 122
123 123 def after_initialize
124 124 if new_record?
125 125 # set default values for new records only
126 126 self.status ||= IssueStatus.default
127 127 self.priority ||= IssuePriority.default
128 128 end
129 129 end
130 130
131 131 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
132 132 def available_custom_fields
133 133 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
134 134 end
135 135
136 136 def copy_from(arg)
137 137 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
138 138 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
139 139 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
140 140 self.status = issue.status
141 141 self
142 142 end
143 143
144 144 # Moves/copies an issue to a new project and tracker
145 145 # Returns the moved/copied issue on success, false on failure
146 146 def move_to_project(*args)
147 147 ret = Issue.transaction do
148 148 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
149 149 end || false
150 150 end
151 151
152 152 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
153 153 options ||= {}
154 154 issue = options[:copy] ? self.class.new.copy_from(self) : self
155 155
156 156 if new_project && issue.project_id != new_project.id
157 157 # delete issue relations
158 158 unless Setting.cross_project_issue_relations?
159 159 issue.relations_from.clear
160 160 issue.relations_to.clear
161 161 end
162 162 # issue is moved to another project
163 163 # reassign to the category with same name if any
164 164 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
165 165 issue.category = new_category
166 166 # Keep the fixed_version if it's still valid in the new_project
167 167 unless new_project.shared_versions.include?(issue.fixed_version)
168 168 issue.fixed_version = nil
169 169 end
170 170 issue.project = new_project
171 171 if issue.parent && issue.parent.project_id != issue.project_id
172 172 issue.parent_issue_id = nil
173 173 end
174 174 end
175 175 if new_tracker
176 176 issue.tracker = new_tracker
177 177 issue.reset_custom_values!
178 178 end
179 179 if options[:copy]
180 180 issue.author = User.current
181 181 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
182 182 issue.status = if options[:attributes] && options[:attributes][:status_id]
183 183 IssueStatus.find_by_id(options[:attributes][:status_id])
184 184 else
185 185 self.status
186 186 end
187 187 end
188 188 # Allow bulk setting of attributes on the issue
189 189 if options[:attributes]
190 190 issue.attributes = options[:attributes]
191 191 end
192 192 if issue.save
193 193 if options[:copy]
194 194 if current_journal && current_journal.notes.present?
195 195 issue.init_journal(current_journal.user, current_journal.notes)
196 196 issue.current_journal.notify = false
197 197 issue.save
198 198 end
199 199 else
200 200 # Manually update project_id on related time entries
201 201 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
202 202
203 203 issue.children.each do |child|
204 204 unless child.move_to_project_without_transaction(new_project)
205 205 # Move failed and transaction was rollback'd
206 206 return false
207 207 end
208 208 end
209 209 end
210 210 else
211 211 return false
212 212 end
213 213 issue
214 214 end
215 215
216 216 def status_id=(sid)
217 217 self.status = nil
218 218 write_attribute(:status_id, sid)
219 219 end
220 220
221 221 def priority_id=(pid)
222 222 self.priority = nil
223 223 write_attribute(:priority_id, pid)
224 224 end
225 225
226 226 def tracker_id=(tid)
227 227 self.tracker = nil
228 228 result = write_attribute(:tracker_id, tid)
229 229 @custom_field_values = nil
230 230 result
231 231 end
232 232
233 233 def description=(arg)
234 234 if arg.is_a?(String)
235 235 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
236 236 end
237 237 write_attribute(:description, arg)
238 238 end
239 239
240 240 # Overrides attributes= so that tracker_id gets assigned first
241 241 def attributes_with_tracker_first=(new_attributes, *args)
242 242 return if new_attributes.nil?
243 243 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
244 244 if new_tracker_id
245 245 self.tracker_id = new_tracker_id
246 246 end
247 247 send :attributes_without_tracker_first=, new_attributes, *args
248 248 end
249 249 # Do not redefine alias chain on reload (see #4838)
250 250 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
251 251
252 252 def estimated_hours=(h)
253 253 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
254 254 end
255 255
256 256 safe_attributes 'tracker_id',
257 257 'status_id',
258 258 'parent_issue_id',
259 259 'category_id',
260 260 'assigned_to_id',
261 261 'priority_id',
262 262 'fixed_version_id',
263 263 'subject',
264 264 'description',
265 265 'start_date',
266 266 'due_date',
267 267 'done_ratio',
268 268 'estimated_hours',
269 269 'custom_field_values',
270 270 'custom_fields',
271 271 'lock_version',
272 272 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
273 273
274 274 safe_attributes 'status_id',
275 275 'assigned_to_id',
276 276 'fixed_version_id',
277 277 'done_ratio',
278 278 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
279 279
280 280 safe_attributes 'is_private',
281 281 :if => lambda {|issue, user|
282 282 user.allowed_to?(:set_issues_private, issue.project) ||
283 283 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
284 284 }
285 285
286 286 # Safely sets attributes
287 287 # Should be called from controllers instead of #attributes=
288 288 # attr_accessible is too rough because we still want things like
289 289 # Issue.new(:project => foo) to work
290 290 # TODO: move workflow/permission checks from controllers to here
291 291 def safe_attributes=(attrs, user=User.current)
292 292 return unless attrs.is_a?(Hash)
293 293
294 294 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
295 295 attrs = delete_unsafe_attributes(attrs, user)
296 296 return if attrs.empty?
297 297
298 298 # Tracker must be set before since new_statuses_allowed_to depends on it.
299 299 if t = attrs.delete('tracker_id')
300 300 self.tracker_id = t
301 301 end
302 302
303 303 if attrs['status_id']
304 304 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
305 305 attrs.delete('status_id')
306 306 end
307 307 end
308 308
309 309 unless leaf?
310 310 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
311 311 end
312 312
313 313 if attrs.has_key?('parent_issue_id')
314 314 if !user.allowed_to?(:manage_subtasks, project)
315 315 attrs.delete('parent_issue_id')
316 316 elsif !attrs['parent_issue_id'].blank?
317 317 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
318 318 end
319 319 end
320 320
321 321 self.attributes = attrs
322 322 end
323 323
324 324 def done_ratio
325 325 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
326 326 status.default_done_ratio
327 327 else
328 328 read_attribute(:done_ratio)
329 329 end
330 330 end
331 331
332 332 def self.use_status_for_done_ratio?
333 333 Setting.issue_done_ratio == 'issue_status'
334 334 end
335 335
336 336 def self.use_field_for_done_ratio?
337 337 Setting.issue_done_ratio == 'issue_field'
338 338 end
339 339
340 340 def validate
341 341 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
342 342 errors.add :due_date, :not_a_date
343 343 end
344 344
345 345 if self.due_date and self.start_date and self.due_date < self.start_date
346 346 errors.add :due_date, :greater_than_start_date
347 347 end
348 348
349 349 if start_date && soonest_start && start_date < soonest_start
350 350 errors.add :start_date, :invalid
351 351 end
352 352
353 353 if fixed_version
354 354 if !assignable_versions.include?(fixed_version)
355 355 errors.add :fixed_version_id, :inclusion
356 356 elsif reopened? && fixed_version.closed?
357 357 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
358 358 end
359 359 end
360 360
361 361 # Checks that the issue can not be added/moved to a disabled tracker
362 362 if project && (tracker_id_changed? || project_id_changed?)
363 363 unless project.trackers.include?(tracker)
364 364 errors.add :tracker_id, :inclusion
365 365 end
366 366 end
367 367
368 368 # Checks parent issue assignment
369 369 if @parent_issue
370 370 if @parent_issue.project_id != project_id
371 371 errors.add :parent_issue_id, :not_same_project
372 372 elsif !new_record?
373 373 # moving an existing issue
374 374 if @parent_issue.root_id != root_id
375 375 # we can always move to another tree
376 376 elsif move_possible?(@parent_issue)
377 377 # move accepted inside tree
378 378 else
379 379 errors.add :parent_issue_id, :not_a_valid_parent
380 380 end
381 381 end
382 382 end
383 383 end
384 384
385 385 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
386 386 # even if the user turns off the setting later
387 387 def update_done_ratio_from_issue_status
388 388 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
389 389 self.done_ratio = status.default_done_ratio
390 390 end
391 391 end
392 392
393 393 def init_journal(user, notes = "")
394 394 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
395 395 @issue_before_change = self.clone
396 396 @issue_before_change.status = self.status
397 397 @custom_values_before_change = {}
398 398 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
399 399 # Make sure updated_on is updated when adding a note.
400 400 updated_on_will_change!
401 401 @current_journal
402 402 end
403 403
404 404 # Return true if the issue is closed, otherwise false
405 405 def closed?
406 406 self.status.is_closed?
407 407 end
408 408
409 409 # Return true if the issue is being reopened
410 410 def reopened?
411 411 if !new_record? && status_id_changed?
412 412 status_was = IssueStatus.find_by_id(status_id_was)
413 413 status_new = IssueStatus.find_by_id(status_id)
414 414 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
415 415 return true
416 416 end
417 417 end
418 418 false
419 419 end
420 420
421 421 # Return true if the issue is being closed
422 422 def closing?
423 423 if !new_record? && status_id_changed?
424 424 status_was = IssueStatus.find_by_id(status_id_was)
425 425 status_new = IssueStatus.find_by_id(status_id)
426 426 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
427 427 return true
428 428 end
429 429 end
430 430 false
431 431 end
432 432
433 433 # Returns true if the issue is overdue
434 434 def overdue?
435 435 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
436 436 end
437 437
438 438 # Is the amount of work done less than it should for the due date
439 439 def behind_schedule?
440 440 return false if start_date.nil? || due_date.nil?
441 441 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
442 442 return done_date <= Date.today
443 443 end
444 444
445 445 # Does this issue have children?
446 446 def children?
447 447 !leaf?
448 448 end
449 449
450 450 # Users the issue can be assigned to
451 451 def assignable_users
452 452 users = project.assignable_users
453 453 users << author if author
454 users << assigned_to if assigned_to
454 455 users.uniq.sort
455 456 end
456 457
457 458 # Versions that the issue can be assigned to
458 459 def assignable_versions
459 460 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
460 461 end
461 462
462 463 # Returns true if this issue is blocked by another issue that is still open
463 464 def blocked?
464 465 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
465 466 end
466 467
467 468 # Returns an array of status that user is able to apply
468 469 def new_statuses_allowed_to(user, include_default=false)
469 470 statuses = status.find_new_statuses_allowed_to(
470 471 user.roles_for_project(project),
471 472 tracker,
472 473 author == user,
473 474 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
474 475 )
475 476 statuses << status unless statuses.empty?
476 477 statuses << IssueStatus.default if include_default
477 478 statuses = statuses.uniq.sort
478 479 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
479 480 end
480 481
481 482 # Returns the mail adresses of users that should be notified
482 483 def recipients
483 484 notified = project.notified_users
484 485 # Author and assignee are always notified unless they have been
485 486 # locked or don't want to be notified
486 487 notified << author if author && author.active? && author.notify_about?(self)
487 488 if assigned_to
488 489 if assigned_to.is_a?(Group)
489 490 notified += assigned_to.users.select {|u| u.active? && u.notify_about?(self)}
490 491 else
491 492 notified << assigned_to if assigned_to.active? && assigned_to.notify_about?(self)
492 493 end
493 494 end
494 495 notified.uniq!
495 496 # Remove users that can not view the issue
496 497 notified.reject! {|user| !visible?(user)}
497 498 notified.collect(&:mail)
498 499 end
499 500
500 501 # Returns the total number of hours spent on this issue and its descendants
501 502 #
502 503 # Example:
503 504 # spent_hours => 0.0
504 505 # spent_hours => 50.2
505 506 def spent_hours
506 507 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
507 508 end
508 509
509 510 def relations
510 511 (relations_from + relations_to).sort
511 512 end
512 513
513 514 # Finds an issue relation given its id.
514 515 def find_relation(relation_id)
515 516 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
516 517 end
517 518
518 519 def all_dependent_issues(except=[])
519 520 except << self
520 521 dependencies = []
521 522 relations_from.each do |relation|
522 523 if relation.issue_to && !except.include?(relation.issue_to)
523 524 dependencies << relation.issue_to
524 525 dependencies += relation.issue_to.all_dependent_issues(except)
525 526 end
526 527 end
527 528 dependencies
528 529 end
529 530
530 531 # Returns an array of issues that duplicate this one
531 532 def duplicates
532 533 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
533 534 end
534 535
535 536 # Returns the due date or the target due date if any
536 537 # Used on gantt chart
537 538 def due_before
538 539 due_date || (fixed_version ? fixed_version.effective_date : nil)
539 540 end
540 541
541 542 # Returns the time scheduled for this issue.
542 543 #
543 544 # Example:
544 545 # Start Date: 2/26/09, End Date: 3/04/09
545 546 # duration => 6
546 547 def duration
547 548 (start_date && due_date) ? due_date - start_date : 0
548 549 end
549 550
550 551 def soonest_start
551 552 @soonest_start ||= (
552 553 relations_to.collect{|relation| relation.successor_soonest_start} +
553 554 ancestors.collect(&:soonest_start)
554 555 ).compact.max
555 556 end
556 557
557 558 def reschedule_after(date)
558 559 return if date.nil?
559 560 if leaf?
560 561 if start_date.nil? || start_date < date
561 562 self.start_date, self.due_date = date, date + duration
562 563 save
563 564 end
564 565 else
565 566 leaves.each do |leaf|
566 567 leaf.reschedule_after(date)
567 568 end
568 569 end
569 570 end
570 571
571 572 def <=>(issue)
572 573 if issue.nil?
573 574 -1
574 575 elsif root_id != issue.root_id
575 576 (root_id || 0) <=> (issue.root_id || 0)
576 577 else
577 578 (lft || 0) <=> (issue.lft || 0)
578 579 end
579 580 end
580 581
581 582 def to_s
582 583 "#{tracker} ##{id}: #{subject}"
583 584 end
584 585
585 586 # Returns a string of css classes that apply to the issue
586 587 def css_classes
587 588 s = "issue status-#{status.position} priority-#{priority.position}"
588 589 s << ' closed' if closed?
589 590 s << ' overdue' if overdue?
590 591 s << ' child' if child?
591 592 s << ' parent' unless leaf?
592 593 s << ' private' if is_private?
593 594 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
594 595 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
595 596 s
596 597 end
597 598
598 599 # Saves an issue, time_entry, attachments, and a journal from the parameters
599 600 # Returns false if save fails
600 601 def save_issue_with_child_records(params, existing_time_entry=nil)
601 602 Issue.transaction do
602 603 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
603 604 @time_entry = existing_time_entry || TimeEntry.new
604 605 @time_entry.project = project
605 606 @time_entry.issue = self
606 607 @time_entry.user = User.current
607 608 @time_entry.spent_on = Date.today
608 609 @time_entry.attributes = params[:time_entry]
609 610 self.time_entries << @time_entry
610 611 end
611 612
612 613 if valid?
613 614 attachments = Attachment.attach_files(self, params[:attachments])
614 615
615 616 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
616 617 # TODO: Rename hook
617 618 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
618 619 begin
619 620 if save
620 621 # TODO: Rename hook
621 622 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
622 623 else
623 624 raise ActiveRecord::Rollback
624 625 end
625 626 rescue ActiveRecord::StaleObjectError
626 627 attachments[:files].each(&:destroy)
627 628 errors.add_to_base l(:notice_locking_conflict)
628 629 raise ActiveRecord::Rollback
629 630 end
630 631 end
631 632 end
632 633 end
633 634
634 635 # Unassigns issues from +version+ if it's no longer shared with issue's project
635 636 def self.update_versions_from_sharing_change(version)
636 637 # Update issues assigned to the version
637 638 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
638 639 end
639 640
640 641 # Unassigns issues from versions that are no longer shared
641 642 # after +project+ was moved
642 643 def self.update_versions_from_hierarchy_change(project)
643 644 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
644 645 # Update issues of the moved projects and issues assigned to a version of a moved project
645 646 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
646 647 end
647 648
648 649 def parent_issue_id=(arg)
649 650 parent_issue_id = arg.blank? ? nil : arg.to_i
650 651 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
651 652 @parent_issue.id
652 653 else
653 654 @parent_issue = nil
654 655 nil
655 656 end
656 657 end
657 658
658 659 def parent_issue_id
659 660 if instance_variable_defined? :@parent_issue
660 661 @parent_issue.nil? ? nil : @parent_issue.id
661 662 else
662 663 parent_id
663 664 end
664 665 end
665 666
666 667 # Extracted from the ReportsController.
667 668 def self.by_tracker(project)
668 669 count_and_group_by(:project => project,
669 670 :field => 'tracker_id',
670 671 :joins => Tracker.table_name)
671 672 end
672 673
673 674 def self.by_version(project)
674 675 count_and_group_by(:project => project,
675 676 :field => 'fixed_version_id',
676 677 :joins => Version.table_name)
677 678 end
678 679
679 680 def self.by_priority(project)
680 681 count_and_group_by(:project => project,
681 682 :field => 'priority_id',
682 683 :joins => IssuePriority.table_name)
683 684 end
684 685
685 686 def self.by_category(project)
686 687 count_and_group_by(:project => project,
687 688 :field => 'category_id',
688 689 :joins => IssueCategory.table_name)
689 690 end
690 691
691 692 def self.by_assigned_to(project)
692 693 count_and_group_by(:project => project,
693 694 :field => 'assigned_to_id',
694 695 :joins => User.table_name)
695 696 end
696 697
697 698 def self.by_author(project)
698 699 count_and_group_by(:project => project,
699 700 :field => 'author_id',
700 701 :joins => User.table_name)
701 702 end
702 703
703 704 def self.by_subproject(project)
704 705 ActiveRecord::Base.connection.select_all("select s.id as status_id,
705 706 s.is_closed as closed,
706 707 #{Issue.table_name}.project_id as project_id,
707 708 count(#{Issue.table_name}.id) as total
708 709 from
709 710 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
710 711 where
711 712 #{Issue.table_name}.status_id=s.id
712 713 and #{Issue.table_name}.project_id = #{Project.table_name}.id
713 714 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
714 715 and #{Issue.table_name}.project_id <> #{project.id}
715 716 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
716 717 end
717 718 # End ReportsController extraction
718 719
719 720 # Returns an array of projects that current user can move issues to
720 721 def self.allowed_target_projects_on_move
721 722 projects = []
722 723 if User.current.admin?
723 724 # admin is allowed to move issues to any active (visible) project
724 725 projects = Project.visible.all
725 726 elsif User.current.logged?
726 727 if Role.non_member.allowed_to?(:move_issues)
727 728 projects = Project.visible.all
728 729 else
729 730 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
730 731 end
731 732 end
732 733 projects
733 734 end
734 735
735 736 private
736 737
737 738 def update_nested_set_attributes
738 739 if root_id.nil?
739 740 # issue was just created
740 741 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
741 742 set_default_left_and_right
742 743 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
743 744 if @parent_issue
744 745 move_to_child_of(@parent_issue)
745 746 end
746 747 reload
747 748 elsif parent_issue_id != parent_id
748 749 former_parent_id = parent_id
749 750 # moving an existing issue
750 751 if @parent_issue && @parent_issue.root_id == root_id
751 752 # inside the same tree
752 753 move_to_child_of(@parent_issue)
753 754 else
754 755 # to another tree
755 756 unless root?
756 757 move_to_right_of(root)
757 758 reload
758 759 end
759 760 old_root_id = root_id
760 761 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
761 762 target_maxright = nested_set_scope.maximum(right_column_name) || 0
762 763 offset = target_maxright + 1 - lft
763 764 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
764 765 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
765 766 self[left_column_name] = lft + offset
766 767 self[right_column_name] = rgt + offset
767 768 if @parent_issue
768 769 move_to_child_of(@parent_issue)
769 770 end
770 771 end
771 772 reload
772 773 # delete invalid relations of all descendants
773 774 self_and_descendants.each do |issue|
774 775 issue.relations.each do |relation|
775 776 relation.destroy unless relation.valid?
776 777 end
777 778 end
778 779 # update former parent
779 780 recalculate_attributes_for(former_parent_id) if former_parent_id
780 781 end
781 782 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
782 783 end
783 784
784 785 def update_parent_attributes
785 786 recalculate_attributes_for(parent_id) if parent_id
786 787 end
787 788
788 789 def recalculate_attributes_for(issue_id)
789 790 if issue_id && p = Issue.find_by_id(issue_id)
790 791 # priority = highest priority of children
791 792 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
792 793 p.priority = IssuePriority.find_by_position(priority_position)
793 794 end
794 795
795 796 # start/due dates = lowest/highest dates of children
796 797 p.start_date = p.children.minimum(:start_date)
797 798 p.due_date = p.children.maximum(:due_date)
798 799 if p.start_date && p.due_date && p.due_date < p.start_date
799 800 p.start_date, p.due_date = p.due_date, p.start_date
800 801 end
801 802
802 803 # done ratio = weighted average ratio of leaves
803 804 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
804 805 leaves_count = p.leaves.count
805 806 if leaves_count > 0
806 807 average = p.leaves.average(:estimated_hours).to_f
807 808 if average == 0
808 809 average = 1
809 810 end
810 811 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
811 812 progress = done / (average * leaves_count)
812 813 p.done_ratio = progress.round
813 814 end
814 815 end
815 816
816 817 # estimate = sum of leaves estimates
817 818 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
818 819 p.estimated_hours = nil if p.estimated_hours == 0.0
819 820
820 821 # ancestors will be recursively updated
821 822 p.save(false)
822 823 end
823 824 end
824 825
825 826 # Update issues so their versions are not pointing to a
826 827 # fixed_version that is not shared with the issue's project
827 828 def self.update_versions(conditions=nil)
828 829 # Only need to update issues with a fixed_version from
829 830 # a different project and that is not systemwide shared
830 831 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
831 832 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
832 833 " AND #{Version.table_name}.sharing <> 'system'",
833 834 conditions),
834 835 :include => [:project, :fixed_version]
835 836 ).each do |issue|
836 837 next if issue.project.nil? || issue.fixed_version.nil?
837 838 unless issue.project.shared_versions.include?(issue.fixed_version)
838 839 issue.init_journal(User.current)
839 840 issue.fixed_version = nil
840 841 issue.save
841 842 end
842 843 end
843 844 end
844 845
845 846 # Callback on attachment deletion
846 847 def attachment_removed(obj)
847 848 journal = init_journal(User.current)
848 849 journal.details << JournalDetail.new(:property => 'attachment',
849 850 :prop_key => obj.id,
850 851 :old_value => obj.filename)
851 852 journal.save
852 853 end
853 854
854 855 # Default assignment based on category
855 856 def default_assign
856 857 if assigned_to.nil? && category && category.assigned_to
857 858 self.assigned_to = category.assigned_to
858 859 end
859 860 end
860 861
861 862 # Updates start/due dates of following issues
862 863 def reschedule_following_issues
863 864 if start_date_changed? || due_date_changed?
864 865 relations_from.each do |relation|
865 866 relation.set_issue_to_dates
866 867 end
867 868 end
868 869 end
869 870
870 871 # Closes duplicates if the issue is being closed
871 872 def close_duplicates
872 873 if closing?
873 874 duplicates.each do |duplicate|
874 875 # Reload is need in case the duplicate was updated by a previous duplicate
875 876 duplicate.reload
876 877 # Don't re-close it if it's already closed
877 878 next if duplicate.closed?
878 879 # Same user and notes
879 880 if @current_journal
880 881 duplicate.init_journal(@current_journal.user, @current_journal.notes)
881 882 end
882 883 duplicate.update_attribute :status, self.status
883 884 end
884 885 end
885 886 end
886 887
887 888 # Saves the changes in a Journal
888 889 # Called after_save
889 890 def create_journal
890 891 if @current_journal
891 892 # attributes changes
892 893 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
893 894 before = @issue_before_change.send(c)
894 895 after = send(c)
895 896 next if before == after || (before.blank? && after.blank?)
896 897 @current_journal.details << JournalDetail.new(:property => 'attr',
897 898 :prop_key => c,
898 899 :old_value => @issue_before_change.send(c),
899 900 :value => send(c))
900 901 }
901 902 # custom fields changes
902 903 custom_values.each {|c|
903 904 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
904 905 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
905 906 @current_journal.details << JournalDetail.new(:property => 'cf',
906 907 :prop_key => c.custom_field_id,
907 908 :old_value => @custom_values_before_change[c.custom_field_id],
908 909 :value => c.value)
909 910 }
910 911 @current_journal.save
911 912 # reset current journal
912 913 init_journal @current_journal.user, @current_journal.notes
913 914 end
914 915 end
915 916
916 917 # Query generator for selecting groups of issue counts for a project
917 918 # based on specific criteria
918 919 #
919 920 # Options
920 921 # * project - Project to search in.
921 922 # * field - String. Issue field to key off of in the grouping.
922 923 # * joins - String. The table name to join against.
923 924 def self.count_and_group_by(options)
924 925 project = options.delete(:project)
925 926 select_field = options.delete(:field)
926 927 joins = options.delete(:joins)
927 928
928 929 where = "#{Issue.table_name}.#{select_field}=j.id"
929 930
930 931 ActiveRecord::Base.connection.select_all("select s.id as status_id,
931 932 s.is_closed as closed,
932 933 j.id as #{select_field},
933 934 count(#{Issue.table_name}.id) as total
934 935 from
935 936 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
936 937 where
937 938 #{Issue.table_name}.status_id=s.id
938 939 and #{where}
939 940 and #{Issue.table_name}.project_id=#{Project.table_name}.id
940 941 and #{visible_condition(User.current, :project => project)}
941 942 group by s.id, s.is_closed, j.id")
942 943 end
943 944 end
@@ -1,1056 +1,1065
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 IssueTest < ActiveSupport::TestCase
21 21 fixtures :projects, :users, :members, :member_roles, :roles,
22 22 :trackers, :projects_trackers,
23 23 :enabled_modules,
24 24 :versions,
25 25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 26 :enumerations,
27 27 :issues,
28 28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 29 :time_entries
30 30
31 31 def test_create
32 32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
33 33 assert issue.save
34 34 issue.reload
35 35 assert_equal 1.5, issue.estimated_hours
36 36 end
37 37
38 38 def test_create_minimal
39 39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
40 40 assert issue.save
41 41 assert issue.description.nil?
42 42 end
43 43
44 44 def test_create_with_required_custom_field
45 45 field = IssueCustomField.find_by_name('Database')
46 46 field.update_attribute(:is_required, true)
47 47
48 48 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
49 49 assert issue.available_custom_fields.include?(field)
50 50 # No value for the custom field
51 51 assert !issue.save
52 52 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
53 53 # Blank value
54 54 issue.custom_field_values = { field.id => '' }
55 55 assert !issue.save
56 56 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
57 57 # Invalid value
58 58 issue.custom_field_values = { field.id => 'SQLServer' }
59 59 assert !issue.save
60 60 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
61 61 # Valid value
62 62 issue.custom_field_values = { field.id => 'PostgreSQL' }
63 63 assert issue.save
64 64 issue.reload
65 65 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
66 66 end
67 67
68 68 def test_create_with_group_assignment
69 69 with_settings :issue_group_assignment => '1' do
70 70 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1, :subject => 'Group assignment', :assigned_to_id => 11).save
71 71 issue = Issue.first(:order => 'id DESC')
72 72 assert_kind_of Group, issue.assigned_to
73 73 assert_equal Group.find(11), issue.assigned_to
74 74 end
75 75 end
76 76
77 77 def assert_visibility_match(user, issues)
78 78 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
79 79 end
80 80
81 81 def test_visible_scope_for_anonymous
82 82 # Anonymous user should see issues of public projects only
83 83 issues = Issue.visible(User.anonymous).all
84 84 assert issues.any?
85 85 assert_nil issues.detect {|issue| !issue.project.is_public?}
86 86 assert_nil issues.detect {|issue| issue.is_private?}
87 87 assert_visibility_match User.anonymous, issues
88 88 end
89 89
90 90 def test_visible_scope_for_anonymous_with_own_issues_visibility
91 91 Role.anonymous.update_attribute :issues_visibility, 'own'
92 92 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => User.anonymous.id, :subject => 'Issue by anonymous')
93 93
94 94 issues = Issue.visible(User.anonymous).all
95 95 assert issues.any?
96 96 assert_nil issues.detect {|issue| issue.author != User.anonymous}
97 97 assert_visibility_match User.anonymous, issues
98 98 end
99 99
100 100 def test_visible_scope_for_anonymous_without_view_issues_permissions
101 101 # Anonymous user should not see issues without permission
102 102 Role.anonymous.remove_permission!(:view_issues)
103 103 issues = Issue.visible(User.anonymous).all
104 104 assert issues.empty?
105 105 assert_visibility_match User.anonymous, issues
106 106 end
107 107
108 108 def test_visible_scope_for_non_member
109 109 user = User.find(9)
110 110 assert user.projects.empty?
111 111 # Non member user should see issues of public projects only
112 112 issues = Issue.visible(user).all
113 113 assert issues.any?
114 114 assert_nil issues.detect {|issue| !issue.project.is_public?}
115 115 assert_nil issues.detect {|issue| issue.is_private?}
116 116 assert_visibility_match user, issues
117 117 end
118 118
119 119 def test_visible_scope_for_non_member_with_own_issues_visibility
120 120 Role.non_member.update_attribute :issues_visibility, 'own'
121 121 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
122 122 user = User.find(9)
123 123
124 124 issues = Issue.visible(user).all
125 125 assert issues.any?
126 126 assert_nil issues.detect {|issue| issue.author != user}
127 127 assert_visibility_match user, issues
128 128 end
129 129
130 130 def test_visible_scope_for_non_member_without_view_issues_permissions
131 131 # Non member user should not see issues without permission
132 132 Role.non_member.remove_permission!(:view_issues)
133 133 user = User.find(9)
134 134 assert user.projects.empty?
135 135 issues = Issue.visible(user).all
136 136 assert issues.empty?
137 137 assert_visibility_match user, issues
138 138 end
139 139
140 140 def test_visible_scope_for_member
141 141 user = User.find(9)
142 142 # User should see issues of projects for which he has view_issues permissions only
143 143 Role.non_member.remove_permission!(:view_issues)
144 144 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
145 145 issues = Issue.visible(user).all
146 146 assert issues.any?
147 147 assert_nil issues.detect {|issue| issue.project_id != 3}
148 148 assert_nil issues.detect {|issue| issue.is_private?}
149 149 assert_visibility_match user, issues
150 150 end
151 151
152 152 def test_visible_scope_for_admin
153 153 user = User.find(1)
154 154 user.members.each(&:destroy)
155 155 assert user.projects.empty?
156 156 issues = Issue.visible(user).all
157 157 assert issues.any?
158 158 # Admin should see issues on private projects that he does not belong to
159 159 assert issues.detect {|issue| !issue.project.is_public?}
160 160 # Admin should see private issues of other users
161 161 assert issues.detect {|issue| issue.is_private? && issue.author != user}
162 162 assert_visibility_match user, issues
163 163 end
164 164
165 165 def test_visible_scope_with_project
166 166 project = Project.find(1)
167 167 issues = Issue.visible(User.find(2), :project => project).all
168 168 projects = issues.collect(&:project).uniq
169 169 assert_equal 1, projects.size
170 170 assert_equal project, projects.first
171 171 end
172 172
173 173 def test_visible_scope_with_project_and_subprojects
174 174 project = Project.find(1)
175 175 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
176 176 projects = issues.collect(&:project).uniq
177 177 assert projects.size > 1
178 178 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
179 179 end
180 180
181 181 def test_visible_and_nested_set_scopes
182 182 assert_equal 0, Issue.find(1).descendants.visible.all.size
183 183 end
184 184
185 185 def test_errors_full_messages_should_include_custom_fields_errors
186 186 field = IssueCustomField.find_by_name('Database')
187 187
188 188 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
189 189 assert issue.available_custom_fields.include?(field)
190 190 # Invalid value
191 191 issue.custom_field_values = { field.id => 'SQLServer' }
192 192
193 193 assert !issue.valid?
194 194 assert_equal 1, issue.errors.full_messages.size
195 195 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
196 196 end
197 197
198 198 def test_update_issue_with_required_custom_field
199 199 field = IssueCustomField.find_by_name('Database')
200 200 field.update_attribute(:is_required, true)
201 201
202 202 issue = Issue.find(1)
203 203 assert_nil issue.custom_value_for(field)
204 204 assert issue.available_custom_fields.include?(field)
205 205 # No change to custom values, issue can be saved
206 206 assert issue.save
207 207 # Blank value
208 208 issue.custom_field_values = { field.id => '' }
209 209 assert !issue.save
210 210 # Valid value
211 211 issue.custom_field_values = { field.id => 'PostgreSQL' }
212 212 assert issue.save
213 213 issue.reload
214 214 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
215 215 end
216 216
217 217 def test_should_not_update_attributes_if_custom_fields_validation_fails
218 218 issue = Issue.find(1)
219 219 field = IssueCustomField.find_by_name('Database')
220 220 assert issue.available_custom_fields.include?(field)
221 221
222 222 issue.custom_field_values = { field.id => 'Invalid' }
223 223 issue.subject = 'Should be not be saved'
224 224 assert !issue.save
225 225
226 226 issue.reload
227 227 assert_equal "Can't print recipes", issue.subject
228 228 end
229 229
230 230 def test_should_not_recreate_custom_values_objects_on_update
231 231 field = IssueCustomField.find_by_name('Database')
232 232
233 233 issue = Issue.find(1)
234 234 issue.custom_field_values = { field.id => 'PostgreSQL' }
235 235 assert issue.save
236 236 custom_value = issue.custom_value_for(field)
237 237 issue.reload
238 238 issue.custom_field_values = { field.id => 'MySQL' }
239 239 assert issue.save
240 240 issue.reload
241 241 assert_equal custom_value.id, issue.custom_value_for(field).id
242 242 end
243 243
244 244 def test_assigning_tracker_id_should_reload_custom_fields_values
245 245 issue = Issue.new(:project => Project.find(1))
246 246 assert issue.custom_field_values.empty?
247 247 issue.tracker_id = 1
248 248 assert issue.custom_field_values.any?
249 249 end
250 250
251 251 def test_assigning_attributes_should_assign_tracker_id_first
252 252 attributes = ActiveSupport::OrderedHash.new
253 253 attributes['custom_field_values'] = { '1' => 'MySQL' }
254 254 attributes['tracker_id'] = '1'
255 255 issue = Issue.new(:project => Project.find(1))
256 256 issue.attributes = attributes
257 257 assert_not_nil issue.custom_value_for(1)
258 258 assert_equal 'MySQL', issue.custom_value_for(1).value
259 259 end
260 260
261 261 def test_should_update_issue_with_disabled_tracker
262 262 p = Project.find(1)
263 263 issue = Issue.find(1)
264 264
265 265 p.trackers.delete(issue.tracker)
266 266 assert !p.trackers.include?(issue.tracker)
267 267
268 268 issue.reload
269 269 issue.subject = 'New subject'
270 270 assert issue.save
271 271 end
272 272
273 273 def test_should_not_set_a_disabled_tracker
274 274 p = Project.find(1)
275 275 p.trackers.delete(Tracker.find(2))
276 276
277 277 issue = Issue.find(1)
278 278 issue.tracker_id = 2
279 279 issue.subject = 'New subject'
280 280 assert !issue.save
281 281 assert_not_nil issue.errors.on(:tracker_id)
282 282 end
283 283
284 284 def test_category_based_assignment
285 285 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
286 286 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
287 287 end
288 288
289 289 def test_new_statuses_allowed_to
290 290 Workflow.delete_all
291 291
292 292 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
293 293 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
294 294 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
295 295 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
296 296 status = IssueStatus.find(1)
297 297 role = Role.find(1)
298 298 tracker = Tracker.find(1)
299 299 user = User.find(2)
300 300
301 301 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
302 302 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
303 303
304 304 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
305 305 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
306 306
307 307 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
308 308 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
309 309
310 310 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
311 311 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
312 312 end
313 313
314 314 def test_copy
315 315 issue = Issue.new.copy_from(1)
316 316 assert issue.save
317 317 issue.reload
318 318 orig = Issue.find(1)
319 319 assert_equal orig.subject, issue.subject
320 320 assert_equal orig.tracker, issue.tracker
321 321 assert_equal "125", issue.custom_value_for(2).value
322 322 end
323 323
324 324 def test_copy_should_copy_status
325 325 orig = Issue.find(8)
326 326 assert orig.status != IssueStatus.default
327 327
328 328 issue = Issue.new.copy_from(orig)
329 329 assert issue.save
330 330 issue.reload
331 331 assert_equal orig.status, issue.status
332 332 end
333 333
334 334 def test_should_close_duplicates
335 335 # Create 3 issues
336 336 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
337 337 assert issue1.save
338 338 issue2 = issue1.clone
339 339 assert issue2.save
340 340 issue3 = issue1.clone
341 341 assert issue3.save
342 342
343 343 # 2 is a dupe of 1
344 344 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
345 345 # And 3 is a dupe of 2
346 346 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
347 347 # And 3 is a dupe of 1 (circular duplicates)
348 348 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
349 349
350 350 assert issue1.reload.duplicates.include?(issue2)
351 351
352 352 # Closing issue 1
353 353 issue1.init_journal(User.find(:first), "Closing issue1")
354 354 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
355 355 assert issue1.save
356 356 # 2 and 3 should be also closed
357 357 assert issue2.reload.closed?
358 358 assert issue3.reload.closed?
359 359 end
360 360
361 361 def test_should_not_close_duplicated_issue
362 362 # Create 3 issues
363 363 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
364 364 assert issue1.save
365 365 issue2 = issue1.clone
366 366 assert issue2.save
367 367
368 368 # 2 is a dupe of 1
369 369 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
370 370 # 2 is a dup of 1 but 1 is not a duplicate of 2
371 371 assert !issue2.reload.duplicates.include?(issue1)
372 372
373 373 # Closing issue 2
374 374 issue2.init_journal(User.find(:first), "Closing issue2")
375 375 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
376 376 assert issue2.save
377 377 # 1 should not be also closed
378 378 assert !issue1.reload.closed?
379 379 end
380 380
381 381 def test_assignable_versions
382 382 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
383 383 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
384 384 end
385 385
386 386 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
387 387 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
388 388 assert !issue.save
389 389 assert_not_nil issue.errors.on(:fixed_version_id)
390 390 end
391 391
392 392 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
393 393 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
394 394 assert !issue.save
395 395 assert_not_nil issue.errors.on(:fixed_version_id)
396 396 end
397 397
398 398 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
399 399 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
400 400 assert issue.save
401 401 end
402 402
403 403 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
404 404 issue = Issue.find(11)
405 405 assert_equal 'closed', issue.fixed_version.status
406 406 issue.subject = 'Subject changed'
407 407 assert issue.save
408 408 end
409 409
410 410 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
411 411 issue = Issue.find(11)
412 412 issue.status_id = 1
413 413 assert !issue.save
414 414 assert_not_nil issue.errors.on_base
415 415 end
416 416
417 417 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
418 418 issue = Issue.find(11)
419 419 issue.status_id = 1
420 420 issue.fixed_version_id = 3
421 421 assert issue.save
422 422 end
423 423
424 424 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
425 425 issue = Issue.find(12)
426 426 assert_equal 'locked', issue.fixed_version.status
427 427 issue.status_id = 1
428 428 assert issue.save
429 429 end
430 430
431 431 def test_move_to_another_project_with_same_category
432 432 issue = Issue.find(1)
433 433 assert issue.move_to_project(Project.find(2))
434 434 issue.reload
435 435 assert_equal 2, issue.project_id
436 436 # Category changes
437 437 assert_equal 4, issue.category_id
438 438 # Make sure time entries were move to the target project
439 439 assert_equal 2, issue.time_entries.first.project_id
440 440 end
441 441
442 442 def test_move_to_another_project_without_same_category
443 443 issue = Issue.find(2)
444 444 assert issue.move_to_project(Project.find(2))
445 445 issue.reload
446 446 assert_equal 2, issue.project_id
447 447 # Category cleared
448 448 assert_nil issue.category_id
449 449 end
450 450
451 451 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
452 452 issue = Issue.find(1)
453 453 issue.update_attribute(:fixed_version_id, 1)
454 454 assert issue.move_to_project(Project.find(2))
455 455 issue.reload
456 456 assert_equal 2, issue.project_id
457 457 # Cleared fixed_version
458 458 assert_equal nil, issue.fixed_version
459 459 end
460 460
461 461 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
462 462 issue = Issue.find(1)
463 463 issue.update_attribute(:fixed_version_id, 4)
464 464 assert issue.move_to_project(Project.find(5))
465 465 issue.reload
466 466 assert_equal 5, issue.project_id
467 467 # Keep fixed_version
468 468 assert_equal 4, issue.fixed_version_id
469 469 end
470 470
471 471 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
472 472 issue = Issue.find(1)
473 473 issue.update_attribute(:fixed_version_id, 1)
474 474 assert issue.move_to_project(Project.find(5))
475 475 issue.reload
476 476 assert_equal 5, issue.project_id
477 477 # Cleared fixed_version
478 478 assert_equal nil, issue.fixed_version
479 479 end
480 480
481 481 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
482 482 issue = Issue.find(1)
483 483 issue.update_attribute(:fixed_version_id, 7)
484 484 assert issue.move_to_project(Project.find(2))
485 485 issue.reload
486 486 assert_equal 2, issue.project_id
487 487 # Keep fixed_version
488 488 assert_equal 7, issue.fixed_version_id
489 489 end
490 490
491 491 def test_move_to_another_project_with_disabled_tracker
492 492 issue = Issue.find(1)
493 493 target = Project.find(2)
494 494 target.tracker_ids = [3]
495 495 target.save
496 496 assert_equal false, issue.move_to_project(target)
497 497 issue.reload
498 498 assert_equal 1, issue.project_id
499 499 end
500 500
501 501 def test_copy_to_the_same_project
502 502 issue = Issue.find(1)
503 503 copy = nil
504 504 assert_difference 'Issue.count' do
505 505 copy = issue.move_to_project(issue.project, nil, :copy => true)
506 506 end
507 507 assert_kind_of Issue, copy
508 508 assert_equal issue.project, copy.project
509 509 assert_equal "125", copy.custom_value_for(2).value
510 510 end
511 511
512 512 def test_copy_to_another_project_and_tracker
513 513 issue = Issue.find(1)
514 514 copy = nil
515 515 assert_difference 'Issue.count' do
516 516 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
517 517 end
518 518 copy.reload
519 519 assert_kind_of Issue, copy
520 520 assert_equal Project.find(3), copy.project
521 521 assert_equal Tracker.find(2), copy.tracker
522 522 # Custom field #2 is not associated with target tracker
523 523 assert_nil copy.custom_value_for(2)
524 524 end
525 525
526 526 context "#move_to_project" do
527 527 context "as a copy" do
528 528 setup do
529 529 @issue = Issue.find(1)
530 530 @copy = nil
531 531 end
532 532
533 533 should "not create a journal" do
534 534 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
535 535 assert_equal 0, @copy.reload.journals.size
536 536 end
537 537
538 538 should "allow assigned_to changes" do
539 539 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
540 540 assert_equal 3, @copy.assigned_to_id
541 541 end
542 542
543 543 should "allow status changes" do
544 544 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
545 545 assert_equal 2, @copy.status_id
546 546 end
547 547
548 548 should "allow start date changes" do
549 549 date = Date.today
550 550 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
551 551 assert_equal date, @copy.start_date
552 552 end
553 553
554 554 should "allow due date changes" do
555 555 date = Date.today
556 556 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
557 557
558 558 assert_equal date, @copy.due_date
559 559 end
560 560
561 561 should "set current user as author" do
562 562 User.current = User.find(9)
563 563 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {}})
564 564
565 565 assert_equal User.current, @copy.author
566 566 end
567 567
568 568 should "keep journal notes" do
569 569 date = Date.today
570 570 notes = "Notes added when copying"
571 571 User.current = User.find(9)
572 572 @issue.init_journal(User.current, notes)
573 573 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
574 574
575 575 assert_equal 1, @copy.journals.size
576 576 journal = @copy.journals.first
577 577 assert_equal 0, journal.details.size
578 578 assert_equal notes, journal.notes
579 579 end
580 580 end
581 581 end
582 582
583 583 def test_recipients_should_not_include_users_that_cannot_view_the_issue
584 584 issue = Issue.find(12)
585 585 assert issue.recipients.include?(issue.author.mail)
586 586 # move the issue to a private project
587 587 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
588 588 # author is not a member of project anymore
589 589 assert !copy.recipients.include?(copy.author.mail)
590 590 end
591 591
592 592 def test_recipients_should_include_the_assigned_group_members
593 593 group_member = User.generate_with_protected!
594 594 group = Group.generate!
595 595 group.users << group_member
596 596
597 597 issue = Issue.find(12)
598 598 issue.assigned_to = group
599 599 assert issue.recipients.include?(group_member.mail)
600 600 end
601 601
602 602 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
603 603 user = User.find(3)
604 604 issue = Issue.find(9)
605 605 Watcher.create!(:user => user, :watchable => issue)
606 606 assert issue.watched_by?(user)
607 607 assert !issue.watcher_recipients.include?(user.mail)
608 608 end
609 609
610 610 def test_issue_destroy
611 611 Issue.find(1).destroy
612 612 assert_nil Issue.find_by_id(1)
613 613 assert_nil TimeEntry.find_by_issue_id(1)
614 614 end
615 615
616 616 def test_blocked
617 617 blocked_issue = Issue.find(9)
618 618 blocking_issue = Issue.find(10)
619 619
620 620 assert blocked_issue.blocked?
621 621 assert !blocking_issue.blocked?
622 622 end
623 623
624 624 def test_blocked_issues_dont_allow_closed_statuses
625 625 blocked_issue = Issue.find(9)
626 626
627 627 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
628 628 assert !allowed_statuses.empty?
629 629 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
630 630 assert closed_statuses.empty?
631 631 end
632 632
633 633 def test_unblocked_issues_allow_closed_statuses
634 634 blocking_issue = Issue.find(10)
635 635
636 636 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
637 637 assert !allowed_statuses.empty?
638 638 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
639 639 assert !closed_statuses.empty?
640 640 end
641 641
642 642 def test_rescheduling_an_issue_should_reschedule_following_issue
643 643 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)
644 644 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)
645 645 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
646 646 assert_equal issue1.due_date + 1, issue2.reload.start_date
647 647
648 648 issue1.due_date = Date.today + 5
649 649 issue1.save!
650 650 assert_equal issue1.due_date + 1, issue2.reload.start_date
651 651 end
652 652
653 653 def test_overdue
654 654 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
655 655 assert !Issue.new(:due_date => Date.today).overdue?
656 656 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
657 657 assert !Issue.new(:due_date => nil).overdue?
658 658 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
659 659 end
660 660
661 661 context "#behind_schedule?" do
662 662 should "be false if the issue has no start_date" do
663 663 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
664 664 end
665 665
666 666 should "be false if the issue has no end_date" do
667 667 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
668 668 end
669 669
670 670 should "be false if the issue has more done than it's calendar time" do
671 671 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
672 672 end
673 673
674 674 should "be true if the issue hasn't been started at all" do
675 675 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
676 676 end
677 677
678 678 should "be true if the issue has used more calendar time than it's done ratio" do
679 679 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
680 680 end
681 681 end
682 682
683 683 context "#assignable_users" do
684 684 should "be Users" do
685 685 assert_kind_of User, Issue.find(1).assignable_users.first
686 686 end
687 687
688 688 should "include the issue author" do
689 689 project = Project.find(1)
690 690 non_project_member = User.generate!
691 691 issue = Issue.generate_for_project!(project, :author => non_project_member)
692 692
693 693 assert issue.assignable_users.include?(non_project_member)
694 694 end
695 695
696 should "include the current assignee" do
697 project = Project.find(1)
698 user = User.generate!
699 issue = Issue.generate_for_project!(project, :assigned_to => user)
700 user.lock!
701
702 assert Issue.find(issue.id).assignable_users.include?(user)
703 end
704
696 705 should "not show the issue author twice" do
697 706 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
698 707 assert_equal 2, assignable_user_ids.length
699 708
700 709 assignable_user_ids.each do |user_id|
701 710 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
702 711 end
703 712 end
704 713
705 714 context "with issue_group_assignment" do
706 715 should "include groups" do
707 716 issue = Issue.new(:project => Project.find(2))
708 717
709 718 with_settings :issue_group_assignment => '1' do
710 719 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
711 720 assert issue.assignable_users.include?(Group.find(11))
712 721 end
713 722 end
714 723 end
715 724
716 725 context "without issue_group_assignment" do
717 726 should "not include groups" do
718 727 issue = Issue.new(:project => Project.find(2))
719 728
720 729 with_settings :issue_group_assignment => '0' do
721 730 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
722 731 assert !issue.assignable_users.include?(Group.find(11))
723 732 end
724 733 end
725 734 end
726 735 end
727 736
728 737 def test_create_should_send_email_notification
729 738 ActionMailer::Base.deliveries.clear
730 739 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
731 740
732 741 assert issue.save
733 742 assert_equal 1, ActionMailer::Base.deliveries.size
734 743 end
735 744
736 745 def test_stale_issue_should_not_send_email_notification
737 746 ActionMailer::Base.deliveries.clear
738 747 issue = Issue.find(1)
739 748 stale = Issue.find(1)
740 749
741 750 issue.init_journal(User.find(1))
742 751 issue.subject = 'Subjet update'
743 752 assert issue.save
744 753 assert_equal 1, ActionMailer::Base.deliveries.size
745 754 ActionMailer::Base.deliveries.clear
746 755
747 756 stale.init_journal(User.find(1))
748 757 stale.subject = 'Another subjet update'
749 758 assert_raise ActiveRecord::StaleObjectError do
750 759 stale.save
751 760 end
752 761 assert ActionMailer::Base.deliveries.empty?
753 762 end
754 763
755 764 def test_journalized_description
756 765 IssueCustomField.delete_all
757 766
758 767 i = Issue.first
759 768 old_description = i.description
760 769 new_description = "This is the new description"
761 770
762 771 i.init_journal(User.find(2))
763 772 i.description = new_description
764 773 assert_difference 'Journal.count', 1 do
765 774 assert_difference 'JournalDetail.count', 1 do
766 775 i.save!
767 776 end
768 777 end
769 778
770 779 detail = JournalDetail.first(:order => 'id DESC')
771 780 assert_equal i, detail.journal.journalized
772 781 assert_equal 'attr', detail.property
773 782 assert_equal 'description', detail.prop_key
774 783 assert_equal old_description, detail.old_value
775 784 assert_equal new_description, detail.value
776 785 end
777 786
778 787 def test_blank_descriptions_should_not_be_journalized
779 788 IssueCustomField.delete_all
780 789 Issue.update_all("description = NULL", "id=1")
781 790
782 791 i = Issue.find(1)
783 792 i.init_journal(User.find(2))
784 793 i.subject = "blank description"
785 794 i.description = "\r\n"
786 795
787 796 assert_difference 'Journal.count', 1 do
788 797 assert_difference 'JournalDetail.count', 1 do
789 798 i.save!
790 799 end
791 800 end
792 801 end
793 802
794 803 def test_description_eol_should_be_normalized
795 804 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
796 805 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
797 806 end
798 807
799 808 def test_saving_twice_should_not_duplicate_journal_details
800 809 i = Issue.find(:first)
801 810 i.init_journal(User.find(2), 'Some notes')
802 811 # initial changes
803 812 i.subject = 'New subject'
804 813 i.done_ratio = i.done_ratio + 10
805 814 assert_difference 'Journal.count' do
806 815 assert i.save
807 816 end
808 817 # 1 more change
809 818 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
810 819 assert_no_difference 'Journal.count' do
811 820 assert_difference 'JournalDetail.count', 1 do
812 821 i.save
813 822 end
814 823 end
815 824 # no more change
816 825 assert_no_difference 'Journal.count' do
817 826 assert_no_difference 'JournalDetail.count' do
818 827 i.save
819 828 end
820 829 end
821 830 end
822 831
823 832 def test_all_dependent_issues
824 833 IssueRelation.delete_all
825 834 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
826 835 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
827 836 assert IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(8), :relation_type => IssueRelation::TYPE_PRECEDES)
828 837
829 838 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
830 839 end
831 840
832 841 def test_all_dependent_issues_with_persistent_circular_dependency
833 842 IssueRelation.delete_all
834 843 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
835 844 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
836 845 # Validation skipping
837 846 assert IssueRelation.new(:issue_from => Issue.find(3), :issue_to => Issue.find(1), :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
838 847
839 848 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
840 849 end
841 850
842 851 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
843 852 IssueRelation.delete_all
844 853 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_RELATES)
845 854 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_RELATES)
846 855 assert IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(8), :relation_type => IssueRelation::TYPE_RELATES)
847 856 # Validation skipping
848 857 assert IssueRelation.new(:issue_from => Issue.find(8), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_RELATES).save(false)
849 858 assert IssueRelation.new(:issue_from => Issue.find(3), :issue_to => Issue.find(1), :relation_type => IssueRelation::TYPE_RELATES).save(false)
850 859
851 860 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
852 861 end
853 862
854 863 context "#done_ratio" do
855 864 setup do
856 865 @issue = Issue.find(1)
857 866 @issue_status = IssueStatus.find(1)
858 867 @issue_status.update_attribute(:default_done_ratio, 50)
859 868 @issue2 = Issue.find(2)
860 869 @issue_status2 = IssueStatus.find(2)
861 870 @issue_status2.update_attribute(:default_done_ratio, 0)
862 871 end
863 872
864 873 context "with Setting.issue_done_ratio using the issue_field" do
865 874 setup do
866 875 Setting.issue_done_ratio = 'issue_field'
867 876 end
868 877
869 878 should "read the issue's field" do
870 879 assert_equal 0, @issue.done_ratio
871 880 assert_equal 30, @issue2.done_ratio
872 881 end
873 882 end
874 883
875 884 context "with Setting.issue_done_ratio using the issue_status" do
876 885 setup do
877 886 Setting.issue_done_ratio = 'issue_status'
878 887 end
879 888
880 889 should "read the Issue Status's default done ratio" do
881 890 assert_equal 50, @issue.done_ratio
882 891 assert_equal 0, @issue2.done_ratio
883 892 end
884 893 end
885 894 end
886 895
887 896 context "#update_done_ratio_from_issue_status" do
888 897 setup do
889 898 @issue = Issue.find(1)
890 899 @issue_status = IssueStatus.find(1)
891 900 @issue_status.update_attribute(:default_done_ratio, 50)
892 901 @issue2 = Issue.find(2)
893 902 @issue_status2 = IssueStatus.find(2)
894 903 @issue_status2.update_attribute(:default_done_ratio, 0)
895 904 end
896 905
897 906 context "with Setting.issue_done_ratio using the issue_field" do
898 907 setup do
899 908 Setting.issue_done_ratio = 'issue_field'
900 909 end
901 910
902 911 should "not change the issue" do
903 912 @issue.update_done_ratio_from_issue_status
904 913 @issue2.update_done_ratio_from_issue_status
905 914
906 915 assert_equal 0, @issue.read_attribute(:done_ratio)
907 916 assert_equal 30, @issue2.read_attribute(:done_ratio)
908 917 end
909 918 end
910 919
911 920 context "with Setting.issue_done_ratio using the issue_status" do
912 921 setup do
913 922 Setting.issue_done_ratio = 'issue_status'
914 923 end
915 924
916 925 should "change the issue's done ratio" do
917 926 @issue.update_done_ratio_from_issue_status
918 927 @issue2.update_done_ratio_from_issue_status
919 928
920 929 assert_equal 50, @issue.read_attribute(:done_ratio)
921 930 assert_equal 0, @issue2.read_attribute(:done_ratio)
922 931 end
923 932 end
924 933 end
925 934
926 935 test "#by_tracker" do
927 936 User.current = User.anonymous
928 937 groups = Issue.by_tracker(Project.find(1))
929 938 assert_equal 3, groups.size
930 939 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
931 940 end
932 941
933 942 test "#by_version" do
934 943 User.current = User.anonymous
935 944 groups = Issue.by_version(Project.find(1))
936 945 assert_equal 3, groups.size
937 946 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
938 947 end
939 948
940 949 test "#by_priority" do
941 950 User.current = User.anonymous
942 951 groups = Issue.by_priority(Project.find(1))
943 952 assert_equal 4, groups.size
944 953 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
945 954 end
946 955
947 956 test "#by_category" do
948 957 User.current = User.anonymous
949 958 groups = Issue.by_category(Project.find(1))
950 959 assert_equal 2, groups.size
951 960 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
952 961 end
953 962
954 963 test "#by_assigned_to" do
955 964 User.current = User.anonymous
956 965 groups = Issue.by_assigned_to(Project.find(1))
957 966 assert_equal 2, groups.size
958 967 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
959 968 end
960 969
961 970 test "#by_author" do
962 971 User.current = User.anonymous
963 972 groups = Issue.by_author(Project.find(1))
964 973 assert_equal 4, groups.size
965 974 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
966 975 end
967 976
968 977 test "#by_subproject" do
969 978 User.current = User.anonymous
970 979 groups = Issue.by_subproject(Project.find(1))
971 980 # Private descendant not visible
972 981 assert_equal 1, groups.size
973 982 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
974 983 end
975 984
976 985 context ".allowed_target_projects_on_move" do
977 986 should "return all active projects for admin users" do
978 987 User.current = User.find(1)
979 988 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
980 989 end
981 990
982 991 should "return allowed projects for non admin users" do
983 992 User.current = User.find(2)
984 993 Role.non_member.remove_permission! :move_issues
985 994 assert_equal 3, Issue.allowed_target_projects_on_move.size
986 995
987 996 Role.non_member.add_permission! :move_issues
988 997 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
989 998 end
990 999 end
991 1000
992 1001 def test_recently_updated_with_limit_scopes
993 1002 #should return the last updated issue
994 1003 assert_equal 1, Issue.recently_updated.with_limit(1).length
995 1004 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
996 1005 end
997 1006
998 1007 def test_on_active_projects_scope
999 1008 assert Project.find(2).archive
1000 1009
1001 1010 before = Issue.on_active_project.length
1002 1011 # test inclusion to results
1003 1012 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1004 1013 assert_equal before + 1, Issue.on_active_project.length
1005 1014
1006 1015 # Move to an archived project
1007 1016 issue.project = Project.find(2)
1008 1017 assert issue.save
1009 1018 assert_equal before, Issue.on_active_project.length
1010 1019 end
1011 1020
1012 1021 context "Issue#recipients" do
1013 1022 setup do
1014 1023 @project = Project.find(1)
1015 1024 @author = User.generate_with_protected!
1016 1025 @assignee = User.generate_with_protected!
1017 1026 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1018 1027 end
1019 1028
1020 1029 should "include project recipients" do
1021 1030 assert @project.recipients.present?
1022 1031 @project.recipients.each do |project_recipient|
1023 1032 assert @issue.recipients.include?(project_recipient)
1024 1033 end
1025 1034 end
1026 1035
1027 1036 should "include the author if the author is active" do
1028 1037 assert @issue.author, "No author set for Issue"
1029 1038 assert @issue.recipients.include?(@issue.author.mail)
1030 1039 end
1031 1040
1032 1041 should "include the assigned to user if the assigned to user is active" do
1033 1042 assert @issue.assigned_to, "No assigned_to set for Issue"
1034 1043 assert @issue.recipients.include?(@issue.assigned_to.mail)
1035 1044 end
1036 1045
1037 1046 should "not include users who opt out of all email" do
1038 1047 @author.update_attribute(:mail_notification, :none)
1039 1048
1040 1049 assert !@issue.recipients.include?(@issue.author.mail)
1041 1050 end
1042 1051
1043 1052 should "not include the issue author if they are only notified of assigned issues" do
1044 1053 @author.update_attribute(:mail_notification, :only_assigned)
1045 1054
1046 1055 assert !@issue.recipients.include?(@issue.author.mail)
1047 1056 end
1048 1057
1049 1058 should "not include the assigned user if they are only notified of owned issues" do
1050 1059 @assignee.update_attribute(:mail_notification, :only_owner)
1051 1060
1052 1061 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1053 1062 end
1054 1063
1055 1064 end
1056 1065 end
General Comments 0
You need to be logged in to leave comments. Login now