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