##// END OF EJS Templates
Adds closed_on column that stores the time of the last closing (#824)....
Jean-Philippe Lang -
r11172:a45c0dc55057
parent child
Show More
@@ -0,0 +1,9
1 class AddIssuesClosedOn < ActiveRecord::Migration
2 def up
3 add_column :issues, :closed_on, :datetime, :default => nil
4 end
5
6 def down
7 remove_column :issues, :closed_on
8 end
9 end
@@ -0,0 +1,25
1 class PopulateIssuesClosedOn < ActiveRecord::Migration
2 def up
3 closed_status_ids = IssueStatus.where(:is_closed => true).pluck(:id)
4 if closed_status_ids.any?
5 # First set closed_on for issues that have been closed once
6 closed_status_values = closed_status_ids.map {|status_id| "'#{status_id}'"}.join(',')
7 subselect = "SELECT MAX(#{Journal.table_name}.created_on)" +
8 " FROM #{Journal.table_name}, #{JournalDetail.table_name}" +
9 " WHERE #{Journal.table_name}.id = #{JournalDetail.table_name}.journal_id" +
10 " AND #{Journal.table_name}.journalized_type = 'Issue' AND #{Journal.table_name}.journalized_id = #{Issue.table_name}.id" +
11 " AND #{JournalDetail.table_name}.property = 'attr' AND #{JournalDetail.table_name}.prop_key = 'status_id'" +
12 " AND #{JournalDetail.table_name}.old_value NOT IN (#{closed_status_values})" +
13 " AND #{JournalDetail.table_name}.value IN (#{closed_status_values})"
14 Issue.update_all "closed_on = (#{subselect})"
15
16 # Then set closed_on for closed issues that weren't up updated by the above UPDATE
17 # No journal was found so we assume that they were closed on creation
18 Issue.update_all "closed_on = created_on", {:status_id => closed_status_ids, :closed_on => nil}
19 end
20 end
21
22 def down
23 Issue.update_all :closed_on => nil
24 end
25 end
@@ -1,1399 +1,1412
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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 include Redmine::Utils::DateCalculation
21 21
22 22 belongs_to :project
23 23 belongs_to :tracker
24 24 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
25 25 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
26 26 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
27 27 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
28 28 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
29 29 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
30 30
31 31 has_many :journals, :as => :journalized, :dependent => :destroy
32 32 has_many :visible_journals,
33 33 :class_name => 'Journal',
34 34 :as => :journalized,
35 35 :conditions => Proc.new {
36 36 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
37 37 },
38 38 :readonly => true
39 39
40 40 has_many :time_entries, :dependent => :delete_all
41 41 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
42 42
43 43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45 45
46 46 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
47 47 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
48 48 acts_as_customizable
49 49 acts_as_watchable
50 50 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
51 51 :include => [:project, :visible_journals],
52 52 # sort by id so that limited eager loading doesn't break with postgresql
53 53 :order_column => "#{table_name}.id"
54 54 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
55 55 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
56 56 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
57 57
58 58 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
59 59 :author_key => :author_id
60 60
61 61 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
62 62
63 63 attr_reader :current_journal
64 64 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
65 65
66 66 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
67 67
68 68 validates_length_of :subject, :maximum => 255
69 69 validates_inclusion_of :done_ratio, :in => 0..100
70 70 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
71 71 validates :start_date, :date => true
72 72 validates :due_date, :date => true
73 73 validate :validate_issue, :validate_required_fields
74 74
75 75 scope :visible, lambda {|*args|
76 76 includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
77 77 }
78 78
79 79 scope :open, lambda {|*args|
80 80 is_closed = args.size > 0 ? !args.first : false
81 81 includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
82 82 }
83 83
84 84 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
85 85 scope :on_active_project, lambda {
86 86 includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
87 87 }
88 88 scope :fixed_version, lambda {|versions|
89 89 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
90 90 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
91 91 }
92 92
93 93 before_create :default_assign
94 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
94 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on
95 95 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
96 96 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
97 97 # Should be after_create but would be called before previous after_save callbacks
98 98 after_save :after_create_from_copy
99 99 after_destroy :update_parent_attributes
100 100
101 101 # Returns a SQL conditions string used to find all issues visible by the specified user
102 102 def self.visible_condition(user, options={})
103 103 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
104 104 if user.logged?
105 105 case role.issues_visibility
106 106 when 'all'
107 107 nil
108 108 when 'default'
109 109 user_ids = [user.id] + user.groups.map(&:id)
110 110 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
111 111 when 'own'
112 112 user_ids = [user.id] + user.groups.map(&:id)
113 113 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
114 114 else
115 115 '1=0'
116 116 end
117 117 else
118 118 "(#{table_name}.is_private = #{connection.quoted_false})"
119 119 end
120 120 end
121 121 end
122 122
123 123 # Returns true if usr or current user is allowed to view the issue
124 124 def visible?(usr=nil)
125 125 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
126 126 if user.logged?
127 127 case role.issues_visibility
128 128 when 'all'
129 129 true
130 130 when 'default'
131 131 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
132 132 when 'own'
133 133 self.author == user || user.is_or_belongs_to?(assigned_to)
134 134 else
135 135 false
136 136 end
137 137 else
138 138 !self.is_private?
139 139 end
140 140 end
141 141 end
142 142
143 143 # Returns true if user or current user is allowed to edit or add a note to the issue
144 144 def editable?(user=User.current)
145 145 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
146 146 end
147 147
148 148 def initialize(attributes=nil, *args)
149 149 super
150 150 if new_record?
151 151 # set default values for new records only
152 152 self.status ||= IssueStatus.default
153 153 self.priority ||= IssuePriority.default
154 154 self.watcher_user_ids = []
155 155 end
156 156 end
157 157
158 158 # AR#Persistence#destroy would raise and RecordNotFound exception
159 159 # if the issue was already deleted or updated (non matching lock_version).
160 160 # This is a problem when bulk deleting issues or deleting a project
161 161 # (because an issue may already be deleted if its parent was deleted
162 162 # first).
163 163 # The issue is reloaded by the nested_set before being deleted so
164 164 # the lock_version condition should not be an issue but we handle it.
165 165 def destroy
166 166 super
167 167 rescue ActiveRecord::RecordNotFound
168 168 # Stale or already deleted
169 169 begin
170 170 reload
171 171 rescue ActiveRecord::RecordNotFound
172 172 # The issue was actually already deleted
173 173 @destroyed = true
174 174 return freeze
175 175 end
176 176 # The issue was stale, retry to destroy
177 177 super
178 178 end
179 179
180 180 def reload(*args)
181 181 @workflow_rule_by_attribute = nil
182 182 @assignable_versions = nil
183 183 super
184 184 end
185 185
186 186 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
187 187 def available_custom_fields
188 188 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
189 189 end
190 190
191 191 # Copies attributes from another issue, arg can be an id or an Issue
192 192 def copy_from(arg, options={})
193 193 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
194 194 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
195 195 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
196 196 self.status = issue.status
197 197 self.author = User.current
198 198 unless options[:attachments] == false
199 199 self.attachments = issue.attachments.map do |attachement|
200 200 attachement.copy(:container => self)
201 201 end
202 202 end
203 203 @copied_from = issue
204 204 @copy_options = options
205 205 self
206 206 end
207 207
208 208 # Returns an unsaved copy of the issue
209 209 def copy(attributes=nil, copy_options={})
210 210 copy = self.class.new.copy_from(self, copy_options)
211 211 copy.attributes = attributes if attributes
212 212 copy
213 213 end
214 214
215 215 # Returns true if the issue is a copy
216 216 def copy?
217 217 @copied_from.present?
218 218 end
219 219
220 220 # Moves/copies an issue to a new project and tracker
221 221 # Returns the moved/copied issue on success, false on failure
222 222 def move_to_project(new_project, new_tracker=nil, options={})
223 223 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
224 224
225 225 if options[:copy]
226 226 issue = self.copy
227 227 else
228 228 issue = self
229 229 end
230 230
231 231 issue.init_journal(User.current, options[:notes])
232 232
233 233 # Preserve previous behaviour
234 234 # #move_to_project doesn't change tracker automatically
235 235 issue.send :project=, new_project, true
236 236 if new_tracker
237 237 issue.tracker = new_tracker
238 238 end
239 239 # Allow bulk setting of attributes on the issue
240 240 if options[:attributes]
241 241 issue.attributes = options[:attributes]
242 242 end
243 243
244 244 issue.save ? issue : false
245 245 end
246 246
247 247 def status_id=(sid)
248 248 self.status = nil
249 249 result = write_attribute(:status_id, sid)
250 250 @workflow_rule_by_attribute = nil
251 251 result
252 252 end
253 253
254 254 def priority_id=(pid)
255 255 self.priority = nil
256 256 write_attribute(:priority_id, pid)
257 257 end
258 258
259 259 def category_id=(cid)
260 260 self.category = nil
261 261 write_attribute(:category_id, cid)
262 262 end
263 263
264 264 def fixed_version_id=(vid)
265 265 self.fixed_version = nil
266 266 write_attribute(:fixed_version_id, vid)
267 267 end
268 268
269 269 def tracker_id=(tid)
270 270 self.tracker = nil
271 271 result = write_attribute(:tracker_id, tid)
272 272 @custom_field_values = nil
273 273 @workflow_rule_by_attribute = nil
274 274 result
275 275 end
276 276
277 277 def project_id=(project_id)
278 278 if project_id.to_s != self.project_id.to_s
279 279 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
280 280 end
281 281 end
282 282
283 283 def project=(project, keep_tracker=false)
284 284 project_was = self.project
285 285 write_attribute(:project_id, project ? project.id : nil)
286 286 association_instance_set('project', project)
287 287 if project_was && project && project_was != project
288 288 @assignable_versions = nil
289 289
290 290 unless keep_tracker || project.trackers.include?(tracker)
291 291 self.tracker = project.trackers.first
292 292 end
293 293 # Reassign to the category with same name if any
294 294 if category
295 295 self.category = project.issue_categories.find_by_name(category.name)
296 296 end
297 297 # Keep the fixed_version if it's still valid in the new_project
298 298 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
299 299 self.fixed_version = nil
300 300 end
301 301 # Clear the parent task if it's no longer valid
302 302 unless valid_parent_project?
303 303 self.parent_issue_id = nil
304 304 end
305 305 @custom_field_values = nil
306 306 end
307 307 end
308 308
309 309 def description=(arg)
310 310 if arg.is_a?(String)
311 311 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
312 312 end
313 313 write_attribute(:description, arg)
314 314 end
315 315
316 316 # Overrides assign_attributes so that project and tracker get assigned first
317 317 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
318 318 return if new_attributes.nil?
319 319 attrs = new_attributes.dup
320 320 attrs.stringify_keys!
321 321
322 322 %w(project project_id tracker tracker_id).each do |attr|
323 323 if attrs.has_key?(attr)
324 324 send "#{attr}=", attrs.delete(attr)
325 325 end
326 326 end
327 327 send :assign_attributes_without_project_and_tracker_first, attrs, *args
328 328 end
329 329 # Do not redefine alias chain on reload (see #4838)
330 330 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
331 331
332 332 def estimated_hours=(h)
333 333 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
334 334 end
335 335
336 336 safe_attributes 'project_id',
337 337 :if => lambda {|issue, user|
338 338 if issue.new_record?
339 339 issue.copy?
340 340 elsif user.allowed_to?(:move_issues, issue.project)
341 341 projects = Issue.allowed_target_projects_on_move(user)
342 342 projects.include?(issue.project) && projects.size > 1
343 343 end
344 344 }
345 345
346 346 safe_attributes 'tracker_id',
347 347 'status_id',
348 348 'category_id',
349 349 'assigned_to_id',
350 350 'priority_id',
351 351 'fixed_version_id',
352 352 'subject',
353 353 'description',
354 354 'start_date',
355 355 'due_date',
356 356 'done_ratio',
357 357 'estimated_hours',
358 358 'custom_field_values',
359 359 'custom_fields',
360 360 'lock_version',
361 361 'notes',
362 362 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
363 363
364 364 safe_attributes 'status_id',
365 365 'assigned_to_id',
366 366 'fixed_version_id',
367 367 'done_ratio',
368 368 'lock_version',
369 369 'notes',
370 370 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
371 371
372 372 safe_attributes 'notes',
373 373 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
374 374
375 375 safe_attributes 'private_notes',
376 376 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
377 377
378 378 safe_attributes 'watcher_user_ids',
379 379 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
380 380
381 381 safe_attributes 'is_private',
382 382 :if => lambda {|issue, user|
383 383 user.allowed_to?(:set_issues_private, issue.project) ||
384 384 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
385 385 }
386 386
387 387 safe_attributes 'parent_issue_id',
388 388 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
389 389 user.allowed_to?(:manage_subtasks, issue.project)}
390 390
391 391 def safe_attribute_names(user=nil)
392 392 names = super
393 393 names -= disabled_core_fields
394 394 names -= read_only_attribute_names(user)
395 395 names
396 396 end
397 397
398 398 # Safely sets attributes
399 399 # Should be called from controllers instead of #attributes=
400 400 # attr_accessible is too rough because we still want things like
401 401 # Issue.new(:project => foo) to work
402 402 def safe_attributes=(attrs, user=User.current)
403 403 return unless attrs.is_a?(Hash)
404 404
405 405 attrs = attrs.dup
406 406
407 407 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
408 408 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
409 409 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
410 410 self.project_id = p
411 411 end
412 412 end
413 413
414 414 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
415 415 self.tracker_id = t
416 416 end
417 417
418 418 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
419 419 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
420 420 self.status_id = s
421 421 end
422 422 end
423 423
424 424 attrs = delete_unsafe_attributes(attrs, user)
425 425 return if attrs.empty?
426 426
427 427 unless leaf?
428 428 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
429 429 end
430 430
431 431 if attrs['parent_issue_id'].present?
432 432 s = attrs['parent_issue_id'].to_s
433 433 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
434 434 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
435 435 end
436 436 end
437 437
438 438 if attrs['custom_field_values'].present?
439 439 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
440 440 end
441 441
442 442 if attrs['custom_fields'].present?
443 443 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
444 444 end
445 445
446 446 # mass-assignment security bypass
447 447 assign_attributes attrs, :without_protection => true
448 448 end
449 449
450 450 def disabled_core_fields
451 451 tracker ? tracker.disabled_core_fields : []
452 452 end
453 453
454 454 # Returns the custom_field_values that can be edited by the given user
455 455 def editable_custom_field_values(user=nil)
456 456 custom_field_values.reject do |value|
457 457 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
458 458 end
459 459 end
460 460
461 461 # Returns the names of attributes that are read-only for user or the current user
462 462 # For users with multiple roles, the read-only fields are the intersection of
463 463 # read-only fields of each role
464 464 # The result is an array of strings where sustom fields are represented with their ids
465 465 #
466 466 # Examples:
467 467 # issue.read_only_attribute_names # => ['due_date', '2']
468 468 # issue.read_only_attribute_names(user) # => []
469 469 def read_only_attribute_names(user=nil)
470 470 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
471 471 end
472 472
473 473 # Returns the names of required attributes for user or the current user
474 474 # For users with multiple roles, the required fields are the intersection of
475 475 # required fields of each role
476 476 # The result is an array of strings where sustom fields are represented with their ids
477 477 #
478 478 # Examples:
479 479 # issue.required_attribute_names # => ['due_date', '2']
480 480 # issue.required_attribute_names(user) # => []
481 481 def required_attribute_names(user=nil)
482 482 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
483 483 end
484 484
485 485 # Returns true if the attribute is required for user
486 486 def required_attribute?(name, user=nil)
487 487 required_attribute_names(user).include?(name.to_s)
488 488 end
489 489
490 490 # Returns a hash of the workflow rule by attribute for the given user
491 491 #
492 492 # Examples:
493 493 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
494 494 def workflow_rule_by_attribute(user=nil)
495 495 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
496 496
497 497 user_real = user || User.current
498 498 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
499 499 return {} if roles.empty?
500 500
501 501 result = {}
502 502 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
503 503 if workflow_permissions.any?
504 504 workflow_rules = workflow_permissions.inject({}) do |h, wp|
505 505 h[wp.field_name] ||= []
506 506 h[wp.field_name] << wp.rule
507 507 h
508 508 end
509 509 workflow_rules.each do |attr, rules|
510 510 next if rules.size < roles.size
511 511 uniq_rules = rules.uniq
512 512 if uniq_rules.size == 1
513 513 result[attr] = uniq_rules.first
514 514 else
515 515 result[attr] = 'required'
516 516 end
517 517 end
518 518 end
519 519 @workflow_rule_by_attribute = result if user.nil?
520 520 result
521 521 end
522 522 private :workflow_rule_by_attribute
523 523
524 524 def done_ratio
525 525 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
526 526 status.default_done_ratio
527 527 else
528 528 read_attribute(:done_ratio)
529 529 end
530 530 end
531 531
532 532 def self.use_status_for_done_ratio?
533 533 Setting.issue_done_ratio == 'issue_status'
534 534 end
535 535
536 536 def self.use_field_for_done_ratio?
537 537 Setting.issue_done_ratio == 'issue_field'
538 538 end
539 539
540 540 def validate_issue
541 541 if due_date && start_date && due_date < start_date
542 542 errors.add :due_date, :greater_than_start_date
543 543 end
544 544
545 545 if start_date && soonest_start && start_date < soonest_start
546 546 errors.add :start_date, :invalid
547 547 end
548 548
549 549 if fixed_version
550 550 if !assignable_versions.include?(fixed_version)
551 551 errors.add :fixed_version_id, :inclusion
552 552 elsif reopened? && fixed_version.closed?
553 553 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
554 554 end
555 555 end
556 556
557 557 # Checks that the issue can not be added/moved to a disabled tracker
558 558 if project && (tracker_id_changed? || project_id_changed?)
559 559 unless project.trackers.include?(tracker)
560 560 errors.add :tracker_id, :inclusion
561 561 end
562 562 end
563 563
564 564 # Checks parent issue assignment
565 565 if @invalid_parent_issue_id.present?
566 566 errors.add :parent_issue_id, :invalid
567 567 elsif @parent_issue
568 568 if !valid_parent_project?(@parent_issue)
569 569 errors.add :parent_issue_id, :invalid
570 570 elsif !new_record?
571 571 # moving an existing issue
572 572 if @parent_issue.root_id != root_id
573 573 # we can always move to another tree
574 574 elsif move_possible?(@parent_issue)
575 575 # move accepted inside tree
576 576 else
577 577 errors.add :parent_issue_id, :invalid
578 578 end
579 579 end
580 580 end
581 581 end
582 582
583 583 # Validates the issue against additional workflow requirements
584 584 def validate_required_fields
585 585 user = new_record? ? author : current_journal.try(:user)
586 586
587 587 required_attribute_names(user).each do |attribute|
588 588 if attribute =~ /^\d+$/
589 589 attribute = attribute.to_i
590 590 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
591 591 if v && v.value.blank?
592 592 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
593 593 end
594 594 else
595 595 if respond_to?(attribute) && send(attribute).blank?
596 596 errors.add attribute, :blank
597 597 end
598 598 end
599 599 end
600 600 end
601 601
602 602 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
603 603 # even if the user turns off the setting later
604 604 def update_done_ratio_from_issue_status
605 605 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
606 606 self.done_ratio = status.default_done_ratio
607 607 end
608 608 end
609 609
610 610 def init_journal(user, notes = "")
611 611 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
612 612 if new_record?
613 613 @current_journal.notify = false
614 614 else
615 615 @attributes_before_change = attributes.dup
616 616 @custom_values_before_change = {}
617 617 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
618 618 end
619 619 @current_journal
620 620 end
621 621
622 622 # Returns the id of the last journal or nil
623 623 def last_journal_id
624 624 if new_record?
625 625 nil
626 626 else
627 627 journals.maximum(:id)
628 628 end
629 629 end
630 630
631 631 # Returns a scope for journals that have an id greater than journal_id
632 632 def journals_after(journal_id)
633 633 scope = journals.reorder("#{Journal.table_name}.id ASC")
634 634 if journal_id.present?
635 635 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
636 636 end
637 637 scope
638 638 end
639 639
640 640 # Return true if the issue is closed, otherwise false
641 641 def closed?
642 642 self.status.is_closed?
643 643 end
644 644
645 645 # Return true if the issue is being reopened
646 646 def reopened?
647 647 if !new_record? && status_id_changed?
648 648 status_was = IssueStatus.find_by_id(status_id_was)
649 649 status_new = IssueStatus.find_by_id(status_id)
650 650 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
651 651 return true
652 652 end
653 653 end
654 654 false
655 655 end
656 656
657 657 # Return true if the issue is being closed
658 658 def closing?
659 659 if !new_record? && status_id_changed?
660 660 status_was = IssueStatus.find_by_id(status_id_was)
661 661 status_new = IssueStatus.find_by_id(status_id)
662 662 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
663 663 return true
664 664 end
665 665 end
666 666 false
667 667 end
668 668
669 669 # Returns true if the issue is overdue
670 670 def overdue?
671 671 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
672 672 end
673 673
674 674 # Is the amount of work done less than it should for the due date
675 675 def behind_schedule?
676 676 return false if start_date.nil? || due_date.nil?
677 677 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
678 678 return done_date <= Date.today
679 679 end
680 680
681 681 # Does this issue have children?
682 682 def children?
683 683 !leaf?
684 684 end
685 685
686 686 # Users the issue can be assigned to
687 687 def assignable_users
688 688 users = project.assignable_users
689 689 users << author if author
690 690 users << assigned_to if assigned_to
691 691 users.uniq.sort
692 692 end
693 693
694 694 # Versions that the issue can be assigned to
695 695 def assignable_versions
696 696 return @assignable_versions if @assignable_versions
697 697
698 698 versions = project.shared_versions.open.all
699 699 if fixed_version
700 700 if fixed_version_id_changed?
701 701 # nothing to do
702 702 elsif project_id_changed?
703 703 if project.shared_versions.include?(fixed_version)
704 704 versions << fixed_version
705 705 end
706 706 else
707 707 versions << fixed_version
708 708 end
709 709 end
710 710 @assignable_versions = versions.uniq.sort
711 711 end
712 712
713 713 # Returns true if this issue is blocked by another issue that is still open
714 714 def blocked?
715 715 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
716 716 end
717 717
718 718 # Returns an array of statuses that user is able to apply
719 719 def new_statuses_allowed_to(user=User.current, include_default=false)
720 720 if new_record? && @copied_from
721 721 [IssueStatus.default, @copied_from.status].compact.uniq.sort
722 722 else
723 723 initial_status = nil
724 724 if new_record?
725 725 initial_status = IssueStatus.default
726 726 elsif status_id_was
727 727 initial_status = IssueStatus.find_by_id(status_id_was)
728 728 end
729 729 initial_status ||= status
730 730
731 731 statuses = initial_status.find_new_statuses_allowed_to(
732 732 user.admin ? Role.all : user.roles_for_project(project),
733 733 tracker,
734 734 author == user,
735 735 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
736 736 )
737 737 statuses << initial_status unless statuses.empty?
738 738 statuses << IssueStatus.default if include_default
739 739 statuses = statuses.compact.uniq.sort
740 740 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
741 741 end
742 742 end
743 743
744 744 def assigned_to_was
745 745 if assigned_to_id_changed? && assigned_to_id_was.present?
746 746 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
747 747 end
748 748 end
749 749
750 750 # Returns the users that should be notified
751 751 def notified_users
752 752 notified = []
753 753 # Author and assignee are always notified unless they have been
754 754 # locked or don't want to be notified
755 755 notified << author if author
756 756 if assigned_to
757 757 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
758 758 end
759 759 if assigned_to_was
760 760 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
761 761 end
762 762 notified = notified.select {|u| u.active? && u.notify_about?(self)}
763 763
764 764 notified += project.notified_users
765 765 notified.uniq!
766 766 # Remove users that can not view the issue
767 767 notified.reject! {|user| !visible?(user)}
768 768 notified
769 769 end
770 770
771 771 # Returns the email addresses that should be notified
772 772 def recipients
773 773 notified_users.collect(&:mail)
774 774 end
775 775
776 776 # Returns the number of hours spent on this issue
777 777 def spent_hours
778 778 @spent_hours ||= time_entries.sum(:hours) || 0
779 779 end
780 780
781 781 # Returns the total number of hours spent on this issue and its descendants
782 782 #
783 783 # Example:
784 784 # spent_hours => 0.0
785 785 # spent_hours => 50.2
786 786 def total_spent_hours
787 787 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
788 788 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
789 789 end
790 790
791 791 def relations
792 792 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
793 793 end
794 794
795 795 # Preloads relations for a collection of issues
796 796 def self.load_relations(issues)
797 797 if issues.any?
798 798 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
799 799 issues.each do |issue|
800 800 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
801 801 end
802 802 end
803 803 end
804 804
805 805 # Preloads visible spent time for a collection of issues
806 806 def self.load_visible_spent_hours(issues, user=User.current)
807 807 if issues.any?
808 808 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
809 809 issues.each do |issue|
810 810 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
811 811 end
812 812 end
813 813 end
814 814
815 815 # Preloads visible relations for a collection of issues
816 816 def self.load_visible_relations(issues, user=User.current)
817 817 if issues.any?
818 818 issue_ids = issues.map(&:id)
819 819 # Relations with issue_from in given issues and visible issue_to
820 820 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
821 821 # Relations with issue_to in given issues and visible issue_from
822 822 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
823 823
824 824 issues.each do |issue|
825 825 relations =
826 826 relations_from.select {|relation| relation.issue_from_id == issue.id} +
827 827 relations_to.select {|relation| relation.issue_to_id == issue.id}
828 828
829 829 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
830 830 end
831 831 end
832 832 end
833 833
834 834 # Finds an issue relation given its id.
835 835 def find_relation(relation_id)
836 836 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
837 837 end
838 838
839 839 def all_dependent_issues(except=[])
840 840 except << self
841 841 dependencies = []
842 842 relations_from.each do |relation|
843 843 if relation.issue_to && !except.include?(relation.issue_to)
844 844 dependencies << relation.issue_to
845 845 dependencies += relation.issue_to.all_dependent_issues(except)
846 846 end
847 847 end
848 848 dependencies
849 849 end
850 850
851 851 # Returns an array of issues that duplicate this one
852 852 def duplicates
853 853 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
854 854 end
855 855
856 856 # Returns the due date or the target due date if any
857 857 # Used on gantt chart
858 858 def due_before
859 859 due_date || (fixed_version ? fixed_version.effective_date : nil)
860 860 end
861 861
862 862 # Returns the time scheduled for this issue.
863 863 #
864 864 # Example:
865 865 # Start Date: 2/26/09, End Date: 3/04/09
866 866 # duration => 6
867 867 def duration
868 868 (start_date && due_date) ? due_date - start_date : 0
869 869 end
870 870
871 871 # Returns the duration in working days
872 872 def working_duration
873 873 (start_date && due_date) ? working_days(start_date, due_date) : 0
874 874 end
875 875
876 876 def soonest_start(reload=false)
877 877 @soonest_start = nil if reload
878 878 @soonest_start ||= (
879 879 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
880 880 ancestors.collect(&:soonest_start)
881 881 ).compact.max
882 882 end
883 883
884 884 # Sets start_date on the given date or the next working day
885 885 # and changes due_date to keep the same working duration.
886 886 def reschedule_on(date)
887 887 wd = working_duration
888 888 date = next_working_date(date)
889 889 self.start_date = date
890 890 self.due_date = add_working_days(date, wd)
891 891 end
892 892
893 893 # Reschedules the issue on the given date or the next working day and saves the record.
894 894 # If the issue is a parent task, this is done by rescheduling its subtasks.
895 895 def reschedule_on!(date)
896 896 return if date.nil?
897 897 if leaf?
898 898 if start_date.nil? || start_date != date
899 899 if start_date && start_date > date
900 900 # Issue can not be moved earlier than its soonest start date
901 901 date = [soonest_start(true), date].compact.max
902 902 end
903 903 reschedule_on(date)
904 904 begin
905 905 save
906 906 rescue ActiveRecord::StaleObjectError
907 907 reload
908 908 reschedule_on(date)
909 909 save
910 910 end
911 911 end
912 912 else
913 913 leaves.each do |leaf|
914 914 if leaf.start_date
915 915 # Only move subtask if it starts at the same date as the parent
916 916 # or if it starts before the given date
917 917 if start_date == leaf.start_date || date > leaf.start_date
918 918 leaf.reschedule_on!(date)
919 919 end
920 920 else
921 921 leaf.reschedule_on!(date)
922 922 end
923 923 end
924 924 end
925 925 end
926 926
927 927 def <=>(issue)
928 928 if issue.nil?
929 929 -1
930 930 elsif root_id != issue.root_id
931 931 (root_id || 0) <=> (issue.root_id || 0)
932 932 else
933 933 (lft || 0) <=> (issue.lft || 0)
934 934 end
935 935 end
936 936
937 937 def to_s
938 938 "#{tracker} ##{id}: #{subject}"
939 939 end
940 940
941 941 # Returns a string of css classes that apply to the issue
942 942 def css_classes
943 943 s = "issue status-#{status_id} #{priority.try(:css_classes)}"
944 944 s << ' closed' if closed?
945 945 s << ' overdue' if overdue?
946 946 s << ' child' if child?
947 947 s << ' parent' unless leaf?
948 948 s << ' private' if is_private?
949 949 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
950 950 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
951 951 s
952 952 end
953 953
954 954 # Saves an issue and a time_entry from the parameters
955 955 def save_issue_with_child_records(params, existing_time_entry=nil)
956 956 Issue.transaction do
957 957 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
958 958 @time_entry = existing_time_entry || TimeEntry.new
959 959 @time_entry.project = project
960 960 @time_entry.issue = self
961 961 @time_entry.user = User.current
962 962 @time_entry.spent_on = User.current.today
963 963 @time_entry.attributes = params[:time_entry]
964 964 self.time_entries << @time_entry
965 965 end
966 966
967 967 # TODO: Rename hook
968 968 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
969 969 if save
970 970 # TODO: Rename hook
971 971 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
972 972 else
973 973 raise ActiveRecord::Rollback
974 974 end
975 975 end
976 976 end
977 977
978 978 # Unassigns issues from +version+ if it's no longer shared with issue's project
979 979 def self.update_versions_from_sharing_change(version)
980 980 # Update issues assigned to the version
981 981 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
982 982 end
983 983
984 984 # Unassigns issues from versions that are no longer shared
985 985 # after +project+ was moved
986 986 def self.update_versions_from_hierarchy_change(project)
987 987 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
988 988 # Update issues of the moved projects and issues assigned to a version of a moved project
989 989 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
990 990 end
991 991
992 992 def parent_issue_id=(arg)
993 993 s = arg.to_s.strip.presence
994 994 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
995 995 @parent_issue.id
996 996 else
997 997 @parent_issue = nil
998 998 @invalid_parent_issue_id = arg
999 999 end
1000 1000 end
1001 1001
1002 1002 def parent_issue_id
1003 1003 if @invalid_parent_issue_id
1004 1004 @invalid_parent_issue_id
1005 1005 elsif instance_variable_defined? :@parent_issue
1006 1006 @parent_issue.nil? ? nil : @parent_issue.id
1007 1007 else
1008 1008 parent_id
1009 1009 end
1010 1010 end
1011 1011
1012 1012 # Returns true if issue's project is a valid
1013 1013 # parent issue project
1014 1014 def valid_parent_project?(issue=parent)
1015 1015 return true if issue.nil? || issue.project_id == project_id
1016 1016
1017 1017 case Setting.cross_project_subtasks
1018 1018 when 'system'
1019 1019 true
1020 1020 when 'tree'
1021 1021 issue.project.root == project.root
1022 1022 when 'hierarchy'
1023 1023 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1024 1024 when 'descendants'
1025 1025 issue.project.is_or_is_ancestor_of?(project)
1026 1026 else
1027 1027 false
1028 1028 end
1029 1029 end
1030 1030
1031 1031 # Extracted from the ReportsController.
1032 1032 def self.by_tracker(project)
1033 1033 count_and_group_by(:project => project,
1034 1034 :field => 'tracker_id',
1035 1035 :joins => Tracker.table_name)
1036 1036 end
1037 1037
1038 1038 def self.by_version(project)
1039 1039 count_and_group_by(:project => project,
1040 1040 :field => 'fixed_version_id',
1041 1041 :joins => Version.table_name)
1042 1042 end
1043 1043
1044 1044 def self.by_priority(project)
1045 1045 count_and_group_by(:project => project,
1046 1046 :field => 'priority_id',
1047 1047 :joins => IssuePriority.table_name)
1048 1048 end
1049 1049
1050 1050 def self.by_category(project)
1051 1051 count_and_group_by(:project => project,
1052 1052 :field => 'category_id',
1053 1053 :joins => IssueCategory.table_name)
1054 1054 end
1055 1055
1056 1056 def self.by_assigned_to(project)
1057 1057 count_and_group_by(:project => project,
1058 1058 :field => 'assigned_to_id',
1059 1059 :joins => User.table_name)
1060 1060 end
1061 1061
1062 1062 def self.by_author(project)
1063 1063 count_and_group_by(:project => project,
1064 1064 :field => 'author_id',
1065 1065 :joins => User.table_name)
1066 1066 end
1067 1067
1068 1068 def self.by_subproject(project)
1069 1069 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1070 1070 s.is_closed as closed,
1071 1071 #{Issue.table_name}.project_id as project_id,
1072 1072 count(#{Issue.table_name}.id) as total
1073 1073 from
1074 1074 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1075 1075 where
1076 1076 #{Issue.table_name}.status_id=s.id
1077 1077 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1078 1078 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1079 1079 and #{Issue.table_name}.project_id <> #{project.id}
1080 1080 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1081 1081 end
1082 1082 # End ReportsController extraction
1083 1083
1084 1084 # Returns an array of projects that user can assign the issue to
1085 1085 def allowed_target_projects(user=User.current)
1086 1086 if new_record?
1087 1087 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
1088 1088 else
1089 1089 self.class.allowed_target_projects_on_move(user)
1090 1090 end
1091 1091 end
1092 1092
1093 1093 # Returns an array of projects that user can move issues to
1094 1094 def self.allowed_target_projects_on_move(user=User.current)
1095 1095 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
1096 1096 end
1097 1097
1098 1098 private
1099 1099
1100 1100 def after_project_change
1101 1101 # Update project_id on related time entries
1102 1102 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
1103 1103
1104 1104 # Delete issue relations
1105 1105 unless Setting.cross_project_issue_relations?
1106 1106 relations_from.clear
1107 1107 relations_to.clear
1108 1108 end
1109 1109
1110 1110 # Move subtasks that were in the same project
1111 1111 children.each do |child|
1112 1112 next unless child.project_id == project_id_was
1113 1113 # Change project and keep project
1114 1114 child.send :project=, project, true
1115 1115 unless child.save
1116 1116 raise ActiveRecord::Rollback
1117 1117 end
1118 1118 end
1119 1119 end
1120 1120
1121 1121 # Callback for after the creation of an issue by copy
1122 1122 # * adds a "copied to" relation with the copied issue
1123 1123 # * copies subtasks from the copied issue
1124 1124 def after_create_from_copy
1125 1125 return unless copy? && !@after_create_from_copy_handled
1126 1126
1127 1127 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1128 1128 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1129 1129 unless relation.save
1130 1130 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1131 1131 end
1132 1132 end
1133 1133
1134 1134 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1135 1135 @copied_from.children.each do |child|
1136 1136 unless child.visible?
1137 1137 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1138 1138 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1139 1139 next
1140 1140 end
1141 1141 copy = Issue.new.copy_from(child, @copy_options)
1142 1142 copy.author = author
1143 1143 copy.project = project
1144 1144 copy.parent_issue_id = id
1145 1145 # Children subtasks are copied recursively
1146 1146 unless copy.save
1147 1147 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1148 1148 end
1149 1149 end
1150 1150 end
1151 1151 @after_create_from_copy_handled = true
1152 1152 end
1153 1153
1154 1154 def update_nested_set_attributes
1155 1155 if root_id.nil?
1156 1156 # issue was just created
1157 1157 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1158 1158 set_default_left_and_right
1159 1159 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1160 1160 if @parent_issue
1161 1161 move_to_child_of(@parent_issue)
1162 1162 end
1163 1163 reload
1164 1164 elsif parent_issue_id != parent_id
1165 1165 former_parent_id = parent_id
1166 1166 # moving an existing issue
1167 1167 if @parent_issue && @parent_issue.root_id == root_id
1168 1168 # inside the same tree
1169 1169 move_to_child_of(@parent_issue)
1170 1170 else
1171 1171 # to another tree
1172 1172 unless root?
1173 1173 move_to_right_of(root)
1174 1174 reload
1175 1175 end
1176 1176 old_root_id = root_id
1177 1177 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1178 1178 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1179 1179 offset = target_maxright + 1 - lft
1180 1180 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1181 1181 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1182 1182 self[left_column_name] = lft + offset
1183 1183 self[right_column_name] = rgt + offset
1184 1184 if @parent_issue
1185 1185 move_to_child_of(@parent_issue)
1186 1186 end
1187 1187 end
1188 1188 reload
1189 1189 # delete invalid relations of all descendants
1190 1190 self_and_descendants.each do |issue|
1191 1191 issue.relations.each do |relation|
1192 1192 relation.destroy unless relation.valid?
1193 1193 end
1194 1194 end
1195 1195 # update former parent
1196 1196 recalculate_attributes_for(former_parent_id) if former_parent_id
1197 1197 end
1198 1198 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1199 1199 end
1200 1200
1201 1201 def update_parent_attributes
1202 1202 recalculate_attributes_for(parent_id) if parent_id
1203 1203 end
1204 1204
1205 1205 def recalculate_attributes_for(issue_id)
1206 1206 if issue_id && p = Issue.find_by_id(issue_id)
1207 1207 # priority = highest priority of children
1208 1208 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1209 1209 p.priority = IssuePriority.find_by_position(priority_position)
1210 1210 end
1211 1211
1212 1212 # start/due dates = lowest/highest dates of children
1213 1213 p.start_date = p.children.minimum(:start_date)
1214 1214 p.due_date = p.children.maximum(:due_date)
1215 1215 if p.start_date && p.due_date && p.due_date < p.start_date
1216 1216 p.start_date, p.due_date = p.due_date, p.start_date
1217 1217 end
1218 1218
1219 1219 # done ratio = weighted average ratio of leaves
1220 1220 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1221 1221 leaves_count = p.leaves.count
1222 1222 if leaves_count > 0
1223 1223 average = p.leaves.average(:estimated_hours).to_f
1224 1224 if average == 0
1225 1225 average = 1
1226 1226 end
1227 1227 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1228 1228 progress = done / (average * leaves_count)
1229 1229 p.done_ratio = progress.round
1230 1230 end
1231 1231 end
1232 1232
1233 1233 # estimate = sum of leaves estimates
1234 1234 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1235 1235 p.estimated_hours = nil if p.estimated_hours == 0.0
1236 1236
1237 1237 # ancestors will be recursively updated
1238 1238 p.save(:validate => false)
1239 1239 end
1240 1240 end
1241 1241
1242 1242 # Update issues so their versions are not pointing to a
1243 1243 # fixed_version that is not shared with the issue's project
1244 1244 def self.update_versions(conditions=nil)
1245 1245 # Only need to update issues with a fixed_version from
1246 1246 # a different project and that is not systemwide shared
1247 1247 Issue.scoped(:conditions => conditions).all(
1248 1248 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1249 1249 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1250 1250 " AND #{Version.table_name}.sharing <> 'system'",
1251 1251 :include => [:project, :fixed_version]
1252 1252 ).each do |issue|
1253 1253 next if issue.project.nil? || issue.fixed_version.nil?
1254 1254 unless issue.project.shared_versions.include?(issue.fixed_version)
1255 1255 issue.init_journal(User.current)
1256 1256 issue.fixed_version = nil
1257 1257 issue.save
1258 1258 end
1259 1259 end
1260 1260 end
1261 1261
1262 1262 # Callback on file attachment
1263 1263 def attachment_added(obj)
1264 1264 if @current_journal && !obj.new_record?
1265 1265 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1266 1266 end
1267 1267 end
1268 1268
1269 1269 # Callback on attachment deletion
1270 1270 def attachment_removed(obj)
1271 1271 if @current_journal && !obj.new_record?
1272 1272 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1273 1273 @current_journal.save
1274 1274 end
1275 1275 end
1276 1276
1277 1277 # Default assignment based on category
1278 1278 def default_assign
1279 1279 if assigned_to.nil? && category && category.assigned_to
1280 1280 self.assigned_to = category.assigned_to
1281 1281 end
1282 1282 end
1283 1283
1284 1284 # Updates start/due dates of following issues
1285 1285 def reschedule_following_issues
1286 1286 if start_date_changed? || due_date_changed?
1287 1287 relations_from.each do |relation|
1288 1288 relation.set_issue_to_dates
1289 1289 end
1290 1290 end
1291 1291 end
1292 1292
1293 1293 # Closes duplicates if the issue is being closed
1294 1294 def close_duplicates
1295 1295 if closing?
1296 1296 duplicates.each do |duplicate|
1297 1297 # Reload is need in case the duplicate was updated by a previous duplicate
1298 1298 duplicate.reload
1299 1299 # Don't re-close it if it's already closed
1300 1300 next if duplicate.closed?
1301 1301 # Same user and notes
1302 1302 if @current_journal
1303 1303 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1304 1304 end
1305 1305 duplicate.update_attribute :status, self.status
1306 1306 end
1307 1307 end
1308 1308 end
1309 1309
1310 # Make sure updated_on is updated when adding a note
1310 # Make sure updated_on is updated when adding a note and set updated_on now
1311 # so we can set closed_on with the same value on closing
1311 1312 def force_updated_on_change
1312 if @current_journal
1313 if @current_journal || changed?
1313 1314 self.updated_on = current_time_from_proper_timezone
1315 if new_record?
1316 self.created_on = updated_on
1317 end
1318 end
1319 end
1320
1321 # Callback for setting closed_on when the issue is closed.
1322 # The closed_on attribute stores the time of the last closing
1323 # and is preserved when the issue is reopened.
1324 def update_closed_on
1325 if closing? || (new_record? && closed?)
1326 self.closed_on = updated_on
1314 1327 end
1315 1328 end
1316 1329
1317 1330 # Saves the changes in a Journal
1318 1331 # Called after_save
1319 1332 def create_journal
1320 1333 if @current_journal
1321 1334 # attributes changes
1322 1335 if @attributes_before_change
1323 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1336 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1324 1337 before = @attributes_before_change[c]
1325 1338 after = send(c)
1326 1339 next if before == after || (before.blank? && after.blank?)
1327 1340 @current_journal.details << JournalDetail.new(:property => 'attr',
1328 1341 :prop_key => c,
1329 1342 :old_value => before,
1330 1343 :value => after)
1331 1344 }
1332 1345 end
1333 1346 if @custom_values_before_change
1334 1347 # custom fields changes
1335 1348 custom_field_values.each {|c|
1336 1349 before = @custom_values_before_change[c.custom_field_id]
1337 1350 after = c.value
1338 1351 next if before == after || (before.blank? && after.blank?)
1339 1352
1340 1353 if before.is_a?(Array) || after.is_a?(Array)
1341 1354 before = [before] unless before.is_a?(Array)
1342 1355 after = [after] unless after.is_a?(Array)
1343 1356
1344 1357 # values removed
1345 1358 (before - after).reject(&:blank?).each do |value|
1346 1359 @current_journal.details << JournalDetail.new(:property => 'cf',
1347 1360 :prop_key => c.custom_field_id,
1348 1361 :old_value => value,
1349 1362 :value => nil)
1350 1363 end
1351 1364 # values added
1352 1365 (after - before).reject(&:blank?).each do |value|
1353 1366 @current_journal.details << JournalDetail.new(:property => 'cf',
1354 1367 :prop_key => c.custom_field_id,
1355 1368 :old_value => nil,
1356 1369 :value => value)
1357 1370 end
1358 1371 else
1359 1372 @current_journal.details << JournalDetail.new(:property => 'cf',
1360 1373 :prop_key => c.custom_field_id,
1361 1374 :old_value => before,
1362 1375 :value => after)
1363 1376 end
1364 1377 }
1365 1378 end
1366 1379 @current_journal.save
1367 1380 # reset current journal
1368 1381 init_journal @current_journal.user, @current_journal.notes
1369 1382 end
1370 1383 end
1371 1384
1372 1385 # Query generator for selecting groups of issue counts for a project
1373 1386 # based on specific criteria
1374 1387 #
1375 1388 # Options
1376 1389 # * project - Project to search in.
1377 1390 # * field - String. Issue field to key off of in the grouping.
1378 1391 # * joins - String. The table name to join against.
1379 1392 def self.count_and_group_by(options)
1380 1393 project = options.delete(:project)
1381 1394 select_field = options.delete(:field)
1382 1395 joins = options.delete(:joins)
1383 1396
1384 1397 where = "#{Issue.table_name}.#{select_field}=j.id"
1385 1398
1386 1399 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1387 1400 s.is_closed as closed,
1388 1401 j.id as #{select_field},
1389 1402 count(#{Issue.table_name}.id) as total
1390 1403 from
1391 1404 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1392 1405 where
1393 1406 #{Issue.table_name}.status_id=s.id
1394 1407 and #{where}
1395 1408 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1396 1409 and #{visible_condition(User.current, :project => project)}
1397 1410 group by s.id, s.is_closed, j.id")
1398 1411 end
1399 1412 end
@@ -1,265 +1,268
1 1 ---
2 2 issues_001:
3 3 created_on: <%= 3.days.ago.to_s(:db) %>
4 4 project_id: 1
5 5 updated_on: <%= 1.day.ago.to_s(:db) %>
6 6 priority_id: 4
7 7 subject: Can't print recipes
8 8 id: 1
9 9 fixed_version_id:
10 10 category_id: 1
11 11 description: Unable to print recipes
12 12 tracker_id: 1
13 13 assigned_to_id:
14 14 author_id: 2
15 15 status_id: 1
16 16 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
17 17 due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
18 18 root_id: 1
19 19 lft: 1
20 20 rgt: 2
21 21 lock_version: 3
22 22 issues_002:
23 23 created_on: 2006-07-19 21:04:21 +02:00
24 24 project_id: 1
25 25 updated_on: 2006-07-19 21:09:50 +02:00
26 26 priority_id: 5
27 27 subject: Add ingredients categories
28 28 id: 2
29 29 fixed_version_id: 2
30 30 category_id:
31 31 description: Ingredients of the recipe should be classified by categories
32 32 tracker_id: 2
33 33 assigned_to_id: 3
34 34 author_id: 2
35 35 status_id: 2
36 36 start_date: <%= 2.day.ago.to_date.to_s(:db) %>
37 37 due_date:
38 38 root_id: 2
39 39 lft: 1
40 40 rgt: 2
41 41 lock_version: 3
42 42 done_ratio: 30
43 43 issues_003:
44 44 created_on: 2006-07-19 21:07:27 +02:00
45 45 project_id: 1
46 46 updated_on: 2006-07-19 21:07:27 +02:00
47 47 priority_id: 4
48 48 subject: Error 281 when updating a recipe
49 49 id: 3
50 50 fixed_version_id:
51 51 category_id:
52 52 description: Error 281 is encountered when saving a recipe
53 53 tracker_id: 1
54 54 assigned_to_id: 3
55 55 author_id: 2
56 56 status_id: 1
57 57 start_date: <%= 15.day.ago.to_date.to_s(:db) %>
58 58 due_date: <%= 5.day.ago.to_date.to_s(:db) %>
59 59 root_id: 3
60 60 lft: 1
61 61 rgt: 2
62 62 issues_004:
63 63 created_on: <%= 5.days.ago.to_s(:db) %>
64 64 project_id: 2
65 65 updated_on: <%= 2.days.ago.to_s(:db) %>
66 66 priority_id: 4
67 67 subject: Issue on project 2
68 68 id: 4
69 69 fixed_version_id:
70 70 category_id:
71 71 description: Issue on project 2
72 72 tracker_id: 1
73 73 assigned_to_id: 2
74 74 author_id: 2
75 75 status_id: 1
76 76 root_id: 4
77 77 lft: 1
78 78 rgt: 2
79 79 issues_005:
80 80 created_on: <%= 5.days.ago.to_s(:db) %>
81 81 project_id: 3
82 82 updated_on: <%= 2.days.ago.to_s(:db) %>
83 83 priority_id: 4
84 84 subject: Subproject issue
85 85 id: 5
86 86 fixed_version_id:
87 87 category_id:
88 88 description: This is an issue on a cookbook subproject
89 89 tracker_id: 1
90 90 assigned_to_id:
91 91 author_id: 2
92 92 status_id: 1
93 93 root_id: 5
94 94 lft: 1
95 95 rgt: 2
96 96 issues_006:
97 97 created_on: <%= 1.minute.ago.to_s(:db) %>
98 98 project_id: 5
99 99 updated_on: <%= 1.minute.ago.to_s(:db) %>
100 100 priority_id: 4
101 101 subject: Issue of a private subproject
102 102 id: 6
103 103 fixed_version_id:
104 104 category_id:
105 105 description: This is an issue of a private subproject of cookbook
106 106 tracker_id: 1
107 107 assigned_to_id:
108 108 author_id: 2
109 109 status_id: 1
110 110 start_date: <%= Date.today.to_s(:db) %>
111 111 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
112 112 root_id: 6
113 113 lft: 1
114 114 rgt: 2
115 115 issues_007:
116 116 created_on: <%= 10.days.ago.to_s(:db) %>
117 117 project_id: 1
118 118 updated_on: <%= 10.days.ago.to_s(:db) %>
119 119 priority_id: 5
120 120 subject: Issue due today
121 121 id: 7
122 122 fixed_version_id:
123 123 category_id:
124 124 description: This is an issue that is due today
125 125 tracker_id: 1
126 126 assigned_to_id:
127 127 author_id: 2
128 128 status_id: 1
129 129 start_date: <%= 10.days.ago.to_s(:db) %>
130 130 due_date: <%= Date.today.to_s(:db) %>
131 131 lock_version: 0
132 132 root_id: 7
133 133 lft: 1
134 134 rgt: 2
135 135 issues_008:
136 136 created_on: <%= 10.days.ago.to_s(:db) %>
137 137 project_id: 1
138 138 updated_on: <%= 10.days.ago.to_s(:db) %>
139 139 priority_id: 5
140 140 subject: Closed issue
141 141 id: 8
142 142 fixed_version_id:
143 143 category_id:
144 144 description: This is a closed issue.
145 145 tracker_id: 1
146 146 assigned_to_id:
147 147 author_id: 2
148 148 status_id: 5
149 149 start_date:
150 150 due_date:
151 151 lock_version: 0
152 152 root_id: 8
153 153 lft: 1
154 154 rgt: 2
155 closed_on: <%= 3.days.ago.to_s(:db) %>
155 156 issues_009:
156 157 created_on: <%= 1.minute.ago.to_s(:db) %>
157 158 project_id: 5
158 159 updated_on: <%= 1.minute.ago.to_s(:db) %>
159 160 priority_id: 5
160 161 subject: Blocked Issue
161 162 id: 9
162 163 fixed_version_id:
163 164 category_id:
164 165 description: This is an issue that is blocked by issue #10
165 166 tracker_id: 1
166 167 assigned_to_id:
167 168 author_id: 2
168 169 status_id: 1
169 170 start_date: <%= Date.today.to_s(:db) %>
170 171 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
171 172 root_id: 9
172 173 lft: 1
173 174 rgt: 2
174 175 issues_010:
175 176 created_on: <%= 1.minute.ago.to_s(:db) %>
176 177 project_id: 5
177 178 updated_on: <%= 1.minute.ago.to_s(:db) %>
178 179 priority_id: 5
179 180 subject: Issue Doing the Blocking
180 181 id: 10
181 182 fixed_version_id:
182 183 category_id:
183 184 description: This is an issue that blocks issue #9
184 185 tracker_id: 1
185 186 assigned_to_id:
186 187 author_id: 2
187 188 status_id: 1
188 189 start_date: <%= Date.today.to_s(:db) %>
189 190 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
190 191 root_id: 10
191 192 lft: 1
192 193 rgt: 2
193 194 issues_011:
194 195 created_on: <%= 3.days.ago.to_s(:db) %>
195 196 project_id: 1
196 197 updated_on: <%= 1.day.ago.to_s(:db) %>
197 198 priority_id: 5
198 199 subject: Closed issue on a closed version
199 200 id: 11
200 201 fixed_version_id: 1
201 202 category_id: 1
202 203 description:
203 204 tracker_id: 1
204 205 assigned_to_id:
205 206 author_id: 2
206 207 status_id: 5
207 208 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
208 209 due_date:
209 210 root_id: 11
210 211 lft: 1
211 212 rgt: 2
213 closed_on: <%= 1.day.ago.to_s(:db) %>
212 214 issues_012:
213 215 created_on: <%= 3.days.ago.to_s(:db) %>
214 216 project_id: 1
215 217 updated_on: <%= 1.day.ago.to_s(:db) %>
216 218 priority_id: 5
217 219 subject: Closed issue on a locked version
218 220 id: 12
219 221 fixed_version_id: 2
220 222 category_id: 1
221 223 description:
222 224 tracker_id: 1
223 225 assigned_to_id:
224 226 author_id: 3
225 227 status_id: 5
226 228 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
227 229 due_date:
228 230 root_id: 12
229 231 lft: 1
230 232 rgt: 2
233 closed_on: <%= 1.day.ago.to_s(:db) %>
231 234 issues_013:
232 235 created_on: <%= 5.days.ago.to_s(:db) %>
233 236 project_id: 3
234 237 updated_on: <%= 2.days.ago.to_s(:db) %>
235 238 priority_id: 4
236 239 subject: Subproject issue two
237 240 id: 13
238 241 fixed_version_id:
239 242 category_id:
240 243 description: This is a second issue on a cookbook subproject
241 244 tracker_id: 1
242 245 assigned_to_id:
243 246 author_id: 2
244 247 status_id: 1
245 248 root_id: 13
246 249 lft: 1
247 250 rgt: 2
248 251 issues_014:
249 252 id: 14
250 253 created_on: <%= 15.days.ago.to_s(:db) %>
251 254 project_id: 3
252 255 updated_on: <%= 15.days.ago.to_s(:db) %>
253 256 priority_id: 5
254 257 subject: Private issue on public project
255 258 fixed_version_id:
256 259 category_id:
257 260 description: This is a private issue
258 261 tracker_id: 1
259 262 assigned_to_id:
260 263 author_id: 2
261 264 status_id: 1
262 265 is_private: true
263 266 root_id: 14
264 267 lft: 1
265 268 rgt: 2
@@ -1,1920 +1,1974
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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 :groups_users,
23 23 :trackers, :projects_trackers,
24 24 :enabled_modules,
25 25 :versions,
26 26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 27 :enumerations,
28 28 :issues, :journals, :journal_details,
29 29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
30 30 :time_entries
31 31
32 32 include Redmine::I18n
33 33
34 34 def teardown
35 35 User.current = nil
36 36 end
37 37
38 38 def test_create
39 39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
40 40 :status_id => 1, :priority => IssuePriority.all.first,
41 41 :subject => 'test_create',
42 42 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
43 43 assert issue.save
44 44 issue.reload
45 45 assert_equal 1.5, issue.estimated_hours
46 46 end
47 47
48 48 def test_create_minimal
49 49 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
50 50 :status_id => 1, :priority => IssuePriority.all.first,
51 51 :subject => 'test_create')
52 52 assert issue.save
53 53 assert issue.description.nil?
54 54 assert_nil issue.estimated_hours
55 55 end
56 56
57 57 def test_start_date_format_should_be_validated
58 58 set_language_if_valid 'en'
59 59 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
60 60 issue = Issue.new(:start_date => invalid_date)
61 61 assert !issue.valid?
62 62 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
63 63 end
64 64 end
65 65
66 66 def test_due_date_format_should_be_validated
67 67 set_language_if_valid 'en'
68 68 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
69 69 issue = Issue.new(:due_date => invalid_date)
70 70 assert !issue.valid?
71 71 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
72 72 end
73 73 end
74 74
75 75 def test_due_date_lesser_than_start_date_should_not_validate
76 76 set_language_if_valid 'en'
77 77 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
78 78 assert !issue.valid?
79 79 assert_include 'Due date must be greater than start date', issue.errors.full_messages
80 80 end
81 81
82 82 def test_estimated_hours_should_be_validated
83 83 set_language_if_valid 'en'
84 84 ['-2'].each do |invalid|
85 85 issue = Issue.new(:estimated_hours => invalid)
86 86 assert !issue.valid?
87 87 assert_include 'Estimated time is invalid', issue.errors.full_messages
88 88 end
89 89 end
90 90
91 91 def test_create_with_required_custom_field
92 92 set_language_if_valid 'en'
93 93 field = IssueCustomField.find_by_name('Database')
94 94 field.update_attribute(:is_required, true)
95 95
96 96 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
97 97 :status_id => 1, :subject => 'test_create',
98 98 :description => 'IssueTest#test_create_with_required_custom_field')
99 99 assert issue.available_custom_fields.include?(field)
100 100 # No value for the custom field
101 101 assert !issue.save
102 102 assert_equal ["Database can't be blank"], issue.errors.full_messages
103 103 # Blank value
104 104 issue.custom_field_values = { field.id => '' }
105 105 assert !issue.save
106 106 assert_equal ["Database can't be blank"], issue.errors.full_messages
107 107 # Invalid value
108 108 issue.custom_field_values = { field.id => 'SQLServer' }
109 109 assert !issue.save
110 110 assert_equal ["Database is not included in the list"], issue.errors.full_messages
111 111 # Valid value
112 112 issue.custom_field_values = { field.id => 'PostgreSQL' }
113 113 assert issue.save
114 114 issue.reload
115 115 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
116 116 end
117 117
118 118 def test_create_with_group_assignment
119 119 with_settings :issue_group_assignment => '1' do
120 120 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
121 121 :subject => 'Group assignment',
122 122 :assigned_to_id => 11).save
123 123 issue = Issue.first(:order => 'id DESC')
124 124 assert_kind_of Group, issue.assigned_to
125 125 assert_equal Group.find(11), issue.assigned_to
126 126 end
127 127 end
128 128
129 129 def test_create_with_parent_issue_id
130 130 issue = Issue.new(:project_id => 1, :tracker_id => 1,
131 131 :author_id => 1, :subject => 'Group assignment',
132 132 :parent_issue_id => 1)
133 133 assert_save issue
134 134 assert_equal 1, issue.parent_issue_id
135 135 assert_equal Issue.find(1), issue.parent
136 136 end
137 137
138 138 def test_create_with_sharp_parent_issue_id
139 139 issue = Issue.new(:project_id => 1, :tracker_id => 1,
140 140 :author_id => 1, :subject => 'Group assignment',
141 141 :parent_issue_id => "#1")
142 142 assert_save issue
143 143 assert_equal 1, issue.parent_issue_id
144 144 assert_equal Issue.find(1), issue.parent
145 145 end
146 146
147 147 def test_create_with_invalid_parent_issue_id
148 148 set_language_if_valid 'en'
149 149 issue = Issue.new(:project_id => 1, :tracker_id => 1,
150 150 :author_id => 1, :subject => 'Group assignment',
151 151 :parent_issue_id => '01ABC')
152 152 assert !issue.save
153 153 assert_equal '01ABC', issue.parent_issue_id
154 154 assert_include 'Parent task is invalid', issue.errors.full_messages
155 155 end
156 156
157 157 def test_create_with_invalid_sharp_parent_issue_id
158 158 set_language_if_valid 'en'
159 159 issue = Issue.new(:project_id => 1, :tracker_id => 1,
160 160 :author_id => 1, :subject => 'Group assignment',
161 161 :parent_issue_id => '#01ABC')
162 162 assert !issue.save
163 163 assert_equal '#01ABC', issue.parent_issue_id
164 164 assert_include 'Parent task is invalid', issue.errors.full_messages
165 165 end
166 166
167 167 def assert_visibility_match(user, issues)
168 168 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
169 169 end
170 170
171 171 def test_visible_scope_for_anonymous
172 172 # Anonymous user should see issues of public projects only
173 173 issues = Issue.visible(User.anonymous).all
174 174 assert issues.any?
175 175 assert_nil issues.detect {|issue| !issue.project.is_public?}
176 176 assert_nil issues.detect {|issue| issue.is_private?}
177 177 assert_visibility_match User.anonymous, issues
178 178 end
179 179
180 180 def test_visible_scope_for_anonymous_without_view_issues_permissions
181 181 # Anonymous user should not see issues without permission
182 182 Role.anonymous.remove_permission!(:view_issues)
183 183 issues = Issue.visible(User.anonymous).all
184 184 assert issues.empty?
185 185 assert_visibility_match User.anonymous, issues
186 186 end
187 187
188 188 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
189 189 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
190 190 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
191 191 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
192 192 assert !issue.visible?(User.anonymous)
193 193 end
194 194
195 195 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
196 196 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
197 197 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
198 198 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
199 199 assert !issue.visible?(User.anonymous)
200 200 end
201 201
202 202 def test_visible_scope_for_non_member
203 203 user = User.find(9)
204 204 assert user.projects.empty?
205 205 # Non member user should see issues of public projects only
206 206 issues = Issue.visible(user).all
207 207 assert issues.any?
208 208 assert_nil issues.detect {|issue| !issue.project.is_public?}
209 209 assert_nil issues.detect {|issue| issue.is_private?}
210 210 assert_visibility_match user, issues
211 211 end
212 212
213 213 def test_visible_scope_for_non_member_with_own_issues_visibility
214 214 Role.non_member.update_attribute :issues_visibility, 'own'
215 215 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
216 216 user = User.find(9)
217 217
218 218 issues = Issue.visible(user).all
219 219 assert issues.any?
220 220 assert_nil issues.detect {|issue| issue.author != user}
221 221 assert_visibility_match user, issues
222 222 end
223 223
224 224 def test_visible_scope_for_non_member_without_view_issues_permissions
225 225 # Non member user should not see issues without permission
226 226 Role.non_member.remove_permission!(:view_issues)
227 227 user = User.find(9)
228 228 assert user.projects.empty?
229 229 issues = Issue.visible(user).all
230 230 assert issues.empty?
231 231 assert_visibility_match user, issues
232 232 end
233 233
234 234 def test_visible_scope_for_member
235 235 user = User.find(9)
236 236 # User should see issues of projects for which he has view_issues permissions only
237 237 Role.non_member.remove_permission!(:view_issues)
238 238 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
239 239 issues = Issue.visible(user).all
240 240 assert issues.any?
241 241 assert_nil issues.detect {|issue| issue.project_id != 3}
242 242 assert_nil issues.detect {|issue| issue.is_private?}
243 243 assert_visibility_match user, issues
244 244 end
245 245
246 246 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
247 247 user = User.find(8)
248 248 assert user.groups.any?
249 249 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
250 250 Role.non_member.remove_permission!(:view_issues)
251 251
252 252 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
253 253 :status_id => 1, :priority => IssuePriority.all.first,
254 254 :subject => 'Assignment test',
255 255 :assigned_to => user.groups.first,
256 256 :is_private => true)
257 257
258 258 Role.find(2).update_attribute :issues_visibility, 'default'
259 259 issues = Issue.visible(User.find(8)).all
260 260 assert issues.any?
261 261 assert issues.include?(issue)
262 262
263 263 Role.find(2).update_attribute :issues_visibility, 'own'
264 264 issues = Issue.visible(User.find(8)).all
265 265 assert issues.any?
266 266 assert issues.include?(issue)
267 267 end
268 268
269 269 def test_visible_scope_for_admin
270 270 user = User.find(1)
271 271 user.members.each(&:destroy)
272 272 assert user.projects.empty?
273 273 issues = Issue.visible(user).all
274 274 assert issues.any?
275 275 # Admin should see issues on private projects that he does not belong to
276 276 assert issues.detect {|issue| !issue.project.is_public?}
277 277 # Admin should see private issues of other users
278 278 assert issues.detect {|issue| issue.is_private? && issue.author != user}
279 279 assert_visibility_match user, issues
280 280 end
281 281
282 282 def test_visible_scope_with_project
283 283 project = Project.find(1)
284 284 issues = Issue.visible(User.find(2), :project => project).all
285 285 projects = issues.collect(&:project).uniq
286 286 assert_equal 1, projects.size
287 287 assert_equal project, projects.first
288 288 end
289 289
290 290 def test_visible_scope_with_project_and_subprojects
291 291 project = Project.find(1)
292 292 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
293 293 projects = issues.collect(&:project).uniq
294 294 assert projects.size > 1
295 295 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
296 296 end
297 297
298 298 def test_visible_and_nested_set_scopes
299 299 assert_equal 0, Issue.find(1).descendants.visible.all.size
300 300 end
301 301
302 302 def test_open_scope
303 303 issues = Issue.open.all
304 304 assert_nil issues.detect(&:closed?)
305 305 end
306 306
307 307 def test_open_scope_with_arg
308 308 issues = Issue.open(false).all
309 309 assert_equal issues, issues.select(&:closed?)
310 310 end
311 311
312 312 def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues
313 313 version = Version.find(2)
314 314 assert version.fixed_issues.any?
315 315 assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort
316 316 end
317 317
318 318 def test_fixed_version_scope_with_empty_array_should_return_no_result
319 319 assert_equal 0, Issue.fixed_version([]).count
320 320 end
321 321
322 322 def test_errors_full_messages_should_include_custom_fields_errors
323 323 field = IssueCustomField.find_by_name('Database')
324 324
325 325 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
326 326 :status_id => 1, :subject => 'test_create',
327 327 :description => 'IssueTest#test_create_with_required_custom_field')
328 328 assert issue.available_custom_fields.include?(field)
329 329 # Invalid value
330 330 issue.custom_field_values = { field.id => 'SQLServer' }
331 331
332 332 assert !issue.valid?
333 333 assert_equal 1, issue.errors.full_messages.size
334 334 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
335 335 issue.errors.full_messages.first
336 336 end
337 337
338 338 def test_update_issue_with_required_custom_field
339 339 field = IssueCustomField.find_by_name('Database')
340 340 field.update_attribute(:is_required, true)
341 341
342 342 issue = Issue.find(1)
343 343 assert_nil issue.custom_value_for(field)
344 344 assert issue.available_custom_fields.include?(field)
345 345 # No change to custom values, issue can be saved
346 346 assert issue.save
347 347 # Blank value
348 348 issue.custom_field_values = { field.id => '' }
349 349 assert !issue.save
350 350 # Valid value
351 351 issue.custom_field_values = { field.id => 'PostgreSQL' }
352 352 assert issue.save
353 353 issue.reload
354 354 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
355 355 end
356 356
357 357 def test_should_not_update_attributes_if_custom_fields_validation_fails
358 358 issue = Issue.find(1)
359 359 field = IssueCustomField.find_by_name('Database')
360 360 assert issue.available_custom_fields.include?(field)
361 361
362 362 issue.custom_field_values = { field.id => 'Invalid' }
363 363 issue.subject = 'Should be not be saved'
364 364 assert !issue.save
365 365
366 366 issue.reload
367 367 assert_equal "Can't print recipes", issue.subject
368 368 end
369 369
370 370 def test_should_not_recreate_custom_values_objects_on_update
371 371 field = IssueCustomField.find_by_name('Database')
372 372
373 373 issue = Issue.find(1)
374 374 issue.custom_field_values = { field.id => 'PostgreSQL' }
375 375 assert issue.save
376 376 custom_value = issue.custom_value_for(field)
377 377 issue.reload
378 378 issue.custom_field_values = { field.id => 'MySQL' }
379 379 assert issue.save
380 380 issue.reload
381 381 assert_equal custom_value.id, issue.custom_value_for(field).id
382 382 end
383 383
384 384 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
385 385 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
386 386 :status_id => 1, :subject => 'Test',
387 387 :custom_field_values => {'2' => 'Test'})
388 388 assert !Tracker.find(2).custom_field_ids.include?(2)
389 389
390 390 issue = Issue.find(issue.id)
391 391 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
392 392
393 393 issue = Issue.find(issue.id)
394 394 custom_value = issue.custom_value_for(2)
395 395 assert_not_nil custom_value
396 396 assert_equal 'Test', custom_value.value
397 397 end
398 398
399 399 def test_assigning_tracker_id_should_reload_custom_fields_values
400 400 issue = Issue.new(:project => Project.find(1))
401 401 assert issue.custom_field_values.empty?
402 402 issue.tracker_id = 1
403 403 assert issue.custom_field_values.any?
404 404 end
405 405
406 406 def test_assigning_attributes_should_assign_project_and_tracker_first
407 407 seq = sequence('seq')
408 408 issue = Issue.new
409 409 issue.expects(:project_id=).in_sequence(seq)
410 410 issue.expects(:tracker_id=).in_sequence(seq)
411 411 issue.expects(:subject=).in_sequence(seq)
412 412 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
413 413 end
414 414
415 415 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
416 416 attributes = ActiveSupport::OrderedHash.new
417 417 attributes['custom_field_values'] = { '1' => 'MySQL' }
418 418 attributes['tracker_id'] = '1'
419 419 issue = Issue.new(:project => Project.find(1))
420 420 issue.attributes = attributes
421 421 assert_equal 'MySQL', issue.custom_field_value(1)
422 422 end
423 423
424 424 def test_reload_should_reload_custom_field_values
425 425 issue = Issue.generate!
426 426 issue.custom_field_values = {'2' => 'Foo'}
427 427 issue.save!
428 428
429 429 issue = Issue.order('id desc').first
430 430 assert_equal 'Foo', issue.custom_field_value(2)
431 431
432 432 issue.custom_field_values = {'2' => 'Bar'}
433 433 assert_equal 'Bar', issue.custom_field_value(2)
434 434
435 435 issue.reload
436 436 assert_equal 'Foo', issue.custom_field_value(2)
437 437 end
438 438
439 439 def test_should_update_issue_with_disabled_tracker
440 440 p = Project.find(1)
441 441 issue = Issue.find(1)
442 442
443 443 p.trackers.delete(issue.tracker)
444 444 assert !p.trackers.include?(issue.tracker)
445 445
446 446 issue.reload
447 447 issue.subject = 'New subject'
448 448 assert issue.save
449 449 end
450 450
451 451 def test_should_not_set_a_disabled_tracker
452 452 p = Project.find(1)
453 453 p.trackers.delete(Tracker.find(2))
454 454
455 455 issue = Issue.find(1)
456 456 issue.tracker_id = 2
457 457 issue.subject = 'New subject'
458 458 assert !issue.save
459 459 assert_not_nil issue.errors[:tracker_id]
460 460 end
461 461
462 462 def test_category_based_assignment
463 463 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
464 464 :status_id => 1, :priority => IssuePriority.all.first,
465 465 :subject => 'Assignment test',
466 466 :description => 'Assignment test', :category_id => 1)
467 467 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
468 468 end
469 469
470 470 def test_new_statuses_allowed_to
471 471 WorkflowTransition.delete_all
472 472 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
473 473 :old_status_id => 1, :new_status_id => 2,
474 474 :author => false, :assignee => false)
475 475 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
476 476 :old_status_id => 1, :new_status_id => 3,
477 477 :author => true, :assignee => false)
478 478 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1,
479 479 :new_status_id => 4, :author => false,
480 480 :assignee => true)
481 481 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
482 482 :old_status_id => 1, :new_status_id => 5,
483 483 :author => true, :assignee => true)
484 484 status = IssueStatus.find(1)
485 485 role = Role.find(1)
486 486 tracker = Tracker.find(1)
487 487 user = User.find(2)
488 488
489 489 issue = Issue.generate!(:tracker => tracker, :status => status,
490 490 :project_id => 1, :author_id => 1)
491 491 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
492 492
493 493 issue = Issue.generate!(:tracker => tracker, :status => status,
494 494 :project_id => 1, :author => user)
495 495 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
496 496
497 497 issue = Issue.generate!(:tracker => tracker, :status => status,
498 498 :project_id => 1, :author_id => 1,
499 499 :assigned_to => user)
500 500 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
501 501
502 502 issue = Issue.generate!(:tracker => tracker, :status => status,
503 503 :project_id => 1, :author => user,
504 504 :assigned_to => user)
505 505 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
506 506 end
507 507
508 508 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
509 509 admin = User.find(1)
510 510 issue = Issue.find(1)
511 511 assert !admin.member_of?(issue.project)
512 512 expected_statuses = [issue.status] +
513 513 WorkflowTransition.find_all_by_old_status_id(
514 514 issue.status_id).map(&:new_status).uniq.sort
515 515 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
516 516 end
517 517
518 518 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
519 519 issue = Issue.find(1).copy
520 520 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
521 521
522 522 issue = Issue.find(2).copy
523 523 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
524 524 end
525 525
526 526 def test_safe_attributes_names_should_not_include_disabled_field
527 527 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
528 528
529 529 issue = Issue.new(:tracker => tracker)
530 530 assert_include 'tracker_id', issue.safe_attribute_names
531 531 assert_include 'status_id', issue.safe_attribute_names
532 532 assert_include 'subject', issue.safe_attribute_names
533 533 assert_include 'description', issue.safe_attribute_names
534 534 assert_include 'custom_field_values', issue.safe_attribute_names
535 535 assert_include 'custom_fields', issue.safe_attribute_names
536 536 assert_include 'lock_version', issue.safe_attribute_names
537 537
538 538 tracker.core_fields.each do |field|
539 539 assert_include field, issue.safe_attribute_names
540 540 end
541 541
542 542 tracker.disabled_core_fields.each do |field|
543 543 assert_not_include field, issue.safe_attribute_names
544 544 end
545 545 end
546 546
547 547 def test_safe_attributes_should_ignore_disabled_fields
548 548 tracker = Tracker.find(1)
549 549 tracker.core_fields = %w(assigned_to_id due_date)
550 550 tracker.save!
551 551
552 552 issue = Issue.new(:tracker => tracker)
553 553 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
554 554 assert_nil issue.start_date
555 555 assert_equal Date.parse('2012-07-14'), issue.due_date
556 556 end
557 557
558 558 def test_safe_attributes_should_accept_target_tracker_enabled_fields
559 559 source = Tracker.find(1)
560 560 source.core_fields = []
561 561 source.save!
562 562 target = Tracker.find(2)
563 563 target.core_fields = %w(assigned_to_id due_date)
564 564 target.save!
565 565
566 566 issue = Issue.new(:tracker => source)
567 567 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
568 568 assert_equal target, issue.tracker
569 569 assert_equal Date.parse('2012-07-14'), issue.due_date
570 570 end
571 571
572 572 def test_safe_attributes_should_not_include_readonly_fields
573 573 WorkflowPermission.delete_all
574 574 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
575 575 :role_id => 1, :field_name => 'due_date',
576 576 :rule => 'readonly')
577 577 user = User.find(2)
578 578
579 579 issue = Issue.new(:project_id => 1, :tracker_id => 1)
580 580 assert_equal %w(due_date), issue.read_only_attribute_names(user)
581 581 assert_not_include 'due_date', issue.safe_attribute_names(user)
582 582
583 583 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
584 584 assert_equal Date.parse('2012-07-14'), issue.start_date
585 585 assert_nil issue.due_date
586 586 end
587 587
588 588 def test_safe_attributes_should_not_include_readonly_custom_fields
589 589 cf1 = IssueCustomField.create!(:name => 'Writable field',
590 590 :field_format => 'string',
591 591 :is_for_all => true, :tracker_ids => [1])
592 592 cf2 = IssueCustomField.create!(:name => 'Readonly field',
593 593 :field_format => 'string',
594 594 :is_for_all => true, :tracker_ids => [1])
595 595 WorkflowPermission.delete_all
596 596 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
597 597 :role_id => 1, :field_name => cf2.id.to_s,
598 598 :rule => 'readonly')
599 599 user = User.find(2)
600 600 issue = Issue.new(:project_id => 1, :tracker_id => 1)
601 601 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
602 602 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
603 603
604 604 issue.send :safe_attributes=, {'custom_field_values' => {
605 605 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
606 606 }}, user
607 607 assert_equal 'value1', issue.custom_field_value(cf1)
608 608 assert_nil issue.custom_field_value(cf2)
609 609
610 610 issue.send :safe_attributes=, {'custom_fields' => [
611 611 {'id' => cf1.id.to_s, 'value' => 'valuea'},
612 612 {'id' => cf2.id.to_s, 'value' => 'valueb'}
613 613 ]}, user
614 614 assert_equal 'valuea', issue.custom_field_value(cf1)
615 615 assert_nil issue.custom_field_value(cf2)
616 616 end
617 617
618 618 def test_editable_custom_field_values_should_return_non_readonly_custom_values
619 619 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
620 620 :is_for_all => true, :tracker_ids => [1, 2])
621 621 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
622 622 :is_for_all => true, :tracker_ids => [1, 2])
623 623 WorkflowPermission.delete_all
624 624 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
625 625 :field_name => cf2.id.to_s, :rule => 'readonly')
626 626 user = User.find(2)
627 627
628 628 issue = Issue.new(:project_id => 1, :tracker_id => 1)
629 629 values = issue.editable_custom_field_values(user)
630 630 assert values.detect {|value| value.custom_field == cf1}
631 631 assert_nil values.detect {|value| value.custom_field == cf2}
632 632
633 633 issue.tracker_id = 2
634 634 values = issue.editable_custom_field_values(user)
635 635 assert values.detect {|value| value.custom_field == cf1}
636 636 assert values.detect {|value| value.custom_field == cf2}
637 637 end
638 638
639 639 def test_safe_attributes_should_accept_target_tracker_writable_fields
640 640 WorkflowPermission.delete_all
641 641 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
642 642 :role_id => 1, :field_name => 'due_date',
643 643 :rule => 'readonly')
644 644 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
645 645 :role_id => 1, :field_name => 'start_date',
646 646 :rule => 'readonly')
647 647 user = User.find(2)
648 648
649 649 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
650 650
651 651 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
652 652 'due_date' => '2012-07-14'}, user
653 653 assert_equal Date.parse('2012-07-12'), issue.start_date
654 654 assert_nil issue.due_date
655 655
656 656 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
657 657 'due_date' => '2012-07-16',
658 658 'tracker_id' => 2}, user
659 659 assert_equal Date.parse('2012-07-12'), issue.start_date
660 660 assert_equal Date.parse('2012-07-16'), issue.due_date
661 661 end
662 662
663 663 def test_safe_attributes_should_accept_target_status_writable_fields
664 664 WorkflowPermission.delete_all
665 665 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
666 666 :role_id => 1, :field_name => 'due_date',
667 667 :rule => 'readonly')
668 668 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
669 669 :role_id => 1, :field_name => 'start_date',
670 670 :rule => 'readonly')
671 671 user = User.find(2)
672 672
673 673 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
674 674
675 675 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
676 676 'due_date' => '2012-07-14'},
677 677 user
678 678 assert_equal Date.parse('2012-07-12'), issue.start_date
679 679 assert_nil issue.due_date
680 680
681 681 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
682 682 'due_date' => '2012-07-16',
683 683 'status_id' => 2},
684 684 user
685 685 assert_equal Date.parse('2012-07-12'), issue.start_date
686 686 assert_equal Date.parse('2012-07-16'), issue.due_date
687 687 end
688 688
689 689 def test_required_attributes_should_be_validated
690 690 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
691 691 :is_for_all => true, :tracker_ids => [1, 2])
692 692
693 693 WorkflowPermission.delete_all
694 694 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
695 695 :role_id => 1, :field_name => 'due_date',
696 696 :rule => 'required')
697 697 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
698 698 :role_id => 1, :field_name => 'category_id',
699 699 :rule => 'required')
700 700 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
701 701 :role_id => 1, :field_name => cf.id.to_s,
702 702 :rule => 'required')
703 703
704 704 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
705 705 :role_id => 1, :field_name => 'start_date',
706 706 :rule => 'required')
707 707 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
708 708 :role_id => 1, :field_name => cf.id.to_s,
709 709 :rule => 'required')
710 710 user = User.find(2)
711 711
712 712 issue = Issue.new(:project_id => 1, :tracker_id => 1,
713 713 :status_id => 1, :subject => 'Required fields',
714 714 :author => user)
715 715 assert_equal [cf.id.to_s, "category_id", "due_date"],
716 716 issue.required_attribute_names(user).sort
717 717 assert !issue.save, "Issue was saved"
718 718 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"],
719 719 issue.errors.full_messages.sort
720 720
721 721 issue.tracker_id = 2
722 722 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
723 723 assert !issue.save, "Issue was saved"
724 724 assert_equal ["Foo can't be blank", "Start date can't be blank"],
725 725 issue.errors.full_messages.sort
726 726
727 727 issue.start_date = Date.today
728 728 issue.custom_field_values = {cf.id.to_s => 'bar'}
729 729 assert issue.save
730 730 end
731 731
732 732 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
733 733 WorkflowPermission.delete_all
734 734 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
735 735 :role_id => 1, :field_name => 'due_date',
736 736 :rule => 'required')
737 737 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
738 738 :role_id => 1, :field_name => 'start_date',
739 739 :rule => 'required')
740 740 user = User.find(2)
741 741 member = Member.find(1)
742 742 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
743 743
744 744 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
745 745
746 746 member.role_ids = [1, 2]
747 747 member.save!
748 748 assert_equal [], issue.required_attribute_names(user.reload)
749 749
750 750 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
751 751 :role_id => 2, :field_name => 'due_date',
752 752 :rule => 'required')
753 753 assert_equal %w(due_date), issue.required_attribute_names(user)
754 754
755 755 member.role_ids = [1, 2, 3]
756 756 member.save!
757 757 assert_equal [], issue.required_attribute_names(user.reload)
758 758
759 759 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
760 760 :role_id => 2, :field_name => 'due_date',
761 761 :rule => 'readonly')
762 762 # required + readonly => required
763 763 assert_equal %w(due_date), issue.required_attribute_names(user)
764 764 end
765 765
766 766 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
767 767 WorkflowPermission.delete_all
768 768 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
769 769 :role_id => 1, :field_name => 'due_date',
770 770 :rule => 'readonly')
771 771 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
772 772 :role_id => 1, :field_name => 'start_date',
773 773 :rule => 'readonly')
774 774 user = User.find(2)
775 775 member = Member.find(1)
776 776 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
777 777
778 778 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
779 779
780 780 member.role_ids = [1, 2]
781 781 member.save!
782 782 assert_equal [], issue.read_only_attribute_names(user.reload)
783 783
784 784 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
785 785 :role_id => 2, :field_name => 'due_date',
786 786 :rule => 'readonly')
787 787 assert_equal %w(due_date), issue.read_only_attribute_names(user)
788 788 end
789 789
790 790 def test_copy
791 791 issue = Issue.new.copy_from(1)
792 792 assert issue.copy?
793 793 assert issue.save
794 794 issue.reload
795 795 orig = Issue.find(1)
796 796 assert_equal orig.subject, issue.subject
797 797 assert_equal orig.tracker, issue.tracker
798 798 assert_equal "125", issue.custom_value_for(2).value
799 799 end
800 800
801 801 def test_copy_should_copy_status
802 802 orig = Issue.find(8)
803 803 assert orig.status != IssueStatus.default
804 804
805 805 issue = Issue.new.copy_from(orig)
806 806 assert issue.save
807 807 issue.reload
808 808 assert_equal orig.status, issue.status
809 809 end
810 810
811 811 def test_copy_should_add_relation_with_copied_issue
812 812 copied = Issue.find(1)
813 813 issue = Issue.new.copy_from(copied)
814 814 assert issue.save
815 815 issue.reload
816 816
817 817 assert_equal 1, issue.relations.size
818 818 relation = issue.relations.first
819 819 assert_equal 'copied_to', relation.relation_type
820 820 assert_equal copied, relation.issue_from
821 821 assert_equal issue, relation.issue_to
822 822 end
823 823
824 824 def test_copy_should_copy_subtasks
825 825 issue = Issue.generate_with_descendants!
826 826
827 827 copy = issue.reload.copy
828 828 copy.author = User.find(7)
829 829 assert_difference 'Issue.count', 1+issue.descendants.count do
830 830 assert copy.save
831 831 end
832 832 copy.reload
833 833 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
834 834 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
835 835 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
836 836 assert_equal copy.author, child_copy.author
837 837 end
838 838
839 839 def test_copy_should_copy_subtasks_to_target_project
840 840 issue = Issue.generate_with_descendants!
841 841
842 842 copy = issue.copy(:project_id => 3)
843 843 assert_difference 'Issue.count', 1+issue.descendants.count do
844 844 assert copy.save
845 845 end
846 846 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
847 847 end
848 848
849 849 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
850 850 issue = Issue.generate_with_descendants!
851 851
852 852 copy = issue.reload.copy
853 853 assert_difference 'Issue.count', 1+issue.descendants.count do
854 854 assert copy.save
855 855 assert copy.save
856 856 end
857 857 end
858 858
859 859 def test_should_not_call_after_project_change_on_creation
860 860 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
861 861 :subject => 'Test', :author_id => 1)
862 862 issue.expects(:after_project_change).never
863 863 issue.save!
864 864 end
865 865
866 866 def test_should_not_call_after_project_change_on_update
867 867 issue = Issue.find(1)
868 868 issue.project = Project.find(1)
869 869 issue.subject = 'No project change'
870 870 issue.expects(:after_project_change).never
871 871 issue.save!
872 872 end
873 873
874 874 def test_should_call_after_project_change_on_project_change
875 875 issue = Issue.find(1)
876 876 issue.project = Project.find(2)
877 877 issue.expects(:after_project_change).once
878 878 issue.save!
879 879 end
880 880
881 881 def test_adding_journal_should_update_timestamp
882 882 issue = Issue.find(1)
883 883 updated_on_was = issue.updated_on
884 884
885 885 issue.init_journal(User.first, "Adding notes")
886 886 assert_difference 'Journal.count' do
887 887 assert issue.save
888 888 end
889 889 issue.reload
890 890
891 891 assert_not_equal updated_on_was, issue.updated_on
892 892 end
893 893
894 894 def test_should_close_duplicates
895 895 # Create 3 issues
896 896 issue1 = Issue.generate!
897 897 issue2 = Issue.generate!
898 898 issue3 = Issue.generate!
899 899
900 900 # 2 is a dupe of 1
901 901 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
902 902 :relation_type => IssueRelation::TYPE_DUPLICATES)
903 903 # And 3 is a dupe of 2
904 904 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
905 905 :relation_type => IssueRelation::TYPE_DUPLICATES)
906 906 # And 3 is a dupe of 1 (circular duplicates)
907 907 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
908 908 :relation_type => IssueRelation::TYPE_DUPLICATES)
909 909
910 910 assert issue1.reload.duplicates.include?(issue2)
911 911
912 912 # Closing issue 1
913 913 issue1.init_journal(User.first, "Closing issue1")
914 914 issue1.status = IssueStatus.where(:is_closed => true).first
915 915 assert issue1.save
916 916 # 2 and 3 should be also closed
917 917 assert issue2.reload.closed?
918 918 assert issue3.reload.closed?
919 919 end
920 920
921 921 def test_should_not_close_duplicated_issue
922 922 issue1 = Issue.generate!
923 923 issue2 = Issue.generate!
924 924
925 925 # 2 is a dupe of 1
926 926 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
927 927 :relation_type => IssueRelation::TYPE_DUPLICATES)
928 928 # 2 is a dup of 1 but 1 is not a duplicate of 2
929 929 assert !issue2.reload.duplicates.include?(issue1)
930 930
931 931 # Closing issue 2
932 932 issue2.init_journal(User.first, "Closing issue2")
933 933 issue2.status = IssueStatus.where(:is_closed => true).first
934 934 assert issue2.save
935 935 # 1 should not be also closed
936 936 assert !issue1.reload.closed?
937 937 end
938 938
939 939 def test_assignable_versions
940 940 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
941 941 :status_id => 1, :fixed_version_id => 1,
942 942 :subject => 'New issue')
943 943 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
944 944 end
945 945
946 946 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
947 947 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
948 948 :status_id => 1, :fixed_version_id => 1,
949 949 :subject => 'New issue')
950 950 assert !issue.save
951 951 assert_not_nil issue.errors[:fixed_version_id]
952 952 end
953 953
954 954 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
955 955 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
956 956 :status_id => 1, :fixed_version_id => 2,
957 957 :subject => 'New issue')
958 958 assert !issue.save
959 959 assert_not_nil issue.errors[:fixed_version_id]
960 960 end
961 961
962 962 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
963 963 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
964 964 :status_id => 1, :fixed_version_id => 3,
965 965 :subject => 'New issue')
966 966 assert issue.save
967 967 end
968 968
969 969 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
970 970 issue = Issue.find(11)
971 971 assert_equal 'closed', issue.fixed_version.status
972 972 issue.subject = 'Subject changed'
973 973 assert issue.save
974 974 end
975 975
976 976 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
977 977 issue = Issue.find(11)
978 978 issue.status_id = 1
979 979 assert !issue.save
980 980 assert_not_nil issue.errors[:base]
981 981 end
982 982
983 983 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
984 984 issue = Issue.find(11)
985 985 issue.status_id = 1
986 986 issue.fixed_version_id = 3
987 987 assert issue.save
988 988 end
989 989
990 990 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
991 991 issue = Issue.find(12)
992 992 assert_equal 'locked', issue.fixed_version.status
993 993 issue.status_id = 1
994 994 assert issue.save
995 995 end
996 996
997 997 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
998 998 issue = Issue.find(2)
999 999 assert_equal 2, issue.fixed_version_id
1000 1000 issue.project_id = 3
1001 1001 assert_nil issue.fixed_version_id
1002 1002 issue.fixed_version_id = 2
1003 1003 assert !issue.save
1004 1004 assert_include 'Target version is not included in the list', issue.errors.full_messages
1005 1005 end
1006 1006
1007 1007 def test_should_keep_shared_version_when_changing_project
1008 1008 Version.find(2).update_attribute :sharing, 'tree'
1009 1009
1010 1010 issue = Issue.find(2)
1011 1011 assert_equal 2, issue.fixed_version_id
1012 1012 issue.project_id = 3
1013 1013 assert_equal 2, issue.fixed_version_id
1014 1014 assert issue.save
1015 1015 end
1016 1016
1017 1017 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
1018 1018 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1019 1019 end
1020 1020
1021 1021 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
1022 1022 Project.find(2).disable_module! :issue_tracking
1023 1023 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1024 1024 end
1025 1025
1026 1026 def test_move_to_another_project_with_same_category
1027 1027 issue = Issue.find(1)
1028 1028 issue.project = Project.find(2)
1029 1029 assert issue.save
1030 1030 issue.reload
1031 1031 assert_equal 2, issue.project_id
1032 1032 # Category changes
1033 1033 assert_equal 4, issue.category_id
1034 1034 # Make sure time entries were move to the target project
1035 1035 assert_equal 2, issue.time_entries.first.project_id
1036 1036 end
1037 1037
1038 1038 def test_move_to_another_project_without_same_category
1039 1039 issue = Issue.find(2)
1040 1040 issue.project = Project.find(2)
1041 1041 assert issue.save
1042 1042 issue.reload
1043 1043 assert_equal 2, issue.project_id
1044 1044 # Category cleared
1045 1045 assert_nil issue.category_id
1046 1046 end
1047 1047
1048 1048 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1049 1049 issue = Issue.find(1)
1050 1050 issue.update_attribute(:fixed_version_id, 1)
1051 1051 issue.project = Project.find(2)
1052 1052 assert issue.save
1053 1053 issue.reload
1054 1054 assert_equal 2, issue.project_id
1055 1055 # Cleared fixed_version
1056 1056 assert_equal nil, issue.fixed_version
1057 1057 end
1058 1058
1059 1059 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1060 1060 issue = Issue.find(1)
1061 1061 issue.update_attribute(:fixed_version_id, 4)
1062 1062 issue.project = Project.find(5)
1063 1063 assert issue.save
1064 1064 issue.reload
1065 1065 assert_equal 5, issue.project_id
1066 1066 # Keep fixed_version
1067 1067 assert_equal 4, issue.fixed_version_id
1068 1068 end
1069 1069
1070 1070 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1071 1071 issue = Issue.find(1)
1072 1072 issue.update_attribute(:fixed_version_id, 1)
1073 1073 issue.project = Project.find(5)
1074 1074 assert issue.save
1075 1075 issue.reload
1076 1076 assert_equal 5, issue.project_id
1077 1077 # Cleared fixed_version
1078 1078 assert_equal nil, issue.fixed_version
1079 1079 end
1080 1080
1081 1081 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1082 1082 issue = Issue.find(1)
1083 1083 issue.update_attribute(:fixed_version_id, 7)
1084 1084 issue.project = Project.find(2)
1085 1085 assert issue.save
1086 1086 issue.reload
1087 1087 assert_equal 2, issue.project_id
1088 1088 # Keep fixed_version
1089 1089 assert_equal 7, issue.fixed_version_id
1090 1090 end
1091 1091
1092 1092 def test_move_to_another_project_should_keep_parent_if_valid
1093 1093 issue = Issue.find(1)
1094 1094 issue.update_attribute(:parent_issue_id, 2)
1095 1095 issue.project = Project.find(3)
1096 1096 assert issue.save
1097 1097 issue.reload
1098 1098 assert_equal 2, issue.parent_id
1099 1099 end
1100 1100
1101 1101 def test_move_to_another_project_should_clear_parent_if_not_valid
1102 1102 issue = Issue.find(1)
1103 1103 issue.update_attribute(:parent_issue_id, 2)
1104 1104 issue.project = Project.find(2)
1105 1105 assert issue.save
1106 1106 issue.reload
1107 1107 assert_nil issue.parent_id
1108 1108 end
1109 1109
1110 1110 def test_move_to_another_project_with_disabled_tracker
1111 1111 issue = Issue.find(1)
1112 1112 target = Project.find(2)
1113 1113 target.tracker_ids = [3]
1114 1114 target.save
1115 1115 issue.project = target
1116 1116 assert issue.save
1117 1117 issue.reload
1118 1118 assert_equal 2, issue.project_id
1119 1119 assert_equal 3, issue.tracker_id
1120 1120 end
1121 1121
1122 1122 def test_copy_to_the_same_project
1123 1123 issue = Issue.find(1)
1124 1124 copy = issue.copy
1125 1125 assert_difference 'Issue.count' do
1126 1126 copy.save!
1127 1127 end
1128 1128 assert_kind_of Issue, copy
1129 1129 assert_equal issue.project, copy.project
1130 1130 assert_equal "125", copy.custom_value_for(2).value
1131 1131 end
1132 1132
1133 1133 def test_copy_to_another_project_and_tracker
1134 1134 issue = Issue.find(1)
1135 1135 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1136 1136 assert_difference 'Issue.count' do
1137 1137 copy.save!
1138 1138 end
1139 1139 copy.reload
1140 1140 assert_kind_of Issue, copy
1141 1141 assert_equal Project.find(3), copy.project
1142 1142 assert_equal Tracker.find(2), copy.tracker
1143 1143 # Custom field #2 is not associated with target tracker
1144 1144 assert_nil copy.custom_value_for(2)
1145 1145 end
1146 1146
1147 1147 test "#copy should not create a journal" do
1148 1148 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1149 1149 copy.save!
1150 1150 assert_equal 0, copy.reload.journals.size
1151 1151 end
1152 1152
1153 1153 test "#copy should allow assigned_to changes" do
1154 1154 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1155 1155 assert_equal 3, copy.assigned_to_id
1156 1156 end
1157 1157
1158 1158 test "#copy should allow status changes" do
1159 1159 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1160 1160 assert_equal 2, copy.status_id
1161 1161 end
1162 1162
1163 1163 test "#copy should allow start date changes" do
1164 1164 date = Date.today
1165 1165 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1166 1166 assert_equal date, copy.start_date
1167 1167 end
1168 1168
1169 1169 test "#copy should allow due date changes" do
1170 1170 date = Date.today
1171 1171 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1172 1172 assert_equal date, copy.due_date
1173 1173 end
1174 1174
1175 1175 test "#copy should set current user as author" do
1176 1176 User.current = User.find(9)
1177 1177 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2)
1178 1178 assert_equal User.current, copy.author
1179 1179 end
1180 1180
1181 1181 test "#copy should create a journal with notes" do
1182 1182 date = Date.today
1183 1183 notes = "Notes added when copying"
1184 1184 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1185 1185 copy.init_journal(User.current, notes)
1186 1186 copy.save!
1187 1187
1188 1188 assert_equal 1, copy.journals.size
1189 1189 journal = copy.journals.first
1190 1190 assert_equal 0, journal.details.size
1191 1191 assert_equal notes, journal.notes
1192 1192 end
1193 1193
1194 1194 def test_valid_parent_project
1195 1195 issue = Issue.find(1)
1196 1196 issue_in_same_project = Issue.find(2)
1197 1197 issue_in_child_project = Issue.find(5)
1198 1198 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1199 1199 issue_in_other_child_project = Issue.find(6)
1200 1200 issue_in_different_tree = Issue.find(4)
1201 1201
1202 1202 with_settings :cross_project_subtasks => '' do
1203 1203 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1204 1204 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1205 1205 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1206 1206 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1207 1207 end
1208 1208
1209 1209 with_settings :cross_project_subtasks => 'system' do
1210 1210 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1211 1211 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1212 1212 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1213 1213 end
1214 1214
1215 1215 with_settings :cross_project_subtasks => 'tree' do
1216 1216 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1217 1217 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1218 1218 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1219 1219 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1220 1220
1221 1221 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1222 1222 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1223 1223 end
1224 1224
1225 1225 with_settings :cross_project_subtasks => 'descendants' do
1226 1226 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1227 1227 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1228 1228 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1229 1229 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1230 1230
1231 1231 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1232 1232 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1233 1233 end
1234 1234 end
1235 1235
1236 1236 def test_recipients_should_include_previous_assignee
1237 1237 user = User.find(3)
1238 1238 user.members.update_all ["mail_notification = ?", false]
1239 1239 user.update_attribute :mail_notification, 'only_assigned'
1240 1240
1241 1241 issue = Issue.find(2)
1242 1242 issue.assigned_to = nil
1243 1243 assert_include user.mail, issue.recipients
1244 1244 issue.save!
1245 1245 assert !issue.recipients.include?(user.mail)
1246 1246 end
1247 1247
1248 1248 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1249 1249 issue = Issue.find(12)
1250 1250 assert issue.recipients.include?(issue.author.mail)
1251 1251 # copy the issue to a private project
1252 1252 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1253 1253 # author is not a member of project anymore
1254 1254 assert !copy.recipients.include?(copy.author.mail)
1255 1255 end
1256 1256
1257 1257 def test_recipients_should_include_the_assigned_group_members
1258 1258 group_member = User.generate!
1259 1259 group = Group.generate!
1260 1260 group.users << group_member
1261 1261
1262 1262 issue = Issue.find(12)
1263 1263 issue.assigned_to = group
1264 1264 assert issue.recipients.include?(group_member.mail)
1265 1265 end
1266 1266
1267 1267 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1268 1268 user = User.find(3)
1269 1269 issue = Issue.find(9)
1270 1270 Watcher.create!(:user => user, :watchable => issue)
1271 1271 assert issue.watched_by?(user)
1272 1272 assert !issue.watcher_recipients.include?(user.mail)
1273 1273 end
1274 1274
1275 1275 def test_issue_destroy
1276 1276 Issue.find(1).destroy
1277 1277 assert_nil Issue.find_by_id(1)
1278 1278 assert_nil TimeEntry.find_by_issue_id(1)
1279 1279 end
1280 1280
1281 1281 def test_destroying_a_deleted_issue_should_not_raise_an_error
1282 1282 issue = Issue.find(1)
1283 1283 Issue.find(1).destroy
1284 1284
1285 1285 assert_nothing_raised do
1286 1286 assert_no_difference 'Issue.count' do
1287 1287 issue.destroy
1288 1288 end
1289 1289 assert issue.destroyed?
1290 1290 end
1291 1291 end
1292 1292
1293 1293 def test_destroying_a_stale_issue_should_not_raise_an_error
1294 1294 issue = Issue.find(1)
1295 1295 Issue.find(1).update_attribute :subject, "Updated"
1296 1296
1297 1297 assert_nothing_raised do
1298 1298 assert_difference 'Issue.count', -1 do
1299 1299 issue.destroy
1300 1300 end
1301 1301 assert issue.destroyed?
1302 1302 end
1303 1303 end
1304 1304
1305 1305 def test_blocked
1306 1306 blocked_issue = Issue.find(9)
1307 1307 blocking_issue = Issue.find(10)
1308 1308
1309 1309 assert blocked_issue.blocked?
1310 1310 assert !blocking_issue.blocked?
1311 1311 end
1312 1312
1313 1313 def test_blocked_issues_dont_allow_closed_statuses
1314 1314 blocked_issue = Issue.find(9)
1315 1315
1316 1316 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1317 1317 assert !allowed_statuses.empty?
1318 1318 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1319 1319 assert closed_statuses.empty?
1320 1320 end
1321 1321
1322 1322 def test_unblocked_issues_allow_closed_statuses
1323 1323 blocking_issue = Issue.find(10)
1324 1324
1325 1325 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1326 1326 assert !allowed_statuses.empty?
1327 1327 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1328 1328 assert !closed_statuses.empty?
1329 1329 end
1330 1330
1331 1331 def test_reschedule_an_issue_without_dates
1332 1332 with_settings :non_working_week_days => [] do
1333 1333 issue = Issue.new(:start_date => nil, :due_date => nil)
1334 1334 issue.reschedule_on '2012-10-09'.to_date
1335 1335 assert_equal '2012-10-09'.to_date, issue.start_date
1336 1336 assert_equal '2012-10-09'.to_date, issue.due_date
1337 1337 end
1338 1338
1339 1339 with_settings :non_working_week_days => %w(6 7) do
1340 1340 issue = Issue.new(:start_date => nil, :due_date => nil)
1341 1341 issue.reschedule_on '2012-10-09'.to_date
1342 1342 assert_equal '2012-10-09'.to_date, issue.start_date
1343 1343 assert_equal '2012-10-09'.to_date, issue.due_date
1344 1344
1345 1345 issue = Issue.new(:start_date => nil, :due_date => nil)
1346 1346 issue.reschedule_on '2012-10-13'.to_date
1347 1347 assert_equal '2012-10-15'.to_date, issue.start_date
1348 1348 assert_equal '2012-10-15'.to_date, issue.due_date
1349 1349 end
1350 1350 end
1351 1351
1352 1352 def test_reschedule_an_issue_with_start_date
1353 1353 with_settings :non_working_week_days => [] do
1354 1354 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1355 1355 issue.reschedule_on '2012-10-13'.to_date
1356 1356 assert_equal '2012-10-13'.to_date, issue.start_date
1357 1357 assert_equal '2012-10-13'.to_date, issue.due_date
1358 1358 end
1359 1359
1360 1360 with_settings :non_working_week_days => %w(6 7) do
1361 1361 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1362 1362 issue.reschedule_on '2012-10-11'.to_date
1363 1363 assert_equal '2012-10-11'.to_date, issue.start_date
1364 1364 assert_equal '2012-10-11'.to_date, issue.due_date
1365 1365
1366 1366 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1367 1367 issue.reschedule_on '2012-10-13'.to_date
1368 1368 assert_equal '2012-10-15'.to_date, issue.start_date
1369 1369 assert_equal '2012-10-15'.to_date, issue.due_date
1370 1370 end
1371 1371 end
1372 1372
1373 1373 def test_reschedule_an_issue_with_start_and_due_dates
1374 1374 with_settings :non_working_week_days => [] do
1375 1375 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1376 1376 issue.reschedule_on '2012-10-13'.to_date
1377 1377 assert_equal '2012-10-13'.to_date, issue.start_date
1378 1378 assert_equal '2012-10-19'.to_date, issue.due_date
1379 1379 end
1380 1380
1381 1381 with_settings :non_working_week_days => %w(6 7) do
1382 1382 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1383 1383 issue.reschedule_on '2012-10-11'.to_date
1384 1384 assert_equal '2012-10-11'.to_date, issue.start_date
1385 1385 assert_equal '2012-10-23'.to_date, issue.due_date
1386 1386
1387 1387 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1388 1388 issue.reschedule_on '2012-10-13'.to_date
1389 1389 assert_equal '2012-10-15'.to_date, issue.start_date
1390 1390 assert_equal '2012-10-25'.to_date, issue.due_date
1391 1391 end
1392 1392 end
1393 1393
1394 1394 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1395 1395 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1396 1396 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1397 1397 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1398 1398 :relation_type => IssueRelation::TYPE_PRECEDES)
1399 1399 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1400 1400
1401 1401 issue1.due_date = '2012-10-23'
1402 1402 issue1.save!
1403 1403 issue2.reload
1404 1404 assert_equal Date.parse('2012-10-24'), issue2.start_date
1405 1405 assert_equal Date.parse('2012-10-26'), issue2.due_date
1406 1406 end
1407 1407
1408 1408 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1409 1409 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1410 1410 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1411 1411 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1412 1412 :relation_type => IssueRelation::TYPE_PRECEDES)
1413 1413 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1414 1414
1415 1415 issue1.start_date = '2012-09-17'
1416 1416 issue1.due_date = '2012-09-18'
1417 1417 issue1.save!
1418 1418 issue2.reload
1419 1419 assert_equal Date.parse('2012-09-19'), issue2.start_date
1420 1420 assert_equal Date.parse('2012-09-21'), issue2.due_date
1421 1421 end
1422 1422
1423 1423 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1424 1424 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1425 1425 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1426 1426 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
1427 1427 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1428 1428 :relation_type => IssueRelation::TYPE_PRECEDES)
1429 1429 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1430 1430 :relation_type => IssueRelation::TYPE_PRECEDES)
1431 1431 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1432 1432
1433 1433 issue1.start_date = '2012-09-17'
1434 1434 issue1.due_date = '2012-09-18'
1435 1435 issue1.save!
1436 1436 issue2.reload
1437 1437 # Issue 2 must start after Issue 3
1438 1438 assert_equal Date.parse('2012-10-03'), issue2.start_date
1439 1439 assert_equal Date.parse('2012-10-05'), issue2.due_date
1440 1440 end
1441 1441
1442 1442 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1443 1443 with_settings :non_working_week_days => [] do
1444 1444 stale = Issue.find(1)
1445 1445 issue = Issue.find(1)
1446 1446 issue.subject = "Updated"
1447 1447 issue.save!
1448 1448 date = 10.days.from_now.to_date
1449 1449 assert_nothing_raised do
1450 1450 stale.reschedule_on!(date)
1451 1451 end
1452 1452 assert_equal date, stale.reload.start_date
1453 1453 end
1454 1454 end
1455 1455
1456 1456 def test_overdue
1457 1457 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1458 1458 assert !Issue.new(:due_date => Date.today).overdue?
1459 1459 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1460 1460 assert !Issue.new(:due_date => nil).overdue?
1461 1461 assert !Issue.new(:due_date => 1.day.ago.to_date,
1462 1462 :status => IssueStatus.where(:is_closed => true).first
1463 1463 ).overdue?
1464 1464 end
1465 1465
1466 1466 test "#behind_schedule? should be false if the issue has no start_date" do
1467 1467 assert !Issue.new(:start_date => nil,
1468 1468 :due_date => 1.day.from_now.to_date,
1469 1469 :done_ratio => 0).behind_schedule?
1470 1470 end
1471 1471
1472 1472 test "#behind_schedule? should be false if the issue has no end_date" do
1473 1473 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1474 1474 :due_date => nil,
1475 1475 :done_ratio => 0).behind_schedule?
1476 1476 end
1477 1477
1478 1478 test "#behind_schedule? should be false if the issue has more done than it's calendar time" do
1479 1479 assert !Issue.new(:start_date => 50.days.ago.to_date,
1480 1480 :due_date => 50.days.from_now.to_date,
1481 1481 :done_ratio => 90).behind_schedule?
1482 1482 end
1483 1483
1484 1484 test "#behind_schedule? should be true if the issue hasn't been started at all" do
1485 1485 assert Issue.new(:start_date => 1.day.ago.to_date,
1486 1486 :due_date => 1.day.from_now.to_date,
1487 1487 :done_ratio => 0).behind_schedule?
1488 1488 end
1489 1489
1490 1490 test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do
1491 1491 assert Issue.new(:start_date => 100.days.ago.to_date,
1492 1492 :due_date => Date.today,
1493 1493 :done_ratio => 90).behind_schedule?
1494 1494 end
1495 1495
1496 1496 test "#assignable_users should be Users" do
1497 1497 assert_kind_of User, Issue.find(1).assignable_users.first
1498 1498 end
1499 1499
1500 1500 test "#assignable_users should include the issue author" do
1501 1501 non_project_member = User.generate!
1502 1502 issue = Issue.generate!(:author => non_project_member)
1503 1503
1504 1504 assert issue.assignable_users.include?(non_project_member)
1505 1505 end
1506 1506
1507 1507 test "#assignable_users should include the current assignee" do
1508 1508 user = User.generate!
1509 1509 issue = Issue.generate!(:assigned_to => user)
1510 1510 user.lock!
1511 1511
1512 1512 assert Issue.find(issue.id).assignable_users.include?(user)
1513 1513 end
1514 1514
1515 1515 test "#assignable_users should not show the issue author twice" do
1516 1516 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1517 1517 assert_equal 2, assignable_user_ids.length
1518 1518
1519 1519 assignable_user_ids.each do |user_id|
1520 1520 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1521 1521 "User #{user_id} appears more or less than once"
1522 1522 end
1523 1523 end
1524 1524
1525 1525 test "#assignable_users with issue_group_assignment should include groups" do
1526 1526 issue = Issue.new(:project => Project.find(2))
1527 1527
1528 1528 with_settings :issue_group_assignment => '1' do
1529 1529 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1530 1530 assert issue.assignable_users.include?(Group.find(11))
1531 1531 end
1532 1532 end
1533 1533
1534 1534 test "#assignable_users without issue_group_assignment should not include groups" do
1535 1535 issue = Issue.new(:project => Project.find(2))
1536 1536
1537 1537 with_settings :issue_group_assignment => '0' do
1538 1538 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1539 1539 assert !issue.assignable_users.include?(Group.find(11))
1540 1540 end
1541 1541 end
1542 1542
1543 1543 def test_create_should_send_email_notification
1544 1544 ActionMailer::Base.deliveries.clear
1545 1545 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1546 1546 :author_id => 3, :status_id => 1,
1547 1547 :priority => IssuePriority.all.first,
1548 1548 :subject => 'test_create', :estimated_hours => '1:30')
1549 1549
1550 1550 assert issue.save
1551 1551 assert_equal 1, ActionMailer::Base.deliveries.size
1552 1552 end
1553 1553
1554 1554 def test_stale_issue_should_not_send_email_notification
1555 1555 ActionMailer::Base.deliveries.clear
1556 1556 issue = Issue.find(1)
1557 1557 stale = Issue.find(1)
1558 1558
1559 1559 issue.init_journal(User.find(1))
1560 1560 issue.subject = 'Subjet update'
1561 1561 assert issue.save
1562 1562 assert_equal 1, ActionMailer::Base.deliveries.size
1563 1563 ActionMailer::Base.deliveries.clear
1564 1564
1565 1565 stale.init_journal(User.find(1))
1566 1566 stale.subject = 'Another subjet update'
1567 1567 assert_raise ActiveRecord::StaleObjectError do
1568 1568 stale.save
1569 1569 end
1570 1570 assert ActionMailer::Base.deliveries.empty?
1571 1571 end
1572 1572
1573 1573 def test_journalized_description
1574 1574 IssueCustomField.delete_all
1575 1575
1576 1576 i = Issue.first
1577 1577 old_description = i.description
1578 1578 new_description = "This is the new description"
1579 1579
1580 1580 i.init_journal(User.find(2))
1581 1581 i.description = new_description
1582 1582 assert_difference 'Journal.count', 1 do
1583 1583 assert_difference 'JournalDetail.count', 1 do
1584 1584 i.save!
1585 1585 end
1586 1586 end
1587 1587
1588 1588 detail = JournalDetail.first(:order => 'id DESC')
1589 1589 assert_equal i, detail.journal.journalized
1590 1590 assert_equal 'attr', detail.property
1591 1591 assert_equal 'description', detail.prop_key
1592 1592 assert_equal old_description, detail.old_value
1593 1593 assert_equal new_description, detail.value
1594 1594 end
1595 1595
1596 1596 def test_blank_descriptions_should_not_be_journalized
1597 1597 IssueCustomField.delete_all
1598 1598 Issue.update_all("description = NULL", "id=1")
1599 1599
1600 1600 i = Issue.find(1)
1601 1601 i.init_journal(User.find(2))
1602 1602 i.subject = "blank description"
1603 1603 i.description = "\r\n"
1604 1604
1605 1605 assert_difference 'Journal.count', 1 do
1606 1606 assert_difference 'JournalDetail.count', 1 do
1607 1607 i.save!
1608 1608 end
1609 1609 end
1610 1610 end
1611 1611
1612 1612 def test_journalized_multi_custom_field
1613 1613 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
1614 1614 :is_filter => true, :is_for_all => true,
1615 1615 :tracker_ids => [1],
1616 1616 :possible_values => ['value1', 'value2', 'value3'],
1617 1617 :multiple => true)
1618 1618
1619 1619 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
1620 1620 :subject => 'Test', :author_id => 1)
1621 1621
1622 1622 assert_difference 'Journal.count' do
1623 1623 assert_difference 'JournalDetail.count' do
1624 1624 issue.init_journal(User.first)
1625 1625 issue.custom_field_values = {field.id => ['value1']}
1626 1626 issue.save!
1627 1627 end
1628 1628 assert_difference 'JournalDetail.count' do
1629 1629 issue.init_journal(User.first)
1630 1630 issue.custom_field_values = {field.id => ['value1', 'value2']}
1631 1631 issue.save!
1632 1632 end
1633 1633 assert_difference 'JournalDetail.count', 2 do
1634 1634 issue.init_journal(User.first)
1635 1635 issue.custom_field_values = {field.id => ['value3', 'value2']}
1636 1636 issue.save!
1637 1637 end
1638 1638 assert_difference 'JournalDetail.count', 2 do
1639 1639 issue.init_journal(User.first)
1640 1640 issue.custom_field_values = {field.id => nil}
1641 1641 issue.save!
1642 1642 end
1643 1643 end
1644 1644 end
1645 1645
1646 1646 def test_description_eol_should_be_normalized
1647 1647 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1648 1648 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1649 1649 end
1650 1650
1651 1651 def test_saving_twice_should_not_duplicate_journal_details
1652 1652 i = Issue.first
1653 1653 i.init_journal(User.find(2), 'Some notes')
1654 1654 # initial changes
1655 1655 i.subject = 'New subject'
1656 1656 i.done_ratio = i.done_ratio + 10
1657 1657 assert_difference 'Journal.count' do
1658 1658 assert i.save
1659 1659 end
1660 1660 # 1 more change
1661 1661 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
1662 1662 assert_no_difference 'Journal.count' do
1663 1663 assert_difference 'JournalDetail.count', 1 do
1664 1664 i.save
1665 1665 end
1666 1666 end
1667 1667 # no more change
1668 1668 assert_no_difference 'Journal.count' do
1669 1669 assert_no_difference 'JournalDetail.count' do
1670 1670 i.save
1671 1671 end
1672 1672 end
1673 1673 end
1674 1674
1675 1675 def test_all_dependent_issues
1676 1676 IssueRelation.delete_all
1677 1677 assert IssueRelation.create!(:issue_from => Issue.find(1),
1678 1678 :issue_to => Issue.find(2),
1679 1679 :relation_type => IssueRelation::TYPE_PRECEDES)
1680 1680 assert IssueRelation.create!(:issue_from => Issue.find(2),
1681 1681 :issue_to => Issue.find(3),
1682 1682 :relation_type => IssueRelation::TYPE_PRECEDES)
1683 1683 assert IssueRelation.create!(:issue_from => Issue.find(3),
1684 1684 :issue_to => Issue.find(8),
1685 1685 :relation_type => IssueRelation::TYPE_PRECEDES)
1686 1686
1687 1687 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1688 1688 end
1689 1689
1690 1690 def test_all_dependent_issues_with_persistent_circular_dependency
1691 1691 IssueRelation.delete_all
1692 1692 assert IssueRelation.create!(:issue_from => Issue.find(1),
1693 1693 :issue_to => Issue.find(2),
1694 1694 :relation_type => IssueRelation::TYPE_PRECEDES)
1695 1695 assert IssueRelation.create!(:issue_from => Issue.find(2),
1696 1696 :issue_to => Issue.find(3),
1697 1697 :relation_type => IssueRelation::TYPE_PRECEDES)
1698 1698
1699 1699 r = IssueRelation.create!(:issue_from => Issue.find(3),
1700 1700 :issue_to => Issue.find(7),
1701 1701 :relation_type => IssueRelation::TYPE_PRECEDES)
1702 1702 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1703 1703
1704 1704 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1705 1705 end
1706 1706
1707 1707 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1708 1708 IssueRelation.delete_all
1709 1709 assert IssueRelation.create!(:issue_from => Issue.find(1),
1710 1710 :issue_to => Issue.find(2),
1711 1711 :relation_type => IssueRelation::TYPE_RELATES)
1712 1712 assert IssueRelation.create!(:issue_from => Issue.find(2),
1713 1713 :issue_to => Issue.find(3),
1714 1714 :relation_type => IssueRelation::TYPE_RELATES)
1715 1715 assert IssueRelation.create!(:issue_from => Issue.find(3),
1716 1716 :issue_to => Issue.find(8),
1717 1717 :relation_type => IssueRelation::TYPE_RELATES)
1718 1718
1719 1719 r = IssueRelation.create!(:issue_from => Issue.find(8),
1720 1720 :issue_to => Issue.find(7),
1721 1721 :relation_type => IssueRelation::TYPE_RELATES)
1722 1722 IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id])
1723 1723
1724 1724 r = IssueRelation.create!(:issue_from => Issue.find(3),
1725 1725 :issue_to => Issue.find(7),
1726 1726 :relation_type => IssueRelation::TYPE_RELATES)
1727 1727 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1728 1728
1729 1729 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1730 1730 end
1731 1731
1732 1732 test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do
1733 1733 @issue = Issue.find(1)
1734 1734 @issue_status = IssueStatus.find(1)
1735 1735 @issue_status.update_attribute(:default_done_ratio, 50)
1736 1736 @issue2 = Issue.find(2)
1737 1737 @issue_status2 = IssueStatus.find(2)
1738 1738 @issue_status2.update_attribute(:default_done_ratio, 0)
1739 1739
1740 1740 with_settings :issue_done_ratio => 'issue_field' do
1741 1741 assert_equal 0, @issue.done_ratio
1742 1742 assert_equal 30, @issue2.done_ratio
1743 1743 end
1744 1744
1745 1745 with_settings :issue_done_ratio => 'issue_status' do
1746 1746 assert_equal 50, @issue.done_ratio
1747 1747 assert_equal 0, @issue2.done_ratio
1748 1748 end
1749 1749 end
1750 1750
1751 1751 test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
1752 1752 @issue = Issue.find(1)
1753 1753 @issue_status = IssueStatus.find(1)
1754 1754 @issue_status.update_attribute(:default_done_ratio, 50)
1755 1755 @issue2 = Issue.find(2)
1756 1756 @issue_status2 = IssueStatus.find(2)
1757 1757 @issue_status2.update_attribute(:default_done_ratio, 0)
1758 1758
1759 1759 with_settings :issue_done_ratio => 'issue_field' do
1760 1760 @issue.update_done_ratio_from_issue_status
1761 1761 @issue2.update_done_ratio_from_issue_status
1762 1762
1763 1763 assert_equal 0, @issue.read_attribute(:done_ratio)
1764 1764 assert_equal 30, @issue2.read_attribute(:done_ratio)
1765 1765 end
1766 1766
1767 1767 with_settings :issue_done_ratio => 'issue_status' do
1768 1768 @issue.update_done_ratio_from_issue_status
1769 1769 @issue2.update_done_ratio_from_issue_status
1770 1770
1771 1771 assert_equal 50, @issue.read_attribute(:done_ratio)
1772 1772 assert_equal 0, @issue2.read_attribute(:done_ratio)
1773 1773 end
1774 1774 end
1775 1775
1776 1776 test "#by_tracker" do
1777 1777 User.current = User.anonymous
1778 1778 groups = Issue.by_tracker(Project.find(1))
1779 1779 assert_equal 3, groups.size
1780 1780 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1781 1781 end
1782 1782
1783 1783 test "#by_version" do
1784 1784 User.current = User.anonymous
1785 1785 groups = Issue.by_version(Project.find(1))
1786 1786 assert_equal 3, groups.size
1787 1787 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1788 1788 end
1789 1789
1790 1790 test "#by_priority" do
1791 1791 User.current = User.anonymous
1792 1792 groups = Issue.by_priority(Project.find(1))
1793 1793 assert_equal 4, groups.size
1794 1794 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1795 1795 end
1796 1796
1797 1797 test "#by_category" do
1798 1798 User.current = User.anonymous
1799 1799 groups = Issue.by_category(Project.find(1))
1800 1800 assert_equal 2, groups.size
1801 1801 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1802 1802 end
1803 1803
1804 1804 test "#by_assigned_to" do
1805 1805 User.current = User.anonymous
1806 1806 groups = Issue.by_assigned_to(Project.find(1))
1807 1807 assert_equal 2, groups.size
1808 1808 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1809 1809 end
1810 1810
1811 1811 test "#by_author" do
1812 1812 User.current = User.anonymous
1813 1813 groups = Issue.by_author(Project.find(1))
1814 1814 assert_equal 4, groups.size
1815 1815 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1816 1816 end
1817 1817
1818 1818 test "#by_subproject" do
1819 1819 User.current = User.anonymous
1820 1820 groups = Issue.by_subproject(Project.find(1))
1821 1821 # Private descendant not visible
1822 1822 assert_equal 1, groups.size
1823 1823 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1824 1824 end
1825 1825
1826 1826 def test_recently_updated_scope
1827 1827 #should return the last updated issue
1828 1828 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
1829 1829 end
1830 1830
1831 1831 def test_on_active_projects_scope
1832 1832 assert Project.find(2).archive
1833 1833
1834 1834 before = Issue.on_active_project.length
1835 1835 # test inclusion to results
1836 1836 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
1837 1837 assert_equal before + 1, Issue.on_active_project.length
1838 1838
1839 1839 # Move to an archived project
1840 1840 issue.project = Project.find(2)
1841 1841 assert issue.save
1842 1842 assert_equal before, Issue.on_active_project.length
1843 1843 end
1844 1844
1845 1845 test "Issue#recipients should include project recipients" do
1846 1846 issue = Issue.generate!
1847 1847 assert issue.project.recipients.present?
1848 1848 issue.project.recipients.each do |project_recipient|
1849 1849 assert issue.recipients.include?(project_recipient)
1850 1850 end
1851 1851 end
1852 1852
1853 1853 test "Issue#recipients should include the author if the author is active" do
1854 1854 issue = Issue.generate!(:author => User.generate!)
1855 1855 assert issue.author, "No author set for Issue"
1856 1856 assert issue.recipients.include?(issue.author.mail)
1857 1857 end
1858 1858
1859 1859 test "Issue#recipients should include the assigned to user if the assigned to user is active" do
1860 1860 issue = Issue.generate!(:assigned_to => User.generate!)
1861 1861 assert issue.assigned_to, "No assigned_to set for Issue"
1862 1862 assert issue.recipients.include?(issue.assigned_to.mail)
1863 1863 end
1864 1864
1865 1865 test "Issue#recipients should not include users who opt out of all email" do
1866 1866 issue = Issue.generate!(:author => User.generate!)
1867 1867 issue.author.update_attribute(:mail_notification, :none)
1868 1868 assert !issue.recipients.include?(issue.author.mail)
1869 1869 end
1870 1870
1871 1871 test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do
1872 1872 issue = Issue.generate!(:author => User.generate!)
1873 1873 issue.author.update_attribute(:mail_notification, :only_assigned)
1874 1874 assert !issue.recipients.include?(issue.author.mail)
1875 1875 end
1876 1876
1877 1877 test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do
1878 1878 issue = Issue.generate!(:assigned_to => User.generate!)
1879 1879 issue.assigned_to.update_attribute(:mail_notification, :only_owner)
1880 1880 assert !issue.recipients.include?(issue.assigned_to.mail)
1881 1881 end
1882 1882
1883 1883 def test_last_journal_id_with_journals_should_return_the_journal_id
1884 1884 assert_equal 2, Issue.find(1).last_journal_id
1885 1885 end
1886 1886
1887 1887 def test_last_journal_id_without_journals_should_return_nil
1888 1888 assert_nil Issue.find(3).last_journal_id
1889 1889 end
1890 1890
1891 1891 def test_journals_after_should_return_journals_with_greater_id
1892 1892 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
1893 1893 assert_equal [], Issue.find(1).journals_after('2')
1894 1894 end
1895 1895
1896 1896 def test_journals_after_with_blank_arg_should_return_all_journals
1897 1897 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
1898 1898 end
1899 1899
1900 1900 def test_css_classes_should_include_priority
1901 1901 issue = Issue.new(:priority => IssuePriority.find(8))
1902 1902 classes = issue.css_classes.split(' ')
1903 1903 assert_include 'priority-8', classes
1904 1904 assert_include 'priority-highest', classes
1905 1905 end
1906 1906
1907 1907 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
1908 1908 set_tmp_attachments_directory
1909 1909 issue = Issue.generate!
1910 1910 issue.save_attachments({
1911 1911 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
1912 1912 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
1913 1913 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
1914 1914 })
1915 1915 issue.attach_saved_attachments
1916 1916
1917 1917 assert_equal 3, issue.reload.attachments.count
1918 1918 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
1919 1919 end
1920
1921 def test_closed_on_should_be_nil_when_creating_an_open_issue
1922 issue = Issue.generate!(:status_id => 1).reload
1923 assert !issue.closed?
1924 assert_nil issue.closed_on
1925 end
1926
1927 def test_closed_on_should_be_set_when_creating_a_closed_issue
1928 issue = Issue.generate!(:status_id => 5).reload
1929 assert issue.closed?
1930 assert_not_nil issue.closed_on
1931 assert_equal issue.updated_on, issue.closed_on
1932 assert_equal issue.created_on, issue.closed_on
1933 end
1934
1935 def test_closed_on_should_be_nil_when_updating_an_open_issue
1936 issue = Issue.find(1)
1937 issue.subject = 'Not closed yet'
1938 issue.save!
1939 issue.reload
1940 assert_nil issue.closed_on
1941 end
1942
1943 def test_closed_on_should_be_set_when_closing_an_open_issue
1944 issue = Issue.find(1)
1945 issue.subject = 'Now closed'
1946 issue.status_id = 5
1947 issue.save!
1948 issue.reload
1949 assert_not_nil issue.closed_on
1950 assert_equal issue.updated_on, issue.closed_on
1951 end
1952
1953 def test_closed_on_should_not_be_updated_when_updating_a_closed_issue
1954 issue = Issue.open(false).first
1955 was_closed_on = issue.closed_on
1956 assert_not_nil was_closed_on
1957 issue.subject = 'Updating a closed issue'
1958 issue.save!
1959 issue.reload
1960 assert_equal was_closed_on, issue.closed_on
1961 end
1962
1963 def test_closed_on_should_be_preserved_when_reopening_a_closed_issue
1964 issue = Issue.open(false).first
1965 was_closed_on = issue.closed_on
1966 assert_not_nil was_closed_on
1967 issue.subject = 'Reopening a closed issue'
1968 issue.status_id = 1
1969 issue.save!
1970 issue.reload
1971 assert !issue.closed?
1972 assert_equal was_closed_on, issue.closed_on
1973 end
1920 1974 end
General Comments 0
You need to be logged in to leave comments. Login now