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