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