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