##// END OF EJS Templates
Fixes Issue#save_issue_with_child_records so that time entry do not get saved if issue save fails....
Jean-Philippe Lang -
r3550:f35921d3080f
parent child
Show More
@@ -1,809 +1,810
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 :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
72 72 after_save :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.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 240 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
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 # Users the issue can be assigned to
362 362 def assignable_users
363 363 project.assignable_users
364 364 end
365 365
366 366 # Versions that the issue can be assigned to
367 367 def assignable_versions
368 368 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
369 369 end
370 370
371 371 # Returns true if this issue is blocked by another issue that is still open
372 372 def blocked?
373 373 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
374 374 end
375 375
376 376 # Returns an array of status that user is able to apply
377 377 def new_statuses_allowed_to(user, include_default=false)
378 378 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
379 379 statuses << status unless statuses.empty?
380 380 statuses << IssueStatus.default if include_default
381 381 statuses = statuses.uniq.sort
382 382 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
383 383 end
384 384
385 385 # Returns the mail adresses of users that should be notified
386 386 def recipients
387 387 notified = project.notified_users
388 388 # Author and assignee are always notified unless they have been locked
389 389 notified << author if author && author.active?
390 390 notified << assigned_to if assigned_to && assigned_to.active?
391 391 notified.uniq!
392 392 # Remove users that can not view the issue
393 393 notified.reject! {|user| !visible?(user)}
394 394 notified.collect(&:mail)
395 395 end
396 396
397 397 # Returns the total number of hours spent on this issue and its descendants
398 398 #
399 399 # Example:
400 400 # spent_hours => 0.0
401 401 # spent_hours => 50.2
402 402 def spent_hours
403 403 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
404 404 end
405 405
406 406 def relations
407 407 (relations_from + relations_to).sort
408 408 end
409 409
410 410 def all_dependent_issues
411 411 dependencies = []
412 412 relations_from.each do |relation|
413 413 dependencies << relation.issue_to
414 414 dependencies += relation.issue_to.all_dependent_issues
415 415 end
416 416 dependencies
417 417 end
418 418
419 419 # Returns an array of issues that duplicate this one
420 420 def duplicates
421 421 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
422 422 end
423 423
424 424 # Returns the due date or the target due date if any
425 425 # Used on gantt chart
426 426 def due_before
427 427 due_date || (fixed_version ? fixed_version.effective_date : nil)
428 428 end
429 429
430 430 # Returns the time scheduled for this issue.
431 431 #
432 432 # Example:
433 433 # Start Date: 2/26/09, End Date: 3/04/09
434 434 # duration => 6
435 435 def duration
436 436 (start_date && due_date) ? due_date - start_date : 0
437 437 end
438 438
439 439 def soonest_start
440 440 @soonest_start ||= (
441 441 relations_to.collect{|relation| relation.successor_soonest_start} +
442 442 ancestors.collect(&:soonest_start)
443 443 ).compact.max
444 444 end
445 445
446 446 def reschedule_after(date)
447 447 return if date.nil?
448 448 if leaf?
449 449 if start_date.nil? || start_date < date
450 450 self.start_date, self.due_date = date, date + duration
451 451 save
452 452 end
453 453 else
454 454 leaves.each do |leaf|
455 455 leaf.reschedule_after(date)
456 456 end
457 457 end
458 458 end
459 459
460 460 def <=>(issue)
461 461 if issue.nil?
462 462 -1
463 463 elsif root_id != issue.root_id
464 464 (root_id || 0) <=> (issue.root_id || 0)
465 465 else
466 466 (lft || 0) <=> (issue.lft || 0)
467 467 end
468 468 end
469 469
470 470 def to_s
471 471 "#{tracker} ##{id}: #{subject}"
472 472 end
473 473
474 474 # Returns a string of css classes that apply to the issue
475 475 def css_classes
476 476 s = "issue status-#{status.position} priority-#{priority.position}"
477 477 s << ' closed' if closed?
478 478 s << ' overdue' if overdue?
479 479 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
480 480 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
481 481 s
482 482 end
483 483
484 484 # Saves an issue, time_entry, attachments, and a journal from the parameters
485 485 # Returns false if save fails
486 486 def save_issue_with_child_records(params, existing_time_entry=nil)
487 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
488 @time_entry = existing_time_entry || TimeEntry.new
489 @time_entry.project = project
490 @time_entry.issue = self
491 @time_entry.user = User.current
492 @time_entry.spent_on = Date.today
493 @time_entry.attributes = params[:time_entry]
494 self.time_entries << @time_entry
495 end
496
497 if valid?
498 attachments = Attachment.attach_files(self, params[:attachments])
499
500 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
501 # TODO: Rename hook
502 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
503 begin
504 if save
505 # TODO: Rename hook
506 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
507 return true
508 else
509 return false
487 Issue.transaction do
488 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
489 @time_entry = existing_time_entry || TimeEntry.new
490 @time_entry.project = project
491 @time_entry.issue = self
492 @time_entry.user = User.current
493 @time_entry.spent_on = Date.today
494 @time_entry.attributes = params[:time_entry]
495 self.time_entries << @time_entry
496 end
497
498 if valid?
499 attachments = Attachment.attach_files(self, params[:attachments])
500
501 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
502 # TODO: Rename hook
503 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
504 begin
505 if save
506 # TODO: Rename hook
507 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
508 else
509 raise ActiveRecord::Rollback
510 end
511 rescue ActiveRecord::StaleObjectError
512 attachments[:files].each(&:destroy)
513 errors.add_to_base l(:notice_locking_conflict)
514 raise ActiveRecord::Rollback
510 515 end
511 rescue ActiveRecord::StaleObjectError
512 attachments[:files].each(&:destroy)
513 errors.add_to_base l(:notice_locking_conflict)
514 return false
515 516 end
516 517 end
517 518 end
518 519
519 520 # Unassigns issues from +version+ if it's no longer shared with issue's project
520 521 def self.update_versions_from_sharing_change(version)
521 522 # Update issues assigned to the version
522 523 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
523 524 end
524 525
525 526 # Unassigns issues from versions that are no longer shared
526 527 # after +project+ was moved
527 528 def self.update_versions_from_hierarchy_change(project)
528 529 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
529 530 # Update issues of the moved projects and issues assigned to a version of a moved project
530 531 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
531 532 end
532 533
533 534 def parent_issue_id=(arg)
534 535 parent_issue_id = arg.blank? ? nil : arg.to_i
535 536 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
536 537 @parent_issue.id
537 538 else
538 539 @parent_issue = nil
539 540 nil
540 541 end
541 542 end
542 543
543 544 def parent_issue_id
544 545 if instance_variable_defined? :@parent_issue
545 546 @parent_issue.nil? ? nil : @parent_issue.id
546 547 else
547 548 parent_id
548 549 end
549 550 end
550 551
551 552 # Extracted from the ReportsController.
552 553 def self.by_tracker(project)
553 554 count_and_group_by(:project => project,
554 555 :field => 'tracker_id',
555 556 :joins => Tracker.table_name)
556 557 end
557 558
558 559 def self.by_version(project)
559 560 count_and_group_by(:project => project,
560 561 :field => 'fixed_version_id',
561 562 :joins => Version.table_name)
562 563 end
563 564
564 565 def self.by_priority(project)
565 566 count_and_group_by(:project => project,
566 567 :field => 'priority_id',
567 568 :joins => IssuePriority.table_name)
568 569 end
569 570
570 571 def self.by_category(project)
571 572 count_and_group_by(:project => project,
572 573 :field => 'category_id',
573 574 :joins => IssueCategory.table_name)
574 575 end
575 576
576 577 def self.by_assigned_to(project)
577 578 count_and_group_by(:project => project,
578 579 :field => 'assigned_to_id',
579 580 :joins => User.table_name)
580 581 end
581 582
582 583 def self.by_author(project)
583 584 count_and_group_by(:project => project,
584 585 :field => 'author_id',
585 586 :joins => User.table_name)
586 587 end
587 588
588 589 def self.by_subproject(project)
589 590 ActiveRecord::Base.connection.select_all("select s.id as status_id,
590 591 s.is_closed as closed,
591 592 i.project_id as project_id,
592 593 count(i.id) as total
593 594 from
594 595 #{Issue.table_name} i, #{IssueStatus.table_name} s
595 596 where
596 597 i.status_id=s.id
597 598 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
598 599 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
599 600 end
600 601 # End ReportsController extraction
601 602
602 603 private
603 604
604 605 def update_nested_set_attributes
605 606 if root_id.nil?
606 607 # issue was just created
607 608 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
608 609 set_default_left_and_right
609 610 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
610 611 if @parent_issue
611 612 move_to_child_of(@parent_issue)
612 613 end
613 614 reload
614 615 elsif parent_issue_id != parent_id
615 616 # moving an existing issue
616 617 if @parent_issue && @parent_issue.root_id == root_id
617 618 # inside the same tree
618 619 move_to_child_of(@parent_issue)
619 620 else
620 621 # to another tree
621 622 unless root?
622 623 move_to_right_of(root)
623 624 reload
624 625 end
625 626 old_root_id = root_id
626 627 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
627 628 target_maxright = nested_set_scope.maximum(right_column_name) || 0
628 629 offset = target_maxright + 1 - lft
629 630 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
630 631 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
631 632 self[left_column_name] = lft + offset
632 633 self[right_column_name] = rgt + offset
633 634 if @parent_issue
634 635 move_to_child_of(@parent_issue)
635 636 end
636 637 end
637 638 reload
638 639 # delete invalid relations of all descendants
639 640 self_and_descendants.each do |issue|
640 641 issue.relations.each do |relation|
641 642 relation.destroy unless relation.valid?
642 643 end
643 644 end
644 645 end
645 646 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
646 647 end
647 648
648 649 def update_parent_attributes
649 650 if parent_id && p = Issue.find_by_id(parent_id)
650 651 # priority = highest priority of children
651 652 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
652 653 p.priority = IssuePriority.find_by_position(priority_position)
653 654 end
654 655
655 656 # start/due dates = lowest/highest dates of children
656 657 p.start_date = p.children.minimum(:start_date)
657 658 p.due_date = p.children.maximum(:due_date)
658 659 if p.start_date && p.due_date && p.due_date < p.start_date
659 660 p.start_date, p.due_date = p.due_date, p.start_date
660 661 end
661 662
662 663 # done ratio = weighted average ratio of leaves
663 664 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio?
664 665 leaves_count = p.leaves.count
665 666 if leaves_count > 0
666 667 average = p.leaves.average(:estimated_hours).to_f
667 668 if average == 0
668 669 average = 1
669 670 end
670 671 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
671 672 progress = done / (average * leaves_count)
672 673 p.done_ratio = progress.round
673 674 end
674 675 end
675 676
676 677 # estimate = sum of leaves estimates
677 678 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
678 679 p.estimated_hours = nil if p.estimated_hours == 0.0
679 680
680 681 # ancestors will be recursively updated
681 682 p.save(false)
682 683 end
683 684 end
684 685
685 686 def destroy_children
686 687 unless leaf?
687 688 children.each do |child|
688 689 child.destroy
689 690 end
690 691 end
691 692 end
692 693
693 694 # Update issues so their versions are not pointing to a
694 695 # fixed_version that is not shared with the issue's project
695 696 def self.update_versions(conditions=nil)
696 697 # Only need to update issues with a fixed_version from
697 698 # a different project and that is not systemwide shared
698 699 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
699 700 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
700 701 " AND #{Version.table_name}.sharing <> 'system'",
701 702 conditions),
702 703 :include => [:project, :fixed_version]
703 704 ).each do |issue|
704 705 next if issue.project.nil? || issue.fixed_version.nil?
705 706 unless issue.project.shared_versions.include?(issue.fixed_version)
706 707 issue.init_journal(User.current)
707 708 issue.fixed_version = nil
708 709 issue.save
709 710 end
710 711 end
711 712 end
712 713
713 714 # Callback on attachment deletion
714 715 def attachment_removed(obj)
715 716 journal = init_journal(User.current)
716 717 journal.details << JournalDetail.new(:property => 'attachment',
717 718 :prop_key => obj.id,
718 719 :old_value => obj.filename)
719 720 journal.save
720 721 end
721 722
722 723 # Default assignment based on category
723 724 def default_assign
724 725 if assigned_to.nil? && category && category.assigned_to
725 726 self.assigned_to = category.assigned_to
726 727 end
727 728 end
728 729
729 730 # Updates start/due dates of following issues
730 731 def reschedule_following_issues
731 732 if start_date_changed? || due_date_changed?
732 733 relations_from.each do |relation|
733 734 relation.set_issue_to_dates
734 735 end
735 736 end
736 737 end
737 738
738 739 # Closes duplicates if the issue is being closed
739 740 def close_duplicates
740 741 if closing?
741 742 duplicates.each do |duplicate|
742 743 # Reload is need in case the duplicate was updated by a previous duplicate
743 744 duplicate.reload
744 745 # Don't re-close it if it's already closed
745 746 next if duplicate.closed?
746 747 # Same user and notes
747 748 if @current_journal
748 749 duplicate.init_journal(@current_journal.user, @current_journal.notes)
749 750 end
750 751 duplicate.update_attribute :status, self.status
751 752 end
752 753 end
753 754 end
754 755
755 756 # Saves the changes in a Journal
756 757 # Called after_save
757 758 def create_journal
758 759 if @current_journal
759 760 # attributes changes
760 761 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
761 762 @current_journal.details << JournalDetail.new(:property => 'attr',
762 763 :prop_key => c,
763 764 :old_value => @issue_before_change.send(c),
764 765 :value => send(c)) unless send(c)==@issue_before_change.send(c)
765 766 }
766 767 # custom fields changes
767 768 custom_values.each {|c|
768 769 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
769 770 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
770 771 @current_journal.details << JournalDetail.new(:property => 'cf',
771 772 :prop_key => c.custom_field_id,
772 773 :old_value => @custom_values_before_change[c.custom_field_id],
773 774 :value => c.value)
774 775 }
775 776 @current_journal.save
776 777 # reset current journal
777 778 init_journal @current_journal.user, @current_journal.notes
778 779 end
779 780 end
780 781
781 782 # Query generator for selecting groups of issue counts for a project
782 783 # based on specific criteria
783 784 #
784 785 # Options
785 786 # * project - Project to search in.
786 787 # * field - String. Issue field to key off of in the grouping.
787 788 # * joins - String. The table name to join against.
788 789 def self.count_and_group_by(options)
789 790 project = options.delete(:project)
790 791 select_field = options.delete(:field)
791 792 joins = options.delete(:joins)
792 793
793 794 where = "i.#{select_field}=j.id"
794 795
795 796 ActiveRecord::Base.connection.select_all("select s.id as status_id,
796 797 s.is_closed as closed,
797 798 j.id as #{select_field},
798 799 count(i.id) as total
799 800 from
800 801 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} as j
801 802 where
802 803 i.status_id=s.id
803 804 and #{where}
804 805 and i.project_id=#{project.id}
805 806 group by s.id, s.is_closed, j.id")
806 807 end
807 808
808 809
809 810 end
@@ -1,1391 +1,1394
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_gantt
235 235 get :gantt, :project_id => 1
236 236 assert_response :success
237 237 assert_template 'gantt.rhtml'
238 238 assert_not_nil assigns(:gantt)
239 239 events = assigns(:gantt).events
240 240 assert_not_nil events
241 241 # Issue with start and due dates
242 242 i = Issue.find(1)
243 243 assert_not_nil i.due_date
244 244 assert events.include?(Issue.find(1))
245 245 # Issue with without due date but targeted to a version with date
246 246 i = Issue.find(2)
247 247 assert_nil i.due_date
248 248 assert events.include?(i)
249 249 end
250 250
251 251 def test_cross_project_gantt
252 252 get :gantt
253 253 assert_response :success
254 254 assert_template 'gantt.rhtml'
255 255 assert_not_nil assigns(:gantt)
256 256 events = assigns(:gantt).events
257 257 assert_not_nil events
258 258 end
259 259
260 260 def test_gantt_export_to_pdf
261 261 get :gantt, :project_id => 1, :format => 'pdf'
262 262 assert_response :success
263 263 assert_equal 'application/pdf', @response.content_type
264 264 assert @response.body.starts_with?('%PDF')
265 265 assert_not_nil assigns(:gantt)
266 266 end
267 267
268 268 def test_cross_project_gantt_export_to_pdf
269 269 get :gantt, :format => 'pdf'
270 270 assert_response :success
271 271 assert_equal 'application/pdf', @response.content_type
272 272 assert @response.body.starts_with?('%PDF')
273 273 assert_not_nil assigns(:gantt)
274 274 end
275 275
276 276 if Object.const_defined?(:Magick)
277 277 def test_gantt_image
278 278 get :gantt, :project_id => 1, :format => 'png'
279 279 assert_response :success
280 280 assert_equal 'image/png', @response.content_type
281 281 end
282 282 else
283 283 puts "RMagick not installed. Skipping tests !!!"
284 284 end
285 285
286 286 def test_calendar
287 287 get :calendar, :project_id => 1
288 288 assert_response :success
289 289 assert_template 'calendar'
290 290 assert_not_nil assigns(:calendar)
291 291 end
292 292
293 293 def test_cross_project_calendar
294 294 get :calendar
295 295 assert_response :success
296 296 assert_template 'calendar'
297 297 assert_not_nil assigns(:calendar)
298 298 end
299 299
300 300 def test_changes
301 301 get :changes, :project_id => 1
302 302 assert_response :success
303 303 assert_not_nil assigns(:journals)
304 304 assert_equal 'application/atom+xml', @response.content_type
305 305 end
306 306
307 307 def test_show_by_anonymous
308 308 get :show, :id => 1
309 309 assert_response :success
310 310 assert_template 'show.rhtml'
311 311 assert_not_nil assigns(:issue)
312 312 assert_equal Issue.find(1), assigns(:issue)
313 313
314 314 # anonymous role is allowed to add a note
315 315 assert_tag :tag => 'form',
316 316 :descendant => { :tag => 'fieldset',
317 317 :child => { :tag => 'legend',
318 318 :content => /Notes/ } }
319 319 end
320 320
321 321 def test_show_by_manager
322 322 @request.session[:user_id] = 2
323 323 get :show, :id => 1
324 324 assert_response :success
325 325
326 326 assert_tag :tag => 'form',
327 327 :descendant => { :tag => 'fieldset',
328 328 :child => { :tag => 'legend',
329 329 :content => /Change properties/ } },
330 330 :descendant => { :tag => 'fieldset',
331 331 :child => { :tag => 'legend',
332 332 :content => /Log time/ } },
333 333 :descendant => { :tag => 'fieldset',
334 334 :child => { :tag => 'legend',
335 335 :content => /Notes/ } }
336 336 end
337 337
338 338 def test_show_should_deny_anonymous_access_without_permission
339 339 Role.anonymous.remove_permission!(:view_issues)
340 340 get :show, :id => 1
341 341 assert_response :redirect
342 342 end
343 343
344 344 def test_show_should_deny_non_member_access_without_permission
345 345 Role.non_member.remove_permission!(:view_issues)
346 346 @request.session[:user_id] = 9
347 347 get :show, :id => 1
348 348 assert_response 403
349 349 end
350 350
351 351 def test_show_should_deny_member_access_without_permission
352 352 Role.find(1).remove_permission!(:view_issues)
353 353 @request.session[:user_id] = 2
354 354 get :show, :id => 1
355 355 assert_response 403
356 356 end
357 357
358 358 def test_show_should_not_disclose_relations_to_invisible_issues
359 359 Setting.cross_project_issue_relations = '1'
360 360 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
361 361 # Relation to a private project issue
362 362 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
363 363
364 364 get :show, :id => 1
365 365 assert_response :success
366 366
367 367 assert_tag :div, :attributes => { :id => 'relations' },
368 368 :descendant => { :tag => 'a', :content => /#2$/ }
369 369 assert_no_tag :div, :attributes => { :id => 'relations' },
370 370 :descendant => { :tag => 'a', :content => /#4$/ }
371 371 end
372 372
373 373 def test_show_atom
374 374 get :show, :id => 2, :format => 'atom'
375 375 assert_response :success
376 376 assert_template 'changes.rxml'
377 377 # Inline image
378 378 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
379 379 end
380 380
381 381 def test_show_export_to_pdf
382 382 get :show, :id => 3, :format => 'pdf'
383 383 assert_response :success
384 384 assert_equal 'application/pdf', @response.content_type
385 385 assert @response.body.starts_with?('%PDF')
386 386 assert_not_nil assigns(:issue)
387 387 end
388 388
389 389 def test_get_new
390 390 @request.session[:user_id] = 2
391 391 get :new, :project_id => 1, :tracker_id => 1
392 392 assert_response :success
393 393 assert_template 'new'
394 394
395 395 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
396 396 :value => 'Default string' }
397 397 end
398 398
399 399 def test_get_new_without_tracker_id
400 400 @request.session[:user_id] = 2
401 401 get :new, :project_id => 1
402 402 assert_response :success
403 403 assert_template 'new'
404 404
405 405 issue = assigns(:issue)
406 406 assert_not_nil issue
407 407 assert_equal Project.find(1).trackers.first, issue.tracker
408 408 end
409 409
410 410 def test_get_new_with_no_default_status_should_display_an_error
411 411 @request.session[:user_id] = 2
412 412 IssueStatus.delete_all
413 413
414 414 get :new, :project_id => 1
415 415 assert_response 500
416 416 assert_not_nil flash[:error]
417 417 assert_tag :tag => 'div', :attributes => { :class => /error/ },
418 418 :content => /No default issue/
419 419 end
420 420
421 421 def test_get_new_with_no_tracker_should_display_an_error
422 422 @request.session[:user_id] = 2
423 423 Tracker.delete_all
424 424
425 425 get :new, :project_id => 1
426 426 assert_response 500
427 427 assert_not_nil flash[:error]
428 428 assert_tag :tag => 'div', :attributes => { :class => /error/ },
429 429 :content => /No tracker/
430 430 end
431 431
432 432 def test_update_new_form
433 433 @request.session[:user_id] = 2
434 434 xhr :post, :update_form, :project_id => 1,
435 435 :issue => {:tracker_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 assert_response :success
440 440 assert_template 'attributes'
441 441
442 442 issue = assigns(:issue)
443 443 assert_kind_of Issue, issue
444 444 assert_equal 1, issue.project_id
445 445 assert_equal 2, issue.tracker_id
446 446 assert_equal 'This is the test_new issue', issue.subject
447 447 end
448 448
449 449 def test_post_new
450 450 @request.session[:user_id] = 2
451 451 assert_difference 'Issue.count' do
452 452 post :new, :project_id => 1,
453 453 :issue => {:tracker_id => 3,
454 454 :status_id => 2,
455 455 :subject => 'This is the test_new issue',
456 456 :description => 'This is the description',
457 457 :priority_id => 5,
458 458 :estimated_hours => '',
459 459 :custom_field_values => {'2' => 'Value for field 2'}}
460 460 end
461 461 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
462 462
463 463 issue = Issue.find_by_subject('This is the test_new issue')
464 464 assert_not_nil issue
465 465 assert_equal 2, issue.author_id
466 466 assert_equal 3, issue.tracker_id
467 467 assert_equal 2, issue.status_id
468 468 assert_nil issue.estimated_hours
469 469 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
470 470 assert_not_nil v
471 471 assert_equal 'Value for field 2', v.value
472 472 end
473 473
474 474 def test_post_new_and_continue
475 475 @request.session[:user_id] = 2
476 476 post :new, :project_id => 1,
477 477 :issue => {:tracker_id => 3,
478 478 :subject => 'This is first issue',
479 479 :priority_id => 5},
480 480 :continue => ''
481 481 assert_redirected_to :controller => 'issues', :action => 'new', :issue => {:tracker_id => 3}
482 482 end
483 483
484 484 def test_post_new_without_custom_fields_param
485 485 @request.session[:user_id] = 2
486 486 assert_difference 'Issue.count' do
487 487 post :new, :project_id => 1,
488 488 :issue => {:tracker_id => 1,
489 489 :subject => 'This is the test_new issue',
490 490 :description => 'This is the description',
491 491 :priority_id => 5}
492 492 end
493 493 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
494 494 end
495 495
496 496 def test_post_new_with_required_custom_field_and_without_custom_fields_param
497 497 field = IssueCustomField.find_by_name('Database')
498 498 field.update_attribute(:is_required, true)
499 499
500 500 @request.session[:user_id] = 2
501 501 post :new, :project_id => 1,
502 502 :issue => {:tracker_id => 1,
503 503 :subject => 'This is the test_new issue',
504 504 :description => 'This is the description',
505 505 :priority_id => 5}
506 506 assert_response :success
507 507 assert_template 'new'
508 508 issue = assigns(:issue)
509 509 assert_not_nil issue
510 510 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
511 511 end
512 512
513 513 def test_post_new_with_watchers
514 514 @request.session[:user_id] = 2
515 515 ActionMailer::Base.deliveries.clear
516 516
517 517 assert_difference 'Watcher.count', 2 do
518 518 post :new, :project_id => 1,
519 519 :issue => {:tracker_id => 1,
520 520 :subject => 'This is a new issue with watchers',
521 521 :description => 'This is the description',
522 522 :priority_id => 5,
523 523 :watcher_user_ids => ['2', '3']}
524 524 end
525 525 issue = Issue.find_by_subject('This is a new issue with watchers')
526 526 assert_not_nil issue
527 527 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
528 528
529 529 # Watchers added
530 530 assert_equal [2, 3], issue.watcher_user_ids.sort
531 531 assert issue.watched_by?(User.find(3))
532 532 # Watchers notified
533 533 mail = ActionMailer::Base.deliveries.last
534 534 assert_kind_of TMail::Mail, mail
535 535 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
536 536 end
537 537
538 538 def test_post_new_subissue
539 539 @request.session[:user_id] = 2
540 540
541 541 assert_difference 'Issue.count' do
542 542 post :new, :project_id => 1,
543 543 :issue => {:tracker_id => 1,
544 544 :subject => 'This is a child issue',
545 545 :parent_issue_id => 2}
546 546 end
547 547 issue = Issue.find_by_subject('This is a child issue')
548 548 assert_not_nil issue
549 549 assert_equal Issue.find(2), issue.parent
550 550 end
551 551
552 552 def test_post_new_should_send_a_notification
553 553 ActionMailer::Base.deliveries.clear
554 554 @request.session[:user_id] = 2
555 555 assert_difference 'Issue.count' do
556 556 post :new, :project_id => 1,
557 557 :issue => {:tracker_id => 3,
558 558 :subject => 'This is the test_new issue',
559 559 :description => 'This is the description',
560 560 :priority_id => 5,
561 561 :estimated_hours => '',
562 562 :custom_field_values => {'2' => 'Value for field 2'}}
563 563 end
564 564 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
565 565
566 566 assert_equal 1, ActionMailer::Base.deliveries.size
567 567 end
568 568
569 569 def test_post_should_preserve_fields_values_on_validation_failure
570 570 @request.session[:user_id] = 2
571 571 post :new, :project_id => 1,
572 572 :issue => {:tracker_id => 1,
573 573 # empty subject
574 574 :subject => '',
575 575 :description => 'This is a description',
576 576 :priority_id => 6,
577 577 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
578 578 assert_response :success
579 579 assert_template 'new'
580 580
581 581 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
582 582 :content => 'This is a description'
583 583 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
584 584 :child => { :tag => 'option', :attributes => { :selected => 'selected',
585 585 :value => '6' },
586 586 :content => 'High' }
587 587 # Custom fields
588 588 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
589 589 :child => { :tag => 'option', :attributes => { :selected => 'selected',
590 590 :value => 'Oracle' },
591 591 :content => 'Oracle' }
592 592 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
593 593 :value => 'Value for field 2'}
594 594 end
595 595
596 596 def test_post_new_should_ignore_non_safe_attributes
597 597 @request.session[:user_id] = 2
598 598 assert_nothing_raised do
599 599 post :new, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
600 600 end
601 601 end
602 602
603 603 context "without workflow privilege" do
604 604 setup do
605 605 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
606 606 Role.anonymous.add_permission! :add_issues
607 607 end
608 608
609 609 context "#new" do
610 610 should "propose default status only" do
611 611 get :new, :project_id => 1
612 612 assert_response :success
613 613 assert_template 'new'
614 614 assert_tag :tag => 'select',
615 615 :attributes => {:name => 'issue[status_id]'},
616 616 :children => {:count => 1},
617 617 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
618 618 end
619 619
620 620 should "accept default status" do
621 621 assert_difference 'Issue.count' do
622 622 post :new, :project_id => 1,
623 623 :issue => {:tracker_id => 1,
624 624 :subject => 'This is an issue',
625 625 :status_id => 1}
626 626 end
627 627 issue = Issue.last(:order => 'id')
628 628 assert_equal IssueStatus.default, issue.status
629 629 end
630 630
631 631 should "ignore unauthorized status" do
632 632 assert_difference 'Issue.count' do
633 633 post :new, :project_id => 1,
634 634 :issue => {:tracker_id => 1,
635 635 :subject => 'This is an issue',
636 636 :status_id => 3}
637 637 end
638 638 issue = Issue.last(:order => 'id')
639 639 assert_equal IssueStatus.default, issue.status
640 640 end
641 641 end
642 642 end
643 643
644 644 def test_copy_issue
645 645 @request.session[:user_id] = 2
646 646 get :new, :project_id => 1, :copy_from => 1
647 647 assert_template 'new'
648 648 assert_not_nil assigns(:issue)
649 649 orig = Issue.find(1)
650 650 assert_equal orig.subject, assigns(:issue).subject
651 651 end
652 652
653 653 def test_get_edit
654 654 @request.session[:user_id] = 2
655 655 get :edit, :id => 1
656 656 assert_response :success
657 657 assert_template 'edit'
658 658 assert_not_nil assigns(:issue)
659 659 assert_equal Issue.find(1), assigns(:issue)
660 660 end
661 661
662 662 def test_get_edit_with_params
663 663 @request.session[:user_id] = 2
664 664 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
665 665 assert_response :success
666 666 assert_template 'edit'
667 667
668 668 issue = assigns(:issue)
669 669 assert_not_nil issue
670 670
671 671 assert_equal 5, issue.status_id
672 672 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
673 673 :child => { :tag => 'option',
674 674 :content => 'Closed',
675 675 :attributes => { :selected => 'selected' } }
676 676
677 677 assert_equal 7, issue.priority_id
678 678 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
679 679 :child => { :tag => 'option',
680 680 :content => 'Urgent',
681 681 :attributes => { :selected => 'selected' } }
682 682 end
683 683
684 684 def test_update_edit_form
685 685 @request.session[:user_id] = 2
686 686 xhr :post, :update_form, :project_id => 1,
687 687 :id => 1,
688 688 :issue => {:tracker_id => 2,
689 689 :subject => 'This is the test_new issue',
690 690 :description => 'This is the description',
691 691 :priority_id => 5}
692 692 assert_response :success
693 693 assert_template 'attributes'
694 694
695 695 issue = assigns(:issue)
696 696 assert_kind_of Issue, issue
697 697 assert_equal 1, issue.id
698 698 assert_equal 1, issue.project_id
699 699 assert_equal 2, issue.tracker_id
700 700 assert_equal 'This is the test_new issue', issue.subject
701 701 end
702 702
703 703 def test_reply_to_issue
704 704 @request.session[:user_id] = 2
705 705 get :reply, :id => 1
706 706 assert_response :success
707 707 assert_select_rjs :show, "update"
708 708 end
709 709
710 710 def test_reply_to_note
711 711 @request.session[:user_id] = 2
712 712 get :reply, :id => 1, :journal_id => 2
713 713 assert_response :success
714 714 assert_select_rjs :show, "update"
715 715 end
716 716
717 717 def test_update_using_invalid_http_verbs
718 718 @request.session[:user_id] = 2
719 719 subject = 'Updated by an invalid http verb'
720 720
721 721 get :update, :id => 1, :issue => {:subject => subject}
722 722 assert_not_equal subject, Issue.find(1).subject
723 723
724 724 post :update, :id => 1, :issue => {:subject => subject}
725 725 assert_not_equal subject, Issue.find(1).subject
726 726
727 727 delete :update, :id => 1, :issue => {:subject => subject}
728 728 assert_not_equal subject, Issue.find(1).subject
729 729 end
730 730
731 731 def test_put_update_without_custom_fields_param
732 732 @request.session[:user_id] = 2
733 733 ActionMailer::Base.deliveries.clear
734 734
735 735 issue = Issue.find(1)
736 736 assert_equal '125', issue.custom_value_for(2).value
737 737 old_subject = issue.subject
738 738 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
739 739
740 740 assert_difference('Journal.count') do
741 741 assert_difference('JournalDetail.count', 2) do
742 742 put :update, :id => 1, :issue => {:subject => new_subject,
743 743 :priority_id => '6',
744 744 :category_id => '1' # no change
745 745 }
746 746 end
747 747 end
748 748 assert_redirected_to :action => 'show', :id => '1'
749 749 issue.reload
750 750 assert_equal new_subject, issue.subject
751 751 # Make sure custom fields were not cleared
752 752 assert_equal '125', issue.custom_value_for(2).value
753 753
754 754 mail = ActionMailer::Base.deliveries.last
755 755 assert_kind_of TMail::Mail, mail
756 756 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
757 757 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
758 758 end
759 759
760 760 def test_put_update_with_custom_field_change
761 761 @request.session[:user_id] = 2
762 762 issue = Issue.find(1)
763 763 assert_equal '125', issue.custom_value_for(2).value
764 764
765 765 assert_difference('Journal.count') do
766 766 assert_difference('JournalDetail.count', 3) do
767 767 put :update, :id => 1, :issue => {:subject => 'Custom field change',
768 768 :priority_id => '6',
769 769 :category_id => '1', # no change
770 770 :custom_field_values => { '2' => 'New custom value' }
771 771 }
772 772 end
773 773 end
774 774 assert_redirected_to :action => 'show', :id => '1'
775 775 issue.reload
776 776 assert_equal 'New custom value', issue.custom_value_for(2).value
777 777
778 778 mail = ActionMailer::Base.deliveries.last
779 779 assert_kind_of TMail::Mail, mail
780 780 assert mail.body.include?("Searchable field changed from 125 to New custom value")
781 781 end
782 782
783 783 def test_put_update_with_status_and_assignee_change
784 784 issue = Issue.find(1)
785 785 assert_equal 1, issue.status_id
786 786 @request.session[:user_id] = 2
787 787 assert_difference('TimeEntry.count', 0) do
788 788 put :update,
789 789 :id => 1,
790 790 :issue => { :status_id => 2, :assigned_to_id => 3 },
791 791 :notes => 'Assigned to dlopper',
792 792 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
793 793 end
794 794 assert_redirected_to :action => 'show', :id => '1'
795 795 issue.reload
796 796 assert_equal 2, issue.status_id
797 797 j = Journal.find(:first, :order => 'id DESC')
798 798 assert_equal 'Assigned to dlopper', j.notes
799 799 assert_equal 2, j.details.size
800 800
801 801 mail = ActionMailer::Base.deliveries.last
802 802 assert mail.body.include?("Status changed from New to Assigned")
803 803 # subject should contain the new status
804 804 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
805 805 end
806 806
807 807 def test_put_update_with_note_only
808 808 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
809 809 # anonymous user
810 810 put :update,
811 811 :id => 1,
812 812 :notes => notes
813 813 assert_redirected_to :action => 'show', :id => '1'
814 814 j = Journal.find(:first, :order => 'id DESC')
815 815 assert_equal notes, j.notes
816 816 assert_equal 0, j.details.size
817 817 assert_equal User.anonymous, j.user
818 818
819 819 mail = ActionMailer::Base.deliveries.last
820 820 assert mail.body.include?(notes)
821 821 end
822 822
823 823 def test_put_update_with_note_and_spent_time
824 824 @request.session[:user_id] = 2
825 825 spent_hours_before = Issue.find(1).spent_hours
826 826 assert_difference('TimeEntry.count') do
827 827 put :update,
828 828 :id => 1,
829 829 :notes => '2.5 hours added',
830 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
830 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first }
831 831 end
832 832 assert_redirected_to :action => 'show', :id => '1'
833 833
834 834 issue = Issue.find(1)
835 835
836 836 j = Journal.find(:first, :order => 'id DESC')
837 837 assert_equal '2.5 hours added', j.notes
838 838 assert_equal 0, j.details.size
839 839
840 t = issue.time_entries.find(:first, :order => 'id DESC')
840 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
841 841 assert_not_nil t
842 842 assert_equal 2.5, t.hours
843 843 assert_equal spent_hours_before + 2.5, issue.spent_hours
844 844 end
845 845
846 846 def test_put_update_with_attachment_only
847 847 set_tmp_attachments_directory
848 848
849 849 # Delete all fixtured journals, a race condition can occur causing the wrong
850 850 # journal to get fetched in the next find.
851 851 Journal.delete_all
852 852
853 853 # anonymous user
854 854 put :update,
855 855 :id => 1,
856 856 :notes => '',
857 857 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
858 858 assert_redirected_to :action => 'show', :id => '1'
859 859 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
860 860 assert j.notes.blank?
861 861 assert_equal 1, j.details.size
862 862 assert_equal 'testfile.txt', j.details.first.value
863 863 assert_equal User.anonymous, j.user
864 864
865 865 mail = ActionMailer::Base.deliveries.last
866 866 assert mail.body.include?('testfile.txt')
867 867 end
868 868
869 869 def test_put_update_with_attachment_that_fails_to_save
870 870 set_tmp_attachments_directory
871 871
872 872 # Delete all fixtured journals, a race condition can occur causing the wrong
873 873 # journal to get fetched in the next find.
874 874 Journal.delete_all
875 875
876 876 # Mock out the unsaved attachment
877 877 Attachment.any_instance.stubs(:create).returns(Attachment.new)
878 878
879 879 # anonymous user
880 880 put :update,
881 881 :id => 1,
882 882 :notes => '',
883 883 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
884 884 assert_redirected_to :action => 'show', :id => '1'
885 885 assert_equal '1 file(s) could not be saved.', flash[:warning]
886 886
887 887 end if Object.const_defined?(:Mocha)
888 888
889 889 def test_put_update_with_no_change
890 890 issue = Issue.find(1)
891 891 issue.journals.clear
892 892 ActionMailer::Base.deliveries.clear
893 893
894 894 put :update,
895 895 :id => 1,
896 896 :notes => ''
897 897 assert_redirected_to :action => 'show', :id => '1'
898 898
899 899 issue.reload
900 900 assert issue.journals.empty?
901 901 # No email should be sent
902 902 assert ActionMailer::Base.deliveries.empty?
903 903 end
904 904
905 905 def test_put_update_should_send_a_notification
906 906 @request.session[:user_id] = 2
907 907 ActionMailer::Base.deliveries.clear
908 908 issue = Issue.find(1)
909 909 old_subject = issue.subject
910 910 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
911 911
912 912 put :update, :id => 1, :issue => {:subject => new_subject,
913 913 :priority_id => '6',
914 914 :category_id => '1' # no change
915 915 }
916 916 assert_equal 1, ActionMailer::Base.deliveries.size
917 917 end
918 918
919 919 def test_put_update_with_invalid_spent_time
920 920 @request.session[:user_id] = 2
921 921 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
922 922
923 923 assert_no_difference('Journal.count') do
924 924 put :update,
925 925 :id => 1,
926 926 :notes => notes,
927 927 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
928 928 end
929 929 assert_response :success
930 930 assert_template 'edit'
931 931
932 932 assert_tag :textarea, :attributes => { :name => 'notes' },
933 933 :content => notes
934 934 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
935 935 end
936 936
937 937 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
938 938 issue = Issue.find(2)
939 939 @request.session[:user_id] = 2
940 940
941 941 put :update,
942 942 :id => issue.id,
943 943 :issue => {
944 944 :fixed_version_id => 4
945 945 }
946 946
947 947 assert_response :redirect
948 948 issue.reload
949 949 assert_equal 4, issue.fixed_version_id
950 950 assert_not_equal issue.project_id, issue.fixed_version.project_id
951 951 end
952 952
953 953 def test_put_update_should_redirect_back_using_the_back_url_parameter
954 954 issue = Issue.find(2)
955 955 @request.session[:user_id] = 2
956 956
957 957 put :update,
958 958 :id => issue.id,
959 959 :issue => {
960 960 :fixed_version_id => 4
961 961 },
962 962 :back_url => '/issues'
963 963
964 964 assert_response :redirect
965 965 assert_redirected_to '/issues'
966 966 end
967 967
968 968 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
969 969 issue = Issue.find(2)
970 970 @request.session[:user_id] = 2
971 971
972 972 put :update,
973 973 :id => issue.id,
974 974 :issue => {
975 975 :fixed_version_id => 4
976 976 },
977 977 :back_url => 'http://google.com'
978 978
979 979 assert_response :redirect
980 980 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
981 981 end
982 982
983 983 def test_put_update_stale_issue
984 984 issue = Issue.find(2)
985 985 @request.session[:user_id] = 2
986 986
987 987 assert_no_difference 'Journal.count' do
988 assert_no_difference 'Attachment.count' do
989 put :update,
990 :id => issue.id,
991 :issue => {
992 :fixed_version_id => 4,
993 :lock_version => (issue.lock_version - 1)
994 },
995 :notes => '',
996 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
988 assert_no_difference 'TimeEntry.count' do
989 assert_no_difference 'Attachment.count' do
990 put :update,
991 :id => issue.id,
992 :issue => {
993 :fixed_version_id => 4,
994 :lock_version => (issue.lock_version - 1)
995 },
996 :notes => '',
997 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}},
998 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
999 end
997 1000 end
998 1001 end
999 1002
1000 1003 assert_response :success
1001 1004 assert_template 'edit'
1002 1005 assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' },
1003 1006 :content => /Data has been updated by another user/
1004 1007 end
1005 1008
1006 1009 def test_get_bulk_edit
1007 1010 @request.session[:user_id] = 2
1008 1011 get :bulk_edit, :ids => [1, 2]
1009 1012 assert_response :success
1010 1013 assert_template 'bulk_edit'
1011 1014
1012 1015 # Project specific custom field, date type
1013 1016 field = CustomField.find(9)
1014 1017 assert !field.is_for_all?
1015 1018 assert_equal 'date', field.field_format
1016 1019 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1017 1020
1018 1021 # System wide custom field
1019 1022 assert CustomField.find(1).is_for_all?
1020 1023 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
1021 1024 end
1022 1025
1023 1026 def test_bulk_edit
1024 1027 @request.session[:user_id] = 2
1025 1028 # update issues priority
1026 1029 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk editing',
1027 1030 :issue => {:priority_id => 7,
1028 1031 :assigned_to_id => '',
1029 1032 :custom_field_values => {'2' => ''}}
1030 1033
1031 1034 assert_response 302
1032 1035 # check that the issues were updated
1033 1036 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
1034 1037
1035 1038 issue = Issue.find(1)
1036 1039 journal = issue.journals.find(:first, :order => 'created_on DESC')
1037 1040 assert_equal '125', issue.custom_value_for(2).value
1038 1041 assert_equal 'Bulk editing', journal.notes
1039 1042 assert_equal 1, journal.details.size
1040 1043 end
1041 1044
1042 1045 def test_bullk_edit_should_send_a_notification
1043 1046 @request.session[:user_id] = 2
1044 1047 ActionMailer::Base.deliveries.clear
1045 1048 post(:bulk_edit,
1046 1049 {
1047 1050 :ids => [1, 2],
1048 1051 :notes => 'Bulk editing',
1049 1052 :issue => {
1050 1053 :priority_id => 7,
1051 1054 :assigned_to_id => '',
1052 1055 :custom_field_values => {'2' => ''}
1053 1056 }
1054 1057 })
1055 1058
1056 1059 assert_response 302
1057 1060 assert_equal 2, ActionMailer::Base.deliveries.size
1058 1061 end
1059 1062
1060 1063 def test_bulk_edit_status
1061 1064 @request.session[:user_id] = 2
1062 1065 # update issues priority
1063 1066 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk editing status',
1064 1067 :issue => {:priority_id => '',
1065 1068 :assigned_to_id => '',
1066 1069 :status_id => '5'}
1067 1070
1068 1071 assert_response 302
1069 1072 issue = Issue.find(1)
1070 1073 assert issue.closed?
1071 1074 end
1072 1075
1073 1076 def test_bulk_edit_custom_field
1074 1077 @request.session[:user_id] = 2
1075 1078 # update issues priority
1076 1079 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk editing custom field',
1077 1080 :issue => {:priority_id => '',
1078 1081 :assigned_to_id => '',
1079 1082 :custom_field_values => {'2' => '777'}}
1080 1083
1081 1084 assert_response 302
1082 1085
1083 1086 issue = Issue.find(1)
1084 1087 journal = issue.journals.find(:first, :order => 'created_on DESC')
1085 1088 assert_equal '777', issue.custom_value_for(2).value
1086 1089 assert_equal 1, journal.details.size
1087 1090 assert_equal '125', journal.details.first.old_value
1088 1091 assert_equal '777', journal.details.first.value
1089 1092 end
1090 1093
1091 1094 def test_bulk_unassign
1092 1095 assert_not_nil Issue.find(2).assigned_to
1093 1096 @request.session[:user_id] = 2
1094 1097 # unassign issues
1095 1098 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
1096 1099 assert_response 302
1097 1100 # check that the issues were updated
1098 1101 assert_nil Issue.find(2).assigned_to
1099 1102 end
1100 1103
1101 1104 def test_post_bulk_edit_should_allow_fixed_version_to_be_set_to_a_subproject
1102 1105 @request.session[:user_id] = 2
1103 1106
1104 1107 post :bulk_edit, :ids => [1,2], :issue => {:fixed_version_id => 4}
1105 1108
1106 1109 assert_response :redirect
1107 1110 issues = Issue.find([1,2])
1108 1111 issues.each do |issue|
1109 1112 assert_equal 4, issue.fixed_version_id
1110 1113 assert_not_equal issue.project_id, issue.fixed_version.project_id
1111 1114 end
1112 1115 end
1113 1116
1114 1117 def test_post_bulk_edit_should_redirect_back_using_the_back_url_parameter
1115 1118 @request.session[:user_id] = 2
1116 1119 post :bulk_edit, :ids => [1,2], :back_url => '/issues'
1117 1120
1118 1121 assert_response :redirect
1119 1122 assert_redirected_to '/issues'
1120 1123 end
1121 1124
1122 1125 def test_post_bulk_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1123 1126 @request.session[:user_id] = 2
1124 1127 post :bulk_edit, :ids => [1,2], :back_url => 'http://google.com'
1125 1128
1126 1129 assert_response :redirect
1127 1130 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1128 1131 end
1129 1132
1130 1133 def test_move_one_issue_to_another_project
1131 1134 @request.session[:user_id] = 2
1132 1135 post :move, :id => 1, :new_project_id => 2, :tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1133 1136 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1134 1137 assert_equal 2, Issue.find(1).project_id
1135 1138 end
1136 1139
1137 1140 def test_move_one_issue_to_another_project_should_follow_when_needed
1138 1141 @request.session[:user_id] = 2
1139 1142 post :move, :id => 1, :new_project_id => 2, :follow => '1'
1140 1143 assert_redirected_to '/issues/1'
1141 1144 end
1142 1145
1143 1146 def test_bulk_move_to_another_project
1144 1147 @request.session[:user_id] = 2
1145 1148 post :move, :ids => [1, 2], :new_project_id => 2
1146 1149 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1147 1150 # Issues moved to project 2
1148 1151 assert_equal 2, Issue.find(1).project_id
1149 1152 assert_equal 2, Issue.find(2).project_id
1150 1153 # No tracker change
1151 1154 assert_equal 1, Issue.find(1).tracker_id
1152 1155 assert_equal 2, Issue.find(2).tracker_id
1153 1156 end
1154 1157
1155 1158 def test_bulk_move_to_another_tracker
1156 1159 @request.session[:user_id] = 2
1157 1160 post :move, :ids => [1, 2], :new_tracker_id => 2
1158 1161 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1159 1162 assert_equal 2, Issue.find(1).tracker_id
1160 1163 assert_equal 2, Issue.find(2).tracker_id
1161 1164 end
1162 1165
1163 1166 def test_bulk_copy_to_another_project
1164 1167 @request.session[:user_id] = 2
1165 1168 assert_difference 'Issue.count', 2 do
1166 1169 assert_no_difference 'Project.find(1).issues.count' do
1167 1170 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1168 1171 end
1169 1172 end
1170 1173 assert_redirected_to 'projects/ecookbook/issues'
1171 1174 end
1172 1175
1173 1176 context "#move via bulk copy" do
1174 1177 should "allow not changing the issue's attributes" do
1175 1178 @request.session[:user_id] = 2
1176 1179 issue_before_move = Issue.find(1)
1177 1180 assert_difference 'Issue.count', 1 do
1178 1181 assert_no_difference 'Project.find(1).issues.count' do
1179 1182 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1180 1183 end
1181 1184 end
1182 1185 issue_after_move = Issue.first(:order => 'id desc', :conditions => {:project_id => 2})
1183 1186 assert_equal issue_before_move.tracker_id, issue_after_move.tracker_id
1184 1187 assert_equal issue_before_move.status_id, issue_after_move.status_id
1185 1188 assert_equal issue_before_move.assigned_to_id, issue_after_move.assigned_to_id
1186 1189 end
1187 1190
1188 1191 should "allow changing the issue's attributes" do
1189 1192 @request.session[:user_id] = 2
1190 1193 assert_difference 'Issue.count', 2 do
1191 1194 assert_no_difference 'Project.find(1).issues.count' do
1192 1195 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => 4, :status_id => 3, :start_date => '2009-12-01', :due_date => '2009-12-31'
1193 1196 end
1194 1197 end
1195 1198
1196 1199 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
1197 1200 assert_equal 2, copied_issues.size
1198 1201 copied_issues.each do |issue|
1199 1202 assert_equal 2, issue.project_id, "Project is incorrect"
1200 1203 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
1201 1204 assert_equal 3, issue.status_id, "Status is incorrect"
1202 1205 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
1203 1206 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
1204 1207 end
1205 1208 end
1206 1209 end
1207 1210
1208 1211 def test_copy_to_another_project_should_follow_when_needed
1209 1212 @request.session[:user_id] = 2
1210 1213 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1211 1214 issue = Issue.first(:order => 'id DESC')
1212 1215 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1213 1216 end
1214 1217
1215 1218 def test_context_menu_one_issue
1216 1219 @request.session[:user_id] = 2
1217 1220 get :context_menu, :ids => [1]
1218 1221 assert_response :success
1219 1222 assert_template 'context_menu'
1220 1223 assert_tag :tag => 'a', :content => 'Edit',
1221 1224 :attributes => { :href => '/issues/1/edit',
1222 1225 :class => 'icon-edit' }
1223 1226 assert_tag :tag => 'a', :content => 'Closed',
1224 1227 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1225 1228 :class => '' }
1226 1229 assert_tag :tag => 'a', :content => 'Immediate',
1227 1230 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;issue%5Bpriority_id%5D=8',
1228 1231 :class => '' }
1229 1232 # Versions
1230 1233 assert_tag :tag => 'a', :content => '2.0',
1231 1234 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=3',
1232 1235 :class => '' }
1233 1236 assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
1234 1237 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=4',
1235 1238 :class => '' }
1236 1239
1237 1240 assert_tag :tag => 'a', :content => 'Dave Lopper',
1238 1241 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;issue%5Bassigned_to_id%5D=3',
1239 1242 :class => '' }
1240 1243 assert_tag :tag => 'a', :content => 'Duplicate',
1241 1244 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1242 1245 :class => 'icon-duplicate' }
1243 1246 assert_tag :tag => 'a', :content => 'Copy',
1244 1247 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1',
1245 1248 :class => 'icon-copy' }
1246 1249 assert_tag :tag => 'a', :content => 'Move',
1247 1250 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1248 1251 :class => 'icon-move' }
1249 1252 assert_tag :tag => 'a', :content => 'Delete',
1250 1253 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1251 1254 :class => 'icon-del' }
1252 1255 end
1253 1256
1254 1257 def test_context_menu_one_issue_by_anonymous
1255 1258 get :context_menu, :ids => [1]
1256 1259 assert_response :success
1257 1260 assert_template 'context_menu'
1258 1261 assert_tag :tag => 'a', :content => 'Delete',
1259 1262 :attributes => { :href => '#',
1260 1263 :class => 'icon-del disabled' }
1261 1264 end
1262 1265
1263 1266 def test_context_menu_multiple_issues_of_same_project
1264 1267 @request.session[:user_id] = 2
1265 1268 get :context_menu, :ids => [1, 2]
1266 1269 assert_response :success
1267 1270 assert_template 'context_menu'
1268 1271 assert_tag :tag => 'a', :content => 'Edit',
1269 1272 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1270 1273 :class => 'icon-edit' }
1271 1274 assert_tag :tag => 'a', :content => 'Immediate',
1272 1275 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;issue%5Bpriority_id%5D=8',
1273 1276 :class => '' }
1274 1277 assert_tag :tag => 'a', :content => 'Dave Lopper',
1275 1278 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;issue%5Bassigned_to_id%5D=3',
1276 1279 :class => '' }
1277 1280 assert_tag :tag => 'a', :content => 'Copy',
1278 1281 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1279 1282 :class => 'icon-copy' }
1280 1283 assert_tag :tag => 'a', :content => 'Move',
1281 1284 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1282 1285 :class => 'icon-move' }
1283 1286 assert_tag :tag => 'a', :content => 'Delete',
1284 1287 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1285 1288 :class => 'icon-del' }
1286 1289 end
1287 1290
1288 1291 def test_context_menu_multiple_issues_of_different_project
1289 1292 @request.session[:user_id] = 2
1290 1293 get :context_menu, :ids => [1, 2, 4]
1291 1294 assert_response :success
1292 1295 assert_template 'context_menu'
1293 1296 assert_tag :tag => 'a', :content => 'Delete',
1294 1297 :attributes => { :href => '#',
1295 1298 :class => 'icon-del disabled' }
1296 1299 end
1297 1300
1298 1301 def test_preview_new_issue
1299 1302 @request.session[:user_id] = 2
1300 1303 post :preview, :project_id => '1', :issue => {:description => 'Foo'}
1301 1304 assert_response :success
1302 1305 assert_template 'preview'
1303 1306 assert_not_nil assigns(:description)
1304 1307 end
1305 1308
1306 1309 def test_preview_notes
1307 1310 @request.session[:user_id] = 2
1308 1311 post :preview, :project_id => '1', :id => 1, :issue => {:description => Issue.find(1).description}, :notes => 'Foo'
1309 1312 assert_response :success
1310 1313 assert_template 'preview'
1311 1314 assert_not_nil assigns(:notes)
1312 1315 end
1313 1316
1314 1317 def test_auto_complete_routing
1315 1318 assert_routing(
1316 1319 {:method => :get, :path => '/issues/auto_complete'},
1317 1320 :controller => 'issues', :action => 'auto_complete'
1318 1321 )
1319 1322 end
1320 1323
1321 1324 def test_auto_complete_should_not_be_case_sensitive
1322 1325 get :auto_complete, :project_id => 'ecookbook', :q => 'ReCiPe'
1323 1326 assert_response :success
1324 1327 assert_not_nil assigns(:issues)
1325 1328 assert assigns(:issues).detect {|issue| issue.subject.match /recipe/}
1326 1329 end
1327 1330
1328 1331 def test_auto_complete_should_return_issue_with_given_id
1329 1332 get :auto_complete, :project_id => 'subproject1', :q => '13'
1330 1333 assert_response :success
1331 1334 assert_not_nil assigns(:issues)
1332 1335 assert assigns(:issues).include?(Issue.find(13))
1333 1336 end
1334 1337
1335 1338 def test_destroy_routing
1336 1339 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1337 1340 {:controller => 'issues', :action => 'destroy', :id => '1'},
1338 1341 {:method => :post, :path => '/issues/1/destroy'}
1339 1342 )
1340 1343 end
1341 1344
1342 1345 def test_destroy_issue_with_no_time_entries
1343 1346 assert_nil TimeEntry.find_by_issue_id(2)
1344 1347 @request.session[:user_id] = 2
1345 1348 post :destroy, :id => 2
1346 1349 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1347 1350 assert_nil Issue.find_by_id(2)
1348 1351 end
1349 1352
1350 1353 def test_destroy_issues_with_time_entries
1351 1354 @request.session[:user_id] = 2
1352 1355 post :destroy, :ids => [1, 3]
1353 1356 assert_response :success
1354 1357 assert_template 'destroy'
1355 1358 assert_not_nil assigns(:hours)
1356 1359 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1357 1360 end
1358 1361
1359 1362 def test_destroy_issues_and_destroy_time_entries
1360 1363 @request.session[:user_id] = 2
1361 1364 post :destroy, :ids => [1, 3], :todo => 'destroy'
1362 1365 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1363 1366 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1364 1367 assert_nil TimeEntry.find_by_id([1, 2])
1365 1368 end
1366 1369
1367 1370 def test_destroy_issues_and_assign_time_entries_to_project
1368 1371 @request.session[:user_id] = 2
1369 1372 post :destroy, :ids => [1, 3], :todo => 'nullify'
1370 1373 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1371 1374 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1372 1375 assert_nil TimeEntry.find(1).issue_id
1373 1376 assert_nil TimeEntry.find(2).issue_id
1374 1377 end
1375 1378
1376 1379 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1377 1380 @request.session[:user_id] = 2
1378 1381 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1379 1382 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1380 1383 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1381 1384 assert_equal 2, TimeEntry.find(1).issue_id
1382 1385 assert_equal 2, TimeEntry.find(2).issue_id
1383 1386 end
1384 1387
1385 1388 def test_default_search_scope
1386 1389 get :index
1387 1390 assert_tag :div, :attributes => {:id => 'quick-search'},
1388 1391 :child => {:tag => 'form',
1389 1392 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1390 1393 end
1391 1394 end
General Comments 0
You need to be logged in to leave comments. Login now