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