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