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