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