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