##// END OF EJS Templates
Merged r4414 from trunk....
Jean-Philippe Lang -
r4301:dbb26b08f804
parent child
Show More
@@ -1,840 +1,840
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 => "#{self.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
70 70 before_create :default_assign
71 71 before_save :close_duplicates, :update_done_ratio_from_issue_status
72 72 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
73 73 after_destroy :destroy_children
74 74 after_destroy :update_parent_attributes
75 75
76 76 # Returns true if usr or current user is allowed to view the issue
77 77 def visible?(usr=nil)
78 78 (usr || User.current).allowed_to?(:view_issues, self.project)
79 79 end
80 80
81 81 def after_initialize
82 82 if new_record?
83 83 # set default values for new records only
84 84 self.status ||= IssueStatus.default
85 85 self.priority ||= IssuePriority.default
86 86 end
87 87 end
88 88
89 89 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
90 90 def available_custom_fields
91 91 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
92 92 end
93 93
94 94 def copy_from(arg)
95 95 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
96 96 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
97 97 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
98 98 self.status = issue.status
99 99 self
100 100 end
101 101
102 102 # Moves/copies an issue to a new project and tracker
103 103 # Returns the moved/copied issue on success, false on failure
104 104 def move_to_project(*args)
105 105 ret = Issue.transaction do
106 106 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
107 107 end || false
108 108 end
109 109
110 110 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
111 111 options ||= {}
112 112 issue = options[:copy] ? self.class.new.copy_from(self) : self
113 113
114 114 if new_project && issue.project_id != new_project.id
115 115 # delete issue relations
116 116 unless Setting.cross_project_issue_relations?
117 117 issue.relations_from.clear
118 118 issue.relations_to.clear
119 119 end
120 120 # issue is moved to another project
121 121 # reassign to the category with same name if any
122 122 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
123 123 issue.category = new_category
124 124 # Keep the fixed_version if it's still valid in the new_project
125 125 unless new_project.shared_versions.include?(issue.fixed_version)
126 126 issue.fixed_version = nil
127 127 end
128 128 issue.project = new_project
129 129 if issue.parent && issue.parent.project_id != issue.project_id
130 130 issue.parent_issue_id = nil
131 131 end
132 132 end
133 133 if new_tracker
134 134 issue.tracker = new_tracker
135 135 issue.reset_custom_values!
136 136 end
137 137 if options[:copy]
138 138 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
139 139 issue.status = if options[:attributes] && options[:attributes][:status_id]
140 140 IssueStatus.find_by_id(options[:attributes][:status_id])
141 141 else
142 142 self.status
143 143 end
144 144 end
145 145 # Allow bulk setting of attributes on the issue
146 146 if options[:attributes]
147 147 issue.attributes = options[:attributes]
148 148 end
149 149 if issue.save
150 150 unless options[:copy]
151 151 # Manually update project_id on related time entries
152 152 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
153 153
154 154 issue.children.each do |child|
155 155 unless child.move_to_project_without_transaction(new_project)
156 156 # Move failed and transaction was rollback'd
157 157 return false
158 158 end
159 159 end
160 160 end
161 161 else
162 162 return false
163 163 end
164 164 issue
165 165 end
166 166
167 167 def status_id=(sid)
168 168 self.status = nil
169 169 write_attribute(:status_id, sid)
170 170 end
171 171
172 172 def priority_id=(pid)
173 173 self.priority = nil
174 174 write_attribute(:priority_id, pid)
175 175 end
176 176
177 177 def tracker_id=(tid)
178 178 self.tracker = nil
179 179 result = write_attribute(:tracker_id, tid)
180 180 @custom_field_values = nil
181 181 result
182 182 end
183 183
184 184 # Overrides attributes= so that tracker_id gets assigned first
185 185 def attributes_with_tracker_first=(new_attributes, *args)
186 186 return if new_attributes.nil?
187 187 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
188 188 if new_tracker_id
189 189 self.tracker_id = new_tracker_id
190 190 end
191 191 send :attributes_without_tracker_first=, new_attributes, *args
192 192 end
193 193 # Do not redefine alias chain on reload (see #4838)
194 194 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
195 195
196 196 def estimated_hours=(h)
197 197 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
198 198 end
199 199
200 200 SAFE_ATTRIBUTES = %w(
201 201 tracker_id
202 202 status_id
203 203 parent_issue_id
204 204 category_id
205 205 assigned_to_id
206 206 priority_id
207 207 fixed_version_id
208 208 subject
209 209 description
210 210 start_date
211 211 due_date
212 212 done_ratio
213 213 estimated_hours
214 214 custom_field_values
215 215 lock_version
216 216 ) unless const_defined?(:SAFE_ATTRIBUTES)
217 217
218 218 # Safely sets attributes
219 219 # Should be called from controllers instead of #attributes=
220 220 # attr_accessible is too rough because we still want things like
221 221 # Issue.new(:project => foo) to work
222 222 # TODO: move workflow/permission checks from controllers to here
223 223 def safe_attributes=(attrs, user=User.current)
224 224 return if attrs.nil?
225 225 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
226 226 if attrs['status_id']
227 227 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
228 228 attrs.delete('status_id')
229 229 end
230 230 end
231 231
232 232 unless leaf?
233 233 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
234 234 end
235 235
236 236 if attrs.has_key?('parent_issue_id')
237 237 if !user.allowed_to?(:manage_subtasks, project)
238 238 attrs.delete('parent_issue_id')
239 239 elsif !attrs['parent_issue_id'].blank?
240 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
240 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
241 241 end
242 242 end
243 243
244 244 self.attributes = attrs
245 245 end
246 246
247 247 def done_ratio
248 248 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
249 249 status.default_done_ratio
250 250 else
251 251 read_attribute(:done_ratio)
252 252 end
253 253 end
254 254
255 255 def self.use_status_for_done_ratio?
256 256 Setting.issue_done_ratio == 'issue_status'
257 257 end
258 258
259 259 def self.use_field_for_done_ratio?
260 260 Setting.issue_done_ratio == 'issue_field'
261 261 end
262 262
263 263 def validate
264 264 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
265 265 errors.add :due_date, :not_a_date
266 266 end
267 267
268 268 if self.due_date and self.start_date and self.due_date < self.start_date
269 269 errors.add :due_date, :greater_than_start_date
270 270 end
271 271
272 272 if start_date && soonest_start && start_date < soonest_start
273 273 errors.add :start_date, :invalid
274 274 end
275 275
276 276 if fixed_version
277 277 if !assignable_versions.include?(fixed_version)
278 278 errors.add :fixed_version_id, :inclusion
279 279 elsif reopened? && fixed_version.closed?
280 280 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
281 281 end
282 282 end
283 283
284 284 # Checks that the issue can not be added/moved to a disabled tracker
285 285 if project && (tracker_id_changed? || project_id_changed?)
286 286 unless project.trackers.include?(tracker)
287 287 errors.add :tracker_id, :inclusion
288 288 end
289 289 end
290 290
291 291 # Checks parent issue assignment
292 292 if @parent_issue
293 293 if @parent_issue.project_id != project_id
294 294 errors.add :parent_issue_id, :not_same_project
295 295 elsif !new_record?
296 296 # moving an existing issue
297 297 if @parent_issue.root_id != root_id
298 298 # we can always move to another tree
299 299 elsif move_possible?(@parent_issue)
300 300 # move accepted inside tree
301 301 else
302 302 errors.add :parent_issue_id, :not_a_valid_parent
303 303 end
304 304 end
305 305 end
306 306 end
307 307
308 308 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
309 309 # even if the user turns off the setting later
310 310 def update_done_ratio_from_issue_status
311 311 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
312 312 self.done_ratio = status.default_done_ratio
313 313 end
314 314 end
315 315
316 316 def init_journal(user, notes = "")
317 317 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
318 318 @issue_before_change = self.clone
319 319 @issue_before_change.status = self.status
320 320 @custom_values_before_change = {}
321 321 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
322 322 # Make sure updated_on is updated when adding a note.
323 323 updated_on_will_change!
324 324 @current_journal
325 325 end
326 326
327 327 # Return true if the issue is closed, otherwise false
328 328 def closed?
329 329 self.status.is_closed?
330 330 end
331 331
332 332 # Return true if the issue is being reopened
333 333 def reopened?
334 334 if !new_record? && status_id_changed?
335 335 status_was = IssueStatus.find_by_id(status_id_was)
336 336 status_new = IssueStatus.find_by_id(status_id)
337 337 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
338 338 return true
339 339 end
340 340 end
341 341 false
342 342 end
343 343
344 344 # Return true if the issue is being closed
345 345 def closing?
346 346 if !new_record? && status_id_changed?
347 347 status_was = IssueStatus.find_by_id(status_id_was)
348 348 status_new = IssueStatus.find_by_id(status_id)
349 349 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
350 350 return true
351 351 end
352 352 end
353 353 false
354 354 end
355 355
356 356 # Returns true if the issue is overdue
357 357 def overdue?
358 358 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
359 359 end
360 360
361 361 # Does this issue have children?
362 362 def children?
363 363 !leaf?
364 364 end
365 365
366 366 # Users the issue can be assigned to
367 367 def assignable_users
368 368 users = project.assignable_users
369 369 users << author if author
370 370 users.uniq.sort
371 371 end
372 372
373 373 # Versions that the issue can be assigned to
374 374 def assignable_versions
375 375 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
376 376 end
377 377
378 378 # Returns true if this issue is blocked by another issue that is still open
379 379 def blocked?
380 380 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
381 381 end
382 382
383 383 # Returns an array of status that user is able to apply
384 384 def new_statuses_allowed_to(user, include_default=false)
385 385 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
386 386 statuses << status unless statuses.empty?
387 387 statuses << IssueStatus.default if include_default
388 388 statuses = statuses.uniq.sort
389 389 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
390 390 end
391 391
392 392 # Returns the mail adresses of users that should be notified
393 393 def recipients
394 394 notified = project.notified_users
395 395 # Author and assignee are always notified unless they have been locked
396 396 notified << author if author && author.active?
397 397 notified << assigned_to if assigned_to && assigned_to.active?
398 398 notified.uniq!
399 399 # Remove users that can not view the issue
400 400 notified.reject! {|user| !visible?(user)}
401 401 notified.collect(&:mail)
402 402 end
403 403
404 404 # Returns the total number of hours spent on this issue and its descendants
405 405 #
406 406 # Example:
407 407 # spent_hours => 0.0
408 408 # spent_hours => 50.2
409 409 def spent_hours
410 410 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
411 411 end
412 412
413 413 def relations
414 414 (relations_from + relations_to).sort
415 415 end
416 416
417 417 def all_dependent_issues
418 418 dependencies = []
419 419 relations_from.each do |relation|
420 420 dependencies << relation.issue_to
421 421 dependencies += relation.issue_to.all_dependent_issues
422 422 end
423 423 dependencies
424 424 end
425 425
426 426 # Returns an array of issues that duplicate this one
427 427 def duplicates
428 428 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
429 429 end
430 430
431 431 # Returns the due date or the target due date if any
432 432 # Used on gantt chart
433 433 def due_before
434 434 due_date || (fixed_version ? fixed_version.effective_date : nil)
435 435 end
436 436
437 437 # Returns the time scheduled for this issue.
438 438 #
439 439 # Example:
440 440 # Start Date: 2/26/09, End Date: 3/04/09
441 441 # duration => 6
442 442 def duration
443 443 (start_date && due_date) ? due_date - start_date : 0
444 444 end
445 445
446 446 def soonest_start
447 447 @soonest_start ||= (
448 448 relations_to.collect{|relation| relation.successor_soonest_start} +
449 449 ancestors.collect(&:soonest_start)
450 450 ).compact.max
451 451 end
452 452
453 453 def reschedule_after(date)
454 454 return if date.nil?
455 455 if leaf?
456 456 if start_date.nil? || start_date < date
457 457 self.start_date, self.due_date = date, date + duration
458 458 save
459 459 end
460 460 else
461 461 leaves.each do |leaf|
462 462 leaf.reschedule_after(date)
463 463 end
464 464 end
465 465 end
466 466
467 467 def <=>(issue)
468 468 if issue.nil?
469 469 -1
470 470 elsif root_id != issue.root_id
471 471 (root_id || 0) <=> (issue.root_id || 0)
472 472 else
473 473 (lft || 0) <=> (issue.lft || 0)
474 474 end
475 475 end
476 476
477 477 def to_s
478 478 "#{tracker} ##{id}: #{subject}"
479 479 end
480 480
481 481 # Returns a string of css classes that apply to the issue
482 482 def css_classes
483 483 s = "issue status-#{status.position} priority-#{priority.position}"
484 484 s << ' closed' if closed?
485 485 s << ' overdue' if overdue?
486 486 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
487 487 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
488 488 s
489 489 end
490 490
491 491 # Saves an issue, time_entry, attachments, and a journal from the parameters
492 492 # Returns false if save fails
493 493 def save_issue_with_child_records(params, existing_time_entry=nil)
494 494 Issue.transaction do
495 495 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
496 496 @time_entry = existing_time_entry || TimeEntry.new
497 497 @time_entry.project = project
498 498 @time_entry.issue = self
499 499 @time_entry.user = User.current
500 500 @time_entry.spent_on = Date.today
501 501 @time_entry.attributes = params[:time_entry]
502 502 self.time_entries << @time_entry
503 503 end
504 504
505 505 if valid?
506 506 attachments = Attachment.attach_files(self, params[:attachments])
507 507
508 508 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
509 509 # TODO: Rename hook
510 510 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
511 511 begin
512 512 if save
513 513 # TODO: Rename hook
514 514 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
515 515 else
516 516 raise ActiveRecord::Rollback
517 517 end
518 518 rescue ActiveRecord::StaleObjectError
519 519 attachments[:files].each(&:destroy)
520 520 errors.add_to_base l(:notice_locking_conflict)
521 521 raise ActiveRecord::Rollback
522 522 end
523 523 end
524 524 end
525 525 end
526 526
527 527 # Unassigns issues from +version+ if it's no longer shared with issue's project
528 528 def self.update_versions_from_sharing_change(version)
529 529 # Update issues assigned to the version
530 530 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
531 531 end
532 532
533 533 # Unassigns issues from versions that are no longer shared
534 534 # after +project+ was moved
535 535 def self.update_versions_from_hierarchy_change(project)
536 536 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
537 537 # Update issues of the moved projects and issues assigned to a version of a moved project
538 538 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
539 539 end
540 540
541 541 def parent_issue_id=(arg)
542 542 parent_issue_id = arg.blank? ? nil : arg.to_i
543 543 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
544 544 @parent_issue.id
545 545 else
546 546 @parent_issue = nil
547 547 nil
548 548 end
549 549 end
550 550
551 551 def parent_issue_id
552 552 if instance_variable_defined? :@parent_issue
553 553 @parent_issue.nil? ? nil : @parent_issue.id
554 554 else
555 555 parent_id
556 556 end
557 557 end
558 558
559 559 # Extracted from the ReportsController.
560 560 def self.by_tracker(project)
561 561 count_and_group_by(:project => project,
562 562 :field => 'tracker_id',
563 563 :joins => Tracker.table_name)
564 564 end
565 565
566 566 def self.by_version(project)
567 567 count_and_group_by(:project => project,
568 568 :field => 'fixed_version_id',
569 569 :joins => Version.table_name)
570 570 end
571 571
572 572 def self.by_priority(project)
573 573 count_and_group_by(:project => project,
574 574 :field => 'priority_id',
575 575 :joins => IssuePriority.table_name)
576 576 end
577 577
578 578 def self.by_category(project)
579 579 count_and_group_by(:project => project,
580 580 :field => 'category_id',
581 581 :joins => IssueCategory.table_name)
582 582 end
583 583
584 584 def self.by_assigned_to(project)
585 585 count_and_group_by(:project => project,
586 586 :field => 'assigned_to_id',
587 587 :joins => User.table_name)
588 588 end
589 589
590 590 def self.by_author(project)
591 591 count_and_group_by(:project => project,
592 592 :field => 'author_id',
593 593 :joins => User.table_name)
594 594 end
595 595
596 596 def self.by_subproject(project)
597 597 ActiveRecord::Base.connection.select_all("select s.id as status_id,
598 598 s.is_closed as closed,
599 599 i.project_id as project_id,
600 600 count(i.id) as total
601 601 from
602 602 #{Issue.table_name} i, #{IssueStatus.table_name} s
603 603 where
604 604 i.status_id=s.id
605 605 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
606 606 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
607 607 end
608 608 # End ReportsController extraction
609 609
610 610 # Returns an array of projects that current user can move issues to
611 611 def self.allowed_target_projects_on_move
612 612 projects = []
613 613 if User.current.admin?
614 614 # admin is allowed to move issues to any active (visible) project
615 615 projects = Project.visible.all
616 616 elsif User.current.logged?
617 617 if Role.non_member.allowed_to?(:move_issues)
618 618 projects = Project.visible.all
619 619 else
620 620 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
621 621 end
622 622 end
623 623 projects
624 624 end
625 625
626 626 private
627 627
628 628 def update_nested_set_attributes
629 629 if root_id.nil?
630 630 # issue was just created
631 631 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
632 632 set_default_left_and_right
633 633 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
634 634 if @parent_issue
635 635 move_to_child_of(@parent_issue)
636 636 end
637 637 reload
638 638 elsif parent_issue_id != parent_id
639 639 former_parent_id = parent_id
640 640 # moving an existing issue
641 641 if @parent_issue && @parent_issue.root_id == root_id
642 642 # inside the same tree
643 643 move_to_child_of(@parent_issue)
644 644 else
645 645 # to another tree
646 646 unless root?
647 647 move_to_right_of(root)
648 648 reload
649 649 end
650 650 old_root_id = root_id
651 651 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
652 652 target_maxright = nested_set_scope.maximum(right_column_name) || 0
653 653 offset = target_maxright + 1 - lft
654 654 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
655 655 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
656 656 self[left_column_name] = lft + offset
657 657 self[right_column_name] = rgt + offset
658 658 if @parent_issue
659 659 move_to_child_of(@parent_issue)
660 660 end
661 661 end
662 662 reload
663 663 # delete invalid relations of all descendants
664 664 self_and_descendants.each do |issue|
665 665 issue.relations.each do |relation|
666 666 relation.destroy unless relation.valid?
667 667 end
668 668 end
669 669 # update former parent
670 670 recalculate_attributes_for(former_parent_id) if former_parent_id
671 671 end
672 672 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
673 673 end
674 674
675 675 def update_parent_attributes
676 676 recalculate_attributes_for(parent_id) if parent_id
677 677 end
678 678
679 679 def recalculate_attributes_for(issue_id)
680 680 if issue_id && p = Issue.find_by_id(issue_id)
681 681 # priority = highest priority of children
682 682 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
683 683 p.priority = IssuePriority.find_by_position(priority_position)
684 684 end
685 685
686 686 # start/due dates = lowest/highest dates of children
687 687 p.start_date = p.children.minimum(:start_date)
688 688 p.due_date = p.children.maximum(:due_date)
689 689 if p.start_date && p.due_date && p.due_date < p.start_date
690 690 p.start_date, p.due_date = p.due_date, p.start_date
691 691 end
692 692
693 693 # done ratio = weighted average ratio of leaves
694 694 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
695 695 leaves_count = p.leaves.count
696 696 if leaves_count > 0
697 697 average = p.leaves.average(:estimated_hours).to_f
698 698 if average == 0
699 699 average = 1
700 700 end
701 701 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
702 702 progress = done / (average * leaves_count)
703 703 p.done_ratio = progress.round
704 704 end
705 705 end
706 706
707 707 # estimate = sum of leaves estimates
708 708 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
709 709 p.estimated_hours = nil if p.estimated_hours == 0.0
710 710
711 711 # ancestors will be recursively updated
712 712 p.save(false)
713 713 end
714 714 end
715 715
716 716 def destroy_children
717 717 unless leaf?
718 718 children.each do |child|
719 719 child.destroy
720 720 end
721 721 end
722 722 end
723 723
724 724 # Update issues so their versions are not pointing to a
725 725 # fixed_version that is not shared with the issue's project
726 726 def self.update_versions(conditions=nil)
727 727 # Only need to update issues with a fixed_version from
728 728 # a different project and that is not systemwide shared
729 729 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
730 730 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
731 731 " AND #{Version.table_name}.sharing <> 'system'",
732 732 conditions),
733 733 :include => [:project, :fixed_version]
734 734 ).each do |issue|
735 735 next if issue.project.nil? || issue.fixed_version.nil?
736 736 unless issue.project.shared_versions.include?(issue.fixed_version)
737 737 issue.init_journal(User.current)
738 738 issue.fixed_version = nil
739 739 issue.save
740 740 end
741 741 end
742 742 end
743 743
744 744 # Callback on attachment deletion
745 745 def attachment_removed(obj)
746 746 journal = init_journal(User.current)
747 747 journal.details << JournalDetail.new(:property => 'attachment',
748 748 :prop_key => obj.id,
749 749 :old_value => obj.filename)
750 750 journal.save
751 751 end
752 752
753 753 # Default assignment based on category
754 754 def default_assign
755 755 if assigned_to.nil? && category && category.assigned_to
756 756 self.assigned_to = category.assigned_to
757 757 end
758 758 end
759 759
760 760 # Updates start/due dates of following issues
761 761 def reschedule_following_issues
762 762 if start_date_changed? || due_date_changed?
763 763 relations_from.each do |relation|
764 764 relation.set_issue_to_dates
765 765 end
766 766 end
767 767 end
768 768
769 769 # Closes duplicates if the issue is being closed
770 770 def close_duplicates
771 771 if closing?
772 772 duplicates.each do |duplicate|
773 773 # Reload is need in case the duplicate was updated by a previous duplicate
774 774 duplicate.reload
775 775 # Don't re-close it if it's already closed
776 776 next if duplicate.closed?
777 777 # Same user and notes
778 778 if @current_journal
779 779 duplicate.init_journal(@current_journal.user, @current_journal.notes)
780 780 end
781 781 duplicate.update_attribute :status, self.status
782 782 end
783 783 end
784 784 end
785 785
786 786 # Saves the changes in a Journal
787 787 # Called after_save
788 788 def create_journal
789 789 if @current_journal
790 790 # attributes changes
791 791 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
792 792 @current_journal.details << JournalDetail.new(:property => 'attr',
793 793 :prop_key => c,
794 794 :old_value => @issue_before_change.send(c),
795 795 :value => send(c)) unless send(c)==@issue_before_change.send(c)
796 796 }
797 797 # custom fields changes
798 798 custom_values.each {|c|
799 799 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
800 800 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
801 801 @current_journal.details << JournalDetail.new(:property => 'cf',
802 802 :prop_key => c.custom_field_id,
803 803 :old_value => @custom_values_before_change[c.custom_field_id],
804 804 :value => c.value)
805 805 }
806 806 @current_journal.save
807 807 # reset current journal
808 808 init_journal @current_journal.user, @current_journal.notes
809 809 end
810 810 end
811 811
812 812 # Query generator for selecting groups of issue counts for a project
813 813 # based on specific criteria
814 814 #
815 815 # Options
816 816 # * project - Project to search in.
817 817 # * field - String. Issue field to key off of in the grouping.
818 818 # * joins - String. The table name to join against.
819 819 def self.count_and_group_by(options)
820 820 project = options.delete(:project)
821 821 select_field = options.delete(:field)
822 822 joins = options.delete(:joins)
823 823
824 824 where = "i.#{select_field}=j.id"
825 825
826 826 ActiveRecord::Base.connection.select_all("select s.id as status_id,
827 827 s.is_closed as closed,
828 828 j.id as #{select_field},
829 829 count(i.id) as total
830 830 from
831 831 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
832 832 where
833 833 i.status_id=s.id
834 834 and #{where}
835 835 and i.project_id=#{project.id}
836 836 group by s.id, s.is_closed, j.id")
837 837 end
838 838
839 839
840 840 end
@@ -1,1092 +1,1106
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 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 require 'issues_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class IssuesController; def rescue_action(e) raise e end; end
23 23
24 24 class IssuesControllerTest < ActionController::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :member_roles,
30 30 :issues,
31 31 :issue_statuses,
32 32 :versions,
33 33 :trackers,
34 34 :projects_trackers,
35 35 :issue_categories,
36 36 :enabled_modules,
37 37 :enumerations,
38 38 :attachments,
39 39 :workflows,
40 40 :custom_fields,
41 41 :custom_values,
42 42 :custom_fields_projects,
43 43 :custom_fields_trackers,
44 44 :time_entries,
45 45 :journals,
46 46 :journal_details,
47 47 :queries
48 48
49 49 def setup
50 50 @controller = IssuesController.new
51 51 @request = ActionController::TestRequest.new
52 52 @response = ActionController::TestResponse.new
53 53 User.current = nil
54 54 end
55 55
56 56 def test_index
57 57 Setting.default_language = 'en'
58 58
59 59 get :index
60 60 assert_response :success
61 61 assert_template 'index.rhtml'
62 62 assert_not_nil assigns(:issues)
63 63 assert_nil assigns(:project)
64 64 assert_tag :tag => 'a', :content => /Can't print recipes/
65 65 assert_tag :tag => 'a', :content => /Subproject issue/
66 66 # private projects hidden
67 67 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
68 68 assert_no_tag :tag => 'a', :content => /Issue on project 2/
69 69 # project column
70 70 assert_tag :tag => 'th', :content => /Project/
71 71 end
72 72
73 73 def test_index_should_not_list_issues_when_module_disabled
74 74 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
75 75 get :index
76 76 assert_response :success
77 77 assert_template 'index.rhtml'
78 78 assert_not_nil assigns(:issues)
79 79 assert_nil assigns(:project)
80 80 assert_no_tag :tag => 'a', :content => /Can't print recipes/
81 81 assert_tag :tag => 'a', :content => /Subproject issue/
82 82 end
83 83
84 84 def test_index_should_not_list_issues_when_module_disabled
85 85 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
86 86 get :index
87 87 assert_response :success
88 88 assert_template 'index.rhtml'
89 89 assert_not_nil assigns(:issues)
90 90 assert_nil assigns(:project)
91 91 assert_no_tag :tag => 'a', :content => /Can't print recipes/
92 92 assert_tag :tag => 'a', :content => /Subproject issue/
93 93 end
94 94
95 95 def test_index_with_project
96 96 Setting.display_subprojects_issues = 0
97 97 get :index, :project_id => 1
98 98 assert_response :success
99 99 assert_template 'index.rhtml'
100 100 assert_not_nil assigns(:issues)
101 101 assert_tag :tag => 'a', :content => /Can't print recipes/
102 102 assert_no_tag :tag => 'a', :content => /Subproject issue/
103 103 end
104 104
105 105 def test_index_with_project_and_subprojects
106 106 Setting.display_subprojects_issues = 1
107 107 get :index, :project_id => 1
108 108 assert_response :success
109 109 assert_template 'index.rhtml'
110 110 assert_not_nil assigns(:issues)
111 111 assert_tag :tag => 'a', :content => /Can't print recipes/
112 112 assert_tag :tag => 'a', :content => /Subproject issue/
113 113 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
114 114 end
115 115
116 116 def test_index_with_project_and_subprojects_should_show_private_subprojects
117 117 @request.session[:user_id] = 2
118 118 Setting.display_subprojects_issues = 1
119 119 get :index, :project_id => 1
120 120 assert_response :success
121 121 assert_template 'index.rhtml'
122 122 assert_not_nil assigns(:issues)
123 123 assert_tag :tag => 'a', :content => /Can't print recipes/
124 124 assert_tag :tag => 'a', :content => /Subproject issue/
125 125 assert_tag :tag => 'a', :content => /Issue of a private subproject/
126 126 end
127 127
128 128 def test_index_with_project_and_filter
129 129 get :index, :project_id => 1, :set_filter => 1
130 130 assert_response :success
131 131 assert_template 'index.rhtml'
132 132 assert_not_nil assigns(:issues)
133 133 end
134 134
135 135 def test_index_with_query
136 136 get :index, :project_id => 1, :query_id => 5
137 137 assert_response :success
138 138 assert_template 'index.rhtml'
139 139 assert_not_nil assigns(:issues)
140 140 assert_nil assigns(:issue_count_by_group)
141 141 end
142 142
143 143 def test_index_with_query_grouped_by_tracker
144 144 get :index, :project_id => 1, :query_id => 6
145 145 assert_response :success
146 146 assert_template 'index.rhtml'
147 147 assert_not_nil assigns(:issues)
148 148 assert_not_nil assigns(:issue_count_by_group)
149 149 end
150 150
151 151 def test_index_with_query_grouped_by_list_custom_field
152 152 get :index, :project_id => 1, :query_id => 9
153 153 assert_response :success
154 154 assert_template 'index.rhtml'
155 155 assert_not_nil assigns(:issues)
156 156 assert_not_nil assigns(:issue_count_by_group)
157 157 end
158 158
159 159 def test_index_sort_by_field_not_included_in_columns
160 160 Setting.issue_list_default_columns = %w(subject author)
161 161 get :index, :sort => 'tracker'
162 162 end
163 163
164 164 def test_index_csv_with_project
165 165 Setting.default_language = 'en'
166 166
167 167 get :index, :format => 'csv'
168 168 assert_response :success
169 169 assert_not_nil assigns(:issues)
170 170 assert_equal 'text/csv', @response.content_type
171 171 assert @response.body.starts_with?("#,")
172 172
173 173 get :index, :project_id => 1, :format => 'csv'
174 174 assert_response :success
175 175 assert_not_nil assigns(:issues)
176 176 assert_equal 'text/csv', @response.content_type
177 177 end
178 178
179 179 def test_index_pdf
180 180 get :index, :format => 'pdf'
181 181 assert_response :success
182 182 assert_not_nil assigns(:issues)
183 183 assert_equal 'application/pdf', @response.content_type
184 184
185 185 get :index, :project_id => 1, :format => 'pdf'
186 186 assert_response :success
187 187 assert_not_nil assigns(:issues)
188 188 assert_equal 'application/pdf', @response.content_type
189 189
190 190 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
191 191 assert_response :success
192 192 assert_not_nil assigns(:issues)
193 193 assert_equal 'application/pdf', @response.content_type
194 194 end
195 195
196 196 def test_index_pdf_with_query_grouped_by_list_custom_field
197 197 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
198 198 assert_response :success
199 199 assert_not_nil assigns(:issues)
200 200 assert_not_nil assigns(:issue_count_by_group)
201 201 assert_equal 'application/pdf', @response.content_type
202 202 end
203 203
204 204 def test_index_sort
205 205 get :index, :sort => 'tracker,id:desc'
206 206 assert_response :success
207 207
208 208 sort_params = @request.session['issues_index_sort']
209 209 assert sort_params.is_a?(String)
210 210 assert_equal 'tracker,id:desc', sort_params
211 211
212 212 issues = assigns(:issues)
213 213 assert_not_nil issues
214 214 assert !issues.empty?
215 215 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
216 216 end
217 217
218 218 def test_index_with_columns
219 219 columns = ['tracker', 'subject', 'assigned_to']
220 220 get :index, :set_filter => 1, :query => { 'column_names' => columns}
221 221 assert_response :success
222 222
223 223 # query should use specified columns
224 224 query = assigns(:query)
225 225 assert_kind_of Query, query
226 226 assert_equal columns, query.column_names.map(&:to_s)
227 227
228 228 # columns should be stored in session
229 229 assert_kind_of Hash, session[:query]
230 230 assert_kind_of Array, session[:query][:column_names]
231 231 assert_equal columns, session[:query][:column_names].map(&:to_s)
232 232 end
233 233
234 234 def test_show_by_anonymous
235 235 get :show, :id => 1
236 236 assert_response :success
237 237 assert_template 'show.rhtml'
238 238 assert_not_nil assigns(:issue)
239 239 assert_equal Issue.find(1), assigns(:issue)
240 240
241 241 # anonymous role is allowed to add a note
242 242 assert_tag :tag => 'form',
243 243 :descendant => { :tag => 'fieldset',
244 244 :child => { :tag => 'legend',
245 245 :content => /Notes/ } }
246 246 end
247 247
248 248 def test_show_by_manager
249 249 @request.session[:user_id] = 2
250 250 get :show, :id => 1
251 251 assert_response :success
252 252
253 253 assert_tag :tag => 'form',
254 254 :descendant => { :tag => 'fieldset',
255 255 :child => { :tag => 'legend',
256 256 :content => /Change properties/ } },
257 257 :descendant => { :tag => 'fieldset',
258 258 :child => { :tag => 'legend',
259 259 :content => /Log time/ } },
260 260 :descendant => { :tag => 'fieldset',
261 261 :child => { :tag => 'legend',
262 262 :content => /Notes/ } }
263 263 end
264 264
265 265 def test_show_should_deny_anonymous_access_without_permission
266 266 Role.anonymous.remove_permission!(:view_issues)
267 267 get :show, :id => 1
268 268 assert_response :redirect
269 269 end
270 270
271 271 def test_show_should_deny_non_member_access_without_permission
272 272 Role.non_member.remove_permission!(:view_issues)
273 273 @request.session[:user_id] = 9
274 274 get :show, :id => 1
275 275 assert_response 403
276 276 end
277 277
278 278 def test_show_should_deny_member_access_without_permission
279 279 Role.find(1).remove_permission!(:view_issues)
280 280 @request.session[:user_id] = 2
281 281 get :show, :id => 1
282 282 assert_response 403
283 283 end
284 284
285 285 def test_show_should_not_disclose_relations_to_invisible_issues
286 286 Setting.cross_project_issue_relations = '1'
287 287 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
288 288 # Relation to a private project issue
289 289 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
290 290
291 291 get :show, :id => 1
292 292 assert_response :success
293 293
294 294 assert_tag :div, :attributes => { :id => 'relations' },
295 295 :descendant => { :tag => 'a', :content => /#2$/ }
296 296 assert_no_tag :div, :attributes => { :id => 'relations' },
297 297 :descendant => { :tag => 'a', :content => /#4$/ }
298 298 end
299 299
300 300 def test_show_atom
301 301 get :show, :id => 2, :format => 'atom'
302 302 assert_response :success
303 303 assert_template 'journals/index.rxml'
304 304 # Inline image
305 305 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
306 306 end
307 307
308 308 def test_show_export_to_pdf
309 309 get :show, :id => 3, :format => 'pdf'
310 310 assert_response :success
311 311 assert_equal 'application/pdf', @response.content_type
312 312 assert @response.body.starts_with?('%PDF')
313 313 assert_not_nil assigns(:issue)
314 314 end
315 315
316 316 def test_get_new
317 317 @request.session[:user_id] = 2
318 318 get :new, :project_id => 1, :tracker_id => 1
319 319 assert_response :success
320 320 assert_template 'new'
321 321
322 322 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
323 323 :value => 'Default string' }
324 324 end
325 325
326 326 def test_get_new_without_tracker_id
327 327 @request.session[:user_id] = 2
328 328 get :new, :project_id => 1
329 329 assert_response :success
330 330 assert_template 'new'
331 331
332 332 issue = assigns(:issue)
333 333 assert_not_nil issue
334 334 assert_equal Project.find(1).trackers.first, issue.tracker
335 335 end
336 336
337 337 def test_get_new_with_no_default_status_should_display_an_error
338 338 @request.session[:user_id] = 2
339 339 IssueStatus.delete_all
340 340
341 341 get :new, :project_id => 1
342 342 assert_response 500
343 343 assert_not_nil flash[:error]
344 344 assert_tag :tag => 'div', :attributes => { :class => /error/ },
345 345 :content => /No default issue/
346 346 end
347 347
348 348 def test_get_new_with_no_tracker_should_display_an_error
349 349 @request.session[:user_id] = 2
350 350 Tracker.delete_all
351 351
352 352 get :new, :project_id => 1
353 353 assert_response 500
354 354 assert_not_nil flash[:error]
355 355 assert_tag :tag => 'div', :attributes => { :class => /error/ },
356 356 :content => /No tracker/
357 357 end
358 358
359 359 def test_update_new_form
360 360 @request.session[:user_id] = 2
361 361 xhr :post, :new, :project_id => 1,
362 362 :issue => {:tracker_id => 2,
363 363 :subject => 'This is the test_new issue',
364 364 :description => 'This is the description',
365 365 :priority_id => 5}
366 366 assert_response :success
367 367 assert_template 'attributes'
368 368
369 369 issue = assigns(:issue)
370 370 assert_kind_of Issue, issue
371 371 assert_equal 1, issue.project_id
372 372 assert_equal 2, issue.tracker_id
373 373 assert_equal 'This is the test_new issue', issue.subject
374 374 end
375 375
376 376 def test_post_create
377 377 @request.session[:user_id] = 2
378 378 assert_difference 'Issue.count' do
379 379 post :create, :project_id => 1,
380 380 :issue => {:tracker_id => 3,
381 381 :status_id => 2,
382 382 :subject => 'This is the test_new issue',
383 383 :description => 'This is the description',
384 384 :priority_id => 5,
385 385 :start_date => '2010-11-07',
386 386 :estimated_hours => '',
387 387 :custom_field_values => {'2' => 'Value for field 2'}}
388 388 end
389 389 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
390 390
391 391 issue = Issue.find_by_subject('This is the test_new issue')
392 392 assert_not_nil issue
393 393 assert_equal 2, issue.author_id
394 394 assert_equal 3, issue.tracker_id
395 395 assert_equal 2, issue.status_id
396 396 assert_equal Date.parse('2010-11-07'), issue.start_date
397 397 assert_nil issue.estimated_hours
398 398 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
399 399 assert_not_nil v
400 400 assert_equal 'Value for field 2', v.value
401 401 end
402 402
403 403 def test_post_create_without_start_date
404 404 @request.session[:user_id] = 2
405 405 assert_difference 'Issue.count' do
406 406 post :create, :project_id => 1,
407 407 :issue => {:tracker_id => 3,
408 408 :status_id => 2,
409 409 :subject => 'This is the test_new issue',
410 410 :description => 'This is the description',
411 411 :priority_id => 5,
412 412 :start_date => '',
413 413 :estimated_hours => '',
414 414 :custom_field_values => {'2' => 'Value for field 2'}}
415 415 end
416 416 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
417 417
418 418 issue = Issue.find_by_subject('This is the test_new issue')
419 419 assert_not_nil issue
420 420 assert_nil issue.start_date
421 421 end
422 422
423 423 def test_post_create_and_continue
424 424 @request.session[:user_id] = 2
425 425 post :create, :project_id => 1,
426 426 :issue => {:tracker_id => 3,
427 427 :subject => 'This is first issue',
428 428 :priority_id => 5},
429 429 :continue => ''
430 430 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook',
431 431 :issue => {:tracker_id => 3}
432 432 end
433 433
434 434 def test_post_create_without_custom_fields_param
435 435 @request.session[:user_id] = 2
436 436 assert_difference 'Issue.count' do
437 437 post :create, :project_id => 1,
438 438 :issue => {:tracker_id => 1,
439 439 :subject => 'This is the test_new issue',
440 440 :description => 'This is the description',
441 441 :priority_id => 5}
442 442 end
443 443 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
444 444 end
445 445
446 446 def test_post_create_with_required_custom_field_and_without_custom_fields_param
447 447 field = IssueCustomField.find_by_name('Database')
448 448 field.update_attribute(:is_required, true)
449 449
450 450 @request.session[:user_id] = 2
451 451 post :create, :project_id => 1,
452 452 :issue => {:tracker_id => 1,
453 453 :subject => 'This is the test_new issue',
454 454 :description => 'This is the description',
455 455 :priority_id => 5}
456 456 assert_response :success
457 457 assert_template 'new'
458 458 issue = assigns(:issue)
459 459 assert_not_nil issue
460 460 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
461 461 end
462 462
463 463 def test_post_create_with_watchers
464 464 @request.session[:user_id] = 2
465 465 ActionMailer::Base.deliveries.clear
466 466
467 467 assert_difference 'Watcher.count', 2 do
468 468 post :create, :project_id => 1,
469 469 :issue => {:tracker_id => 1,
470 470 :subject => 'This is a new issue with watchers',
471 471 :description => 'This is the description',
472 472 :priority_id => 5,
473 473 :watcher_user_ids => ['2', '3']}
474 474 end
475 475 issue = Issue.find_by_subject('This is a new issue with watchers')
476 476 assert_not_nil issue
477 477 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
478 478
479 479 # Watchers added
480 480 assert_equal [2, 3], issue.watcher_user_ids.sort
481 481 assert issue.watched_by?(User.find(3))
482 482 # Watchers notified
483 483 mail = ActionMailer::Base.deliveries.last
484 484 assert_kind_of TMail::Mail, mail
485 485 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
486 486 end
487 487
488 488 def test_post_create_subissue
489 489 @request.session[:user_id] = 2
490 490
491 491 assert_difference 'Issue.count' do
492 492 post :create, :project_id => 1,
493 493 :issue => {:tracker_id => 1,
494 494 :subject => 'This is a child issue',
495 495 :parent_issue_id => 2}
496 496 end
497 497 issue = Issue.find_by_subject('This is a child issue')
498 498 assert_not_nil issue
499 499 assert_equal Issue.find(2), issue.parent
500 500 end
501 501
502 def test_post_create_subissue_with_non_numeric_parent_id
503 @request.session[:user_id] = 2
504
505 assert_difference 'Issue.count' do
506 post :create, :project_id => 1,
507 :issue => {:tracker_id => 1,
508 :subject => 'This is a child issue',
509 :parent_issue_id => 'ABC'}
510 end
511 issue = Issue.find_by_subject('This is a child issue')
512 assert_not_nil issue
513 assert_nil issue.parent
514 end
515
502 516 def test_post_create_should_send_a_notification
503 517 ActionMailer::Base.deliveries.clear
504 518 @request.session[:user_id] = 2
505 519 assert_difference 'Issue.count' do
506 520 post :create, :project_id => 1,
507 521 :issue => {:tracker_id => 3,
508 522 :subject => 'This is the test_new issue',
509 523 :description => 'This is the description',
510 524 :priority_id => 5,
511 525 :estimated_hours => '',
512 526 :custom_field_values => {'2' => 'Value for field 2'}}
513 527 end
514 528 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
515 529
516 530 assert_equal 1, ActionMailer::Base.deliveries.size
517 531 end
518 532
519 533 def test_post_create_should_preserve_fields_values_on_validation_failure
520 534 @request.session[:user_id] = 2
521 535 post :create, :project_id => 1,
522 536 :issue => {:tracker_id => 1,
523 537 # empty subject
524 538 :subject => '',
525 539 :description => 'This is a description',
526 540 :priority_id => 6,
527 541 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
528 542 assert_response :success
529 543 assert_template 'new'
530 544
531 545 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
532 546 :content => 'This is a description'
533 547 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
534 548 :child => { :tag => 'option', :attributes => { :selected => 'selected',
535 549 :value => '6' },
536 550 :content => 'High' }
537 551 # Custom fields
538 552 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
539 553 :child => { :tag => 'option', :attributes => { :selected => 'selected',
540 554 :value => 'Oracle' },
541 555 :content => 'Oracle' }
542 556 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
543 557 :value => 'Value for field 2'}
544 558 end
545 559
546 560 def test_post_create_should_ignore_non_safe_attributes
547 561 @request.session[:user_id] = 2
548 562 assert_nothing_raised do
549 563 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
550 564 end
551 565 end
552 566
553 567 context "without workflow privilege" do
554 568 setup do
555 569 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
556 570 Role.anonymous.add_permission! :add_issues
557 571 end
558 572
559 573 context "#new" do
560 574 should "propose default status only" do
561 575 get :new, :project_id => 1
562 576 assert_response :success
563 577 assert_template 'new'
564 578 assert_tag :tag => 'select',
565 579 :attributes => {:name => 'issue[status_id]'},
566 580 :children => {:count => 1},
567 581 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
568 582 end
569 583
570 584 should "accept default status" do
571 585 assert_difference 'Issue.count' do
572 586 post :create, :project_id => 1,
573 587 :issue => {:tracker_id => 1,
574 588 :subject => 'This is an issue',
575 589 :status_id => 1}
576 590 end
577 591 issue = Issue.last(:order => 'id')
578 592 assert_equal IssueStatus.default, issue.status
579 593 end
580 594
581 595 should "ignore unauthorized status" do
582 596 assert_difference 'Issue.count' do
583 597 post :create, :project_id => 1,
584 598 :issue => {:tracker_id => 1,
585 599 :subject => 'This is an issue',
586 600 :status_id => 3}
587 601 end
588 602 issue = Issue.last(:order => 'id')
589 603 assert_equal IssueStatus.default, issue.status
590 604 end
591 605 end
592 606 end
593 607
594 608 def test_copy_issue
595 609 @request.session[:user_id] = 2
596 610 get :new, :project_id => 1, :copy_from => 1
597 611 assert_template 'new'
598 612 assert_not_nil assigns(:issue)
599 613 orig = Issue.find(1)
600 614 assert_equal orig.subject, assigns(:issue).subject
601 615 end
602 616
603 617 def test_get_edit
604 618 @request.session[:user_id] = 2
605 619 get :edit, :id => 1
606 620 assert_response :success
607 621 assert_template 'edit'
608 622 assert_not_nil assigns(:issue)
609 623 assert_equal Issue.find(1), assigns(:issue)
610 624 end
611 625
612 626 def test_get_edit_with_params
613 627 @request.session[:user_id] = 2
614 628 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
615 629 assert_response :success
616 630 assert_template 'edit'
617 631
618 632 issue = assigns(:issue)
619 633 assert_not_nil issue
620 634
621 635 assert_equal 5, issue.status_id
622 636 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
623 637 :child => { :tag => 'option',
624 638 :content => 'Closed',
625 639 :attributes => { :selected => 'selected' } }
626 640
627 641 assert_equal 7, issue.priority_id
628 642 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
629 643 :child => { :tag => 'option',
630 644 :content => 'Urgent',
631 645 :attributes => { :selected => 'selected' } }
632 646 end
633 647
634 648 def test_update_edit_form
635 649 @request.session[:user_id] = 2
636 650 xhr :post, :new, :project_id => 1,
637 651 :id => 1,
638 652 :issue => {:tracker_id => 2,
639 653 :subject => 'This is the test_new issue',
640 654 :description => 'This is the description',
641 655 :priority_id => 5}
642 656 assert_response :success
643 657 assert_template 'attributes'
644 658
645 659 issue = assigns(:issue)
646 660 assert_kind_of Issue, issue
647 661 assert_equal 1, issue.id
648 662 assert_equal 1, issue.project_id
649 663 assert_equal 2, issue.tracker_id
650 664 assert_equal 'This is the test_new issue', issue.subject
651 665 end
652 666
653 667 def test_update_using_invalid_http_verbs
654 668 @request.session[:user_id] = 2
655 669 subject = 'Updated by an invalid http verb'
656 670
657 671 get :update, :id => 1, :issue => {:subject => subject}
658 672 assert_not_equal subject, Issue.find(1).subject
659 673
660 674 post :update, :id => 1, :issue => {:subject => subject}
661 675 assert_not_equal subject, Issue.find(1).subject
662 676
663 677 delete :update, :id => 1, :issue => {:subject => subject}
664 678 assert_not_equal subject, Issue.find(1).subject
665 679 end
666 680
667 681 def test_put_update_without_custom_fields_param
668 682 @request.session[:user_id] = 2
669 683 ActionMailer::Base.deliveries.clear
670 684
671 685 issue = Issue.find(1)
672 686 assert_equal '125', issue.custom_value_for(2).value
673 687 old_subject = issue.subject
674 688 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
675 689
676 690 assert_difference('Journal.count') do
677 691 assert_difference('JournalDetail.count', 2) do
678 692 put :update, :id => 1, :issue => {:subject => new_subject,
679 693 :priority_id => '6',
680 694 :category_id => '1' # no change
681 695 }
682 696 end
683 697 end
684 698 assert_redirected_to :action => 'show', :id => '1'
685 699 issue.reload
686 700 assert_equal new_subject, issue.subject
687 701 # Make sure custom fields were not cleared
688 702 assert_equal '125', issue.custom_value_for(2).value
689 703
690 704 mail = ActionMailer::Base.deliveries.last
691 705 assert_kind_of TMail::Mail, mail
692 706 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
693 707 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
694 708 end
695 709
696 710 def test_put_update_with_custom_field_change
697 711 @request.session[:user_id] = 2
698 712 issue = Issue.find(1)
699 713 assert_equal '125', issue.custom_value_for(2).value
700 714
701 715 assert_difference('Journal.count') do
702 716 assert_difference('JournalDetail.count', 3) do
703 717 put :update, :id => 1, :issue => {:subject => 'Custom field change',
704 718 :priority_id => '6',
705 719 :category_id => '1', # no change
706 720 :custom_field_values => { '2' => 'New custom value' }
707 721 }
708 722 end
709 723 end
710 724 assert_redirected_to :action => 'show', :id => '1'
711 725 issue.reload
712 726 assert_equal 'New custom value', issue.custom_value_for(2).value
713 727
714 728 mail = ActionMailer::Base.deliveries.last
715 729 assert_kind_of TMail::Mail, mail
716 730 assert mail.body.include?("Searchable field changed from 125 to New custom value")
717 731 end
718 732
719 733 def test_put_update_with_status_and_assignee_change
720 734 issue = Issue.find(1)
721 735 assert_equal 1, issue.status_id
722 736 @request.session[:user_id] = 2
723 737 assert_difference('TimeEntry.count', 0) do
724 738 put :update,
725 739 :id => 1,
726 740 :issue => { :status_id => 2, :assigned_to_id => 3 },
727 741 :notes => 'Assigned to dlopper',
728 742 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
729 743 end
730 744 assert_redirected_to :action => 'show', :id => '1'
731 745 issue.reload
732 746 assert_equal 2, issue.status_id
733 747 j = Journal.find(:first, :order => 'id DESC')
734 748 assert_equal 'Assigned to dlopper', j.notes
735 749 assert_equal 2, j.details.size
736 750
737 751 mail = ActionMailer::Base.deliveries.last
738 752 assert mail.body.include?("Status changed from New to Assigned")
739 753 # subject should contain the new status
740 754 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
741 755 end
742 756
743 757 def test_put_update_with_note_only
744 758 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
745 759 # anonymous user
746 760 put :update,
747 761 :id => 1,
748 762 :notes => notes
749 763 assert_redirected_to :action => 'show', :id => '1'
750 764 j = Journal.find(:first, :order => 'id DESC')
751 765 assert_equal notes, j.notes
752 766 assert_equal 0, j.details.size
753 767 assert_equal User.anonymous, j.user
754 768
755 769 mail = ActionMailer::Base.deliveries.last
756 770 assert mail.body.include?(notes)
757 771 end
758 772
759 773 def test_put_update_with_note_and_spent_time
760 774 @request.session[:user_id] = 2
761 775 spent_hours_before = Issue.find(1).spent_hours
762 776 assert_difference('TimeEntry.count') do
763 777 put :update,
764 778 :id => 1,
765 779 :notes => '2.5 hours added',
766 780 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
767 781 end
768 782 assert_redirected_to :action => 'show', :id => '1'
769 783
770 784 issue = Issue.find(1)
771 785
772 786 j = Journal.find(:first, :order => 'id DESC')
773 787 assert_equal '2.5 hours added', j.notes
774 788 assert_equal 0, j.details.size
775 789
776 790 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
777 791 assert_not_nil t
778 792 assert_equal 2.5, t.hours
779 793 assert_equal spent_hours_before + 2.5, issue.spent_hours
780 794 end
781 795
782 796 def test_put_update_with_attachment_only
783 797 set_tmp_attachments_directory
784 798
785 799 # Delete all fixtured journals, a race condition can occur causing the wrong
786 800 # journal to get fetched in the next find.
787 801 Journal.delete_all
788 802
789 803 # anonymous user
790 804 put :update,
791 805 :id => 1,
792 806 :notes => '',
793 807 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
794 808 assert_redirected_to :action => 'show', :id => '1'
795 809 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
796 810 assert j.notes.blank?
797 811 assert_equal 1, j.details.size
798 812 assert_equal 'testfile.txt', j.details.first.value
799 813 assert_equal User.anonymous, j.user
800 814
801 815 mail = ActionMailer::Base.deliveries.last
802 816 assert mail.body.include?('testfile.txt')
803 817 end
804 818
805 819 def test_put_update_with_attachment_that_fails_to_save
806 820 set_tmp_attachments_directory
807 821
808 822 # Delete all fixtured journals, a race condition can occur causing the wrong
809 823 # journal to get fetched in the next find.
810 824 Journal.delete_all
811 825
812 826 # Mock out the unsaved attachment
813 827 Attachment.any_instance.stubs(:create).returns(Attachment.new)
814 828
815 829 # anonymous user
816 830 put :update,
817 831 :id => 1,
818 832 :notes => '',
819 833 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
820 834 assert_redirected_to :action => 'show', :id => '1'
821 835 assert_equal '1 file(s) could not be saved.', flash[:warning]
822 836
823 837 end if Object.const_defined?(:Mocha)
824 838
825 839 def test_put_update_with_no_change
826 840 issue = Issue.find(1)
827 841 issue.journals.clear
828 842 ActionMailer::Base.deliveries.clear
829 843
830 844 put :update,
831 845 :id => 1,
832 846 :notes => ''
833 847 assert_redirected_to :action => 'show', :id => '1'
834 848
835 849 issue.reload
836 850 assert issue.journals.empty?
837 851 # No email should be sent
838 852 assert ActionMailer::Base.deliveries.empty?
839 853 end
840 854
841 855 def test_put_update_should_send_a_notification
842 856 @request.session[:user_id] = 2
843 857 ActionMailer::Base.deliveries.clear
844 858 issue = Issue.find(1)
845 859 old_subject = issue.subject
846 860 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
847 861
848 862 put :update, :id => 1, :issue => {:subject => new_subject,
849 863 :priority_id => '6',
850 864 :category_id => '1' # no change
851 865 }
852 866 assert_equal 1, ActionMailer::Base.deliveries.size
853 867 end
854 868
855 869 def test_put_update_with_invalid_spent_time
856 870 @request.session[:user_id] = 2
857 871 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
858 872
859 873 assert_no_difference('Journal.count') do
860 874 put :update,
861 875 :id => 1,
862 876 :notes => notes,
863 877 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
864 878 end
865 879 assert_response :success
866 880 assert_template 'edit'
867 881
868 882 assert_tag :textarea, :attributes => { :name => 'notes' },
869 883 :content => notes
870 884 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
871 885 end
872 886
873 887 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
874 888 issue = Issue.find(2)
875 889 @request.session[:user_id] = 2
876 890
877 891 put :update,
878 892 :id => issue.id,
879 893 :issue => {
880 894 :fixed_version_id => 4
881 895 }
882 896
883 897 assert_response :redirect
884 898 issue.reload
885 899 assert_equal 4, issue.fixed_version_id
886 900 assert_not_equal issue.project_id, issue.fixed_version.project_id
887 901 end
888 902
889 903 def test_put_update_should_redirect_back_using_the_back_url_parameter
890 904 issue = Issue.find(2)
891 905 @request.session[:user_id] = 2
892 906
893 907 put :update,
894 908 :id => issue.id,
895 909 :issue => {
896 910 :fixed_version_id => 4
897 911 },
898 912 :back_url => '/issues'
899 913
900 914 assert_response :redirect
901 915 assert_redirected_to '/issues'
902 916 end
903 917
904 918 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
905 919 issue = Issue.find(2)
906 920 @request.session[:user_id] = 2
907 921
908 922 put :update,
909 923 :id => issue.id,
910 924 :issue => {
911 925 :fixed_version_id => 4
912 926 },
913 927 :back_url => 'http://google.com'
914 928
915 929 assert_response :redirect
916 930 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
917 931 end
918 932
919 933 def test_get_bulk_edit
920 934 @request.session[:user_id] = 2
921 935 get :bulk_edit, :ids => [1, 2]
922 936 assert_response :success
923 937 assert_template 'bulk_edit'
924 938
925 939 # Project specific custom field, date type
926 940 field = CustomField.find(9)
927 941 assert !field.is_for_all?
928 942 assert_equal 'date', field.field_format
929 943 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
930 944
931 945 # System wide custom field
932 946 assert CustomField.find(1).is_for_all?
933 947 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
934 948 end
935 949
936 950 def test_bulk_update
937 951 @request.session[:user_id] = 2
938 952 # update issues priority
939 953 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
940 954 :issue => {:priority_id => 7,
941 955 :assigned_to_id => '',
942 956 :custom_field_values => {'2' => ''}}
943 957
944 958 assert_response 302
945 959 # check that the issues were updated
946 960 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
947 961
948 962 issue = Issue.find(1)
949 963 journal = issue.journals.find(:first, :order => 'created_on DESC')
950 964 assert_equal '125', issue.custom_value_for(2).value
951 965 assert_equal 'Bulk editing', journal.notes
952 966 assert_equal 1, journal.details.size
953 967 end
954 968
955 969 def test_bullk_update_should_send_a_notification
956 970 @request.session[:user_id] = 2
957 971 ActionMailer::Base.deliveries.clear
958 972 post(:bulk_update,
959 973 {
960 974 :ids => [1, 2],
961 975 :notes => 'Bulk editing',
962 976 :issue => {
963 977 :priority_id => 7,
964 978 :assigned_to_id => '',
965 979 :custom_field_values => {'2' => ''}
966 980 }
967 981 })
968 982
969 983 assert_response 302
970 984 assert_equal 2, ActionMailer::Base.deliveries.size
971 985 end
972 986
973 987 def test_bulk_update_status
974 988 @request.session[:user_id] = 2
975 989 # update issues priority
976 990 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
977 991 :issue => {:priority_id => '',
978 992 :assigned_to_id => '',
979 993 :status_id => '5'}
980 994
981 995 assert_response 302
982 996 issue = Issue.find(1)
983 997 assert issue.closed?
984 998 end
985 999
986 1000 def test_bulk_update_custom_field
987 1001 @request.session[:user_id] = 2
988 1002 # update issues priority
989 1003 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
990 1004 :issue => {:priority_id => '',
991 1005 :assigned_to_id => '',
992 1006 :custom_field_values => {'2' => '777'}}
993 1007
994 1008 assert_response 302
995 1009
996 1010 issue = Issue.find(1)
997 1011 journal = issue.journals.find(:first, :order => 'created_on DESC')
998 1012 assert_equal '777', issue.custom_value_for(2).value
999 1013 assert_equal 1, journal.details.size
1000 1014 assert_equal '125', journal.details.first.old_value
1001 1015 assert_equal '777', journal.details.first.value
1002 1016 end
1003 1017
1004 1018 def test_bulk_update_unassign
1005 1019 assert_not_nil Issue.find(2).assigned_to
1006 1020 @request.session[:user_id] = 2
1007 1021 # unassign issues
1008 1022 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
1009 1023 assert_response 302
1010 1024 # check that the issues were updated
1011 1025 assert_nil Issue.find(2).assigned_to
1012 1026 end
1013 1027
1014 1028 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
1015 1029 @request.session[:user_id] = 2
1016 1030
1017 1031 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
1018 1032
1019 1033 assert_response :redirect
1020 1034 issues = Issue.find([1,2])
1021 1035 issues.each do |issue|
1022 1036 assert_equal 4, issue.fixed_version_id
1023 1037 assert_not_equal issue.project_id, issue.fixed_version.project_id
1024 1038 end
1025 1039 end
1026 1040
1027 1041 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
1028 1042 @request.session[:user_id] = 2
1029 1043 post :bulk_update, :ids => [1,2], :back_url => '/issues'
1030 1044
1031 1045 assert_response :redirect
1032 1046 assert_redirected_to '/issues'
1033 1047 end
1034 1048
1035 1049 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1036 1050 @request.session[:user_id] = 2
1037 1051 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
1038 1052
1039 1053 assert_response :redirect
1040 1054 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1041 1055 end
1042 1056
1043 1057 def test_destroy_issue_with_no_time_entries
1044 1058 assert_nil TimeEntry.find_by_issue_id(2)
1045 1059 @request.session[:user_id] = 2
1046 1060 post :destroy, :id => 2
1047 1061 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1048 1062 assert_nil Issue.find_by_id(2)
1049 1063 end
1050 1064
1051 1065 def test_destroy_issues_with_time_entries
1052 1066 @request.session[:user_id] = 2
1053 1067 post :destroy, :ids => [1, 3]
1054 1068 assert_response :success
1055 1069 assert_template 'destroy'
1056 1070 assert_not_nil assigns(:hours)
1057 1071 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1058 1072 end
1059 1073
1060 1074 def test_destroy_issues_and_destroy_time_entries
1061 1075 @request.session[:user_id] = 2
1062 1076 post :destroy, :ids => [1, 3], :todo => 'destroy'
1063 1077 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1064 1078 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1065 1079 assert_nil TimeEntry.find_by_id([1, 2])
1066 1080 end
1067 1081
1068 1082 def test_destroy_issues_and_assign_time_entries_to_project
1069 1083 @request.session[:user_id] = 2
1070 1084 post :destroy, :ids => [1, 3], :todo => 'nullify'
1071 1085 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1072 1086 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1073 1087 assert_nil TimeEntry.find(1).issue_id
1074 1088 assert_nil TimeEntry.find(2).issue_id
1075 1089 end
1076 1090
1077 1091 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1078 1092 @request.session[:user_id] = 2
1079 1093 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1080 1094 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1081 1095 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1082 1096 assert_equal 2, TimeEntry.find(1).issue_id
1083 1097 assert_equal 2, TimeEntry.find(2).issue_id
1084 1098 end
1085 1099
1086 1100 def test_default_search_scope
1087 1101 get :index
1088 1102 assert_tag :div, :attributes => {:id => 'quick-search'},
1089 1103 :child => {:tag => 'form',
1090 1104 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1091 1105 end
1092 1106 end
General Comments 0
You need to be logged in to leave comments. Login now