##// END OF EJS Templates
Fixed that members without view issues permission are able to list issues on public projects if the non member role has the permission (#20206)....
Jean-Philippe Lang -
r14068:1def32c4dda9
parent child
Show More
@@ -1,1007 +1,1011
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 Project < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 include Redmine::NestedSet::ProjectNestedSet
21 21
22 22 # Project statuses
23 23 STATUS_ACTIVE = 1
24 24 STATUS_CLOSED = 5
25 25 STATUS_ARCHIVED = 9
26 26
27 27 # Maximum length for project identifiers
28 28 IDENTIFIER_MAX_LENGTH = 100
29 29
30 30 # Specific overridden Activities
31 31 has_many :time_entry_activities
32 32 has_many :memberships, :class_name => 'Member', :inverse_of => :project
33 33 # Memberships of active users only
34 34 has_many :members,
35 35 lambda { joins(:principal).where(:users => {:type => 'User', :status => Principal::STATUS_ACTIVE}) }
36 36 has_many :enabled_modules, :dependent => :delete_all
37 37 has_and_belongs_to_many :trackers, lambda {order(:position)}
38 38 has_many :issues, :dependent => :destroy
39 39 has_many :issue_changes, :through => :issues, :source => :journals
40 40 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
41 41 has_many :time_entries, :dependent => :destroy
42 42 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
43 43 has_many :documents, :dependent => :destroy
44 44 has_many :news, lambda {includes(:author)}, :dependent => :destroy
45 45 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
46 46 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
47 47 has_one :repository, lambda {where(["is_default = ?", true])}
48 48 has_many :repositories, :dependent => :destroy
49 49 has_many :changesets, :through => :repository
50 50 has_one :wiki, :dependent => :destroy
51 51 # Custom field for the project issues
52 52 has_and_belongs_to_many :issue_custom_fields,
53 53 lambda {order("#{CustomField.table_name}.position")},
54 54 :class_name => 'IssueCustomField',
55 55 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 56 :association_foreign_key => 'custom_field_id'
57 57
58 58 acts_as_attachable :view_permission => :view_files,
59 59 :edit_permission => :manage_files,
60 60 :delete_permission => :manage_files
61 61
62 62 acts_as_customizable
63 63 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => "#{Project.table_name}.id", :permission => nil
64 64 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
65 65 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 66 :author => nil
67 67
68 68 attr_protected :status
69 69
70 70 validates_presence_of :name, :identifier
71 71 validates_uniqueness_of :identifier, :if => Proc.new {|p| p.identifier_changed?}
72 72 validates_length_of :name, :maximum => 255
73 73 validates_length_of :homepage, :maximum => 255
74 74 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
75 75 # downcase letters, digits, dashes but not digits only
76 76 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
77 77 # reserved words
78 78 validates_exclusion_of :identifier, :in => %w( new )
79 79 validate :validate_parent
80 80
81 81 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
82 82 after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?}
83 83 after_update :update_versions_from_hierarchy_change, :if => Proc.new {|project| project.parent_id_changed?}
84 84 before_destroy :delete_all_members
85 85
86 86 scope :has_module, lambda {|mod|
87 87 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
88 88 }
89 89 scope :active, lambda { where(:status => STATUS_ACTIVE) }
90 90 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
91 91 scope :all_public, lambda { where(:is_public => true) }
92 92 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
93 93 scope :allowed_to, lambda {|*args|
94 94 user = User.current
95 95 permission = nil
96 96 if args.first.is_a?(Symbol)
97 97 permission = args.shift
98 98 else
99 99 user = args.shift
100 100 permission = args.shift
101 101 end
102 102 where(Project.allowed_to_condition(user, permission, *args))
103 103 }
104 104 scope :like, lambda {|arg|
105 105 if arg.blank?
106 106 where(nil)
107 107 else
108 108 pattern = "%#{arg.to_s.strip.downcase}%"
109 109 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
110 110 end
111 111 }
112 112 scope :sorted, lambda {order(:lft)}
113 113
114 114 def initialize(attributes=nil, *args)
115 115 super
116 116
117 117 initialized = (attributes || {}).stringify_keys
118 118 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
119 119 self.identifier = Project.next_identifier
120 120 end
121 121 if !initialized.key?('is_public')
122 122 self.is_public = Setting.default_projects_public?
123 123 end
124 124 if !initialized.key?('enabled_module_names')
125 125 self.enabled_module_names = Setting.default_projects_modules
126 126 end
127 127 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
128 128 default = Setting.default_projects_tracker_ids
129 129 if default.is_a?(Array)
130 130 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
131 131 else
132 132 self.trackers = Tracker.sorted.to_a
133 133 end
134 134 end
135 135 end
136 136
137 137 def identifier=(identifier)
138 138 super unless identifier_frozen?
139 139 end
140 140
141 141 def identifier_frozen?
142 142 errors[:identifier].blank? && !(new_record? || identifier.blank?)
143 143 end
144 144
145 145 # returns latest created projects
146 146 # non public projects will be returned only if user is a member of those
147 147 def self.latest(user=nil, count=5)
148 148 visible(user).limit(count).order("created_on DESC").to_a
149 149 end
150 150
151 151 # Returns true if the project is visible to +user+ or to the current user.
152 152 def visible?(user=User.current)
153 153 user.allowed_to?(:view_project, self)
154 154 end
155 155
156 156 # Returns a SQL conditions string used to find all projects visible by the specified user.
157 157 #
158 158 # Examples:
159 159 # Project.visible_condition(admin) => "projects.status = 1"
160 160 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
161 161 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
162 162 def self.visible_condition(user, options={})
163 163 allowed_to_condition(user, :view_project, options)
164 164 end
165 165
166 166 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
167 167 #
168 168 # Valid options:
169 169 # * :project => limit the condition to project
170 170 # * :with_subprojects => limit the condition to project and its subprojects
171 171 # * :member => limit the condition to the user projects
172 172 def self.allowed_to_condition(user, permission, options={})
173 173 perm = Redmine::AccessControl.permission(permission)
174 174 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
175 175 if perm && perm.project_module
176 176 # If the permission belongs to a project module, make sure the module is enabled
177 177 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
178 178 end
179 179 if project = options[:project]
180 180 project_statement = project.project_condition(options[:with_subprojects])
181 181 base_statement = "(#{project_statement}) AND (#{base_statement})"
182 182 end
183 183
184 184 if user.admin?
185 185 base_statement
186 186 else
187 187 statement_by_role = {}
188 188 unless options[:member]
189 189 role = user.builtin_role
190 190 if role.allowed_to?(permission)
191 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
191 s = "#{Project.table_name}.is_public = #{connection.quoted_true}"
192 if user.id
193 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id = #{user.id}))"
194 end
195 statement_by_role[role] = s
192 196 end
193 197 end
194 198 user.projects_by_role.each do |role, projects|
195 199 if role.allowed_to?(permission) && projects.any?
196 200 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
197 201 end
198 202 end
199 203 if statement_by_role.empty?
200 204 "1=0"
201 205 else
202 206 if block_given?
203 207 statement_by_role.each do |role, statement|
204 208 if s = yield(role, user)
205 209 statement_by_role[role] = "(#{statement} AND (#{s}))"
206 210 end
207 211 end
208 212 end
209 213 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
210 214 end
211 215 end
212 216 end
213 217
214 218 def override_roles(role)
215 219 @override_members ||= memberships.
216 220 joins(:principal).
217 221 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
218 222
219 223 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
220 224 member = @override_members.detect {|m| m.principal.is_a? group_class}
221 225 member ? member.roles.to_a : [role]
222 226 end
223 227
224 228 def principals
225 229 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
226 230 end
227 231
228 232 def users
229 233 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
230 234 end
231 235
232 236 # Returns the Systemwide and project specific activities
233 237 def activities(include_inactive=false)
234 238 t = TimeEntryActivity.table_name
235 239 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
236 240
237 241 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
238 242 if overridden_activity_ids.any?
239 243 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
240 244 end
241 245 unless include_inactive
242 246 scope = scope.active
243 247 end
244 248 scope
245 249 end
246 250
247 251 # Will create a new Project specific Activity or update an existing one
248 252 #
249 253 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
250 254 # does not successfully save.
251 255 def update_or_create_time_entry_activity(id, activity_hash)
252 256 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
253 257 self.create_time_entry_activity_if_needed(activity_hash)
254 258 else
255 259 activity = project.time_entry_activities.find_by_id(id.to_i)
256 260 activity.update_attributes(activity_hash) if activity
257 261 end
258 262 end
259 263
260 264 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
261 265 #
262 266 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
263 267 # does not successfully save.
264 268 def create_time_entry_activity_if_needed(activity)
265 269 if activity['parent_id']
266 270 parent_activity = TimeEntryActivity.find(activity['parent_id'])
267 271 activity['name'] = parent_activity.name
268 272 activity['position'] = parent_activity.position
269 273 if Enumeration.overriding_change?(activity, parent_activity)
270 274 project_activity = self.time_entry_activities.create(activity)
271 275 if project_activity.new_record?
272 276 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
273 277 else
274 278 self.time_entries.
275 279 where(["activity_id = ?", parent_activity.id]).
276 280 update_all("activity_id = #{project_activity.id}")
277 281 end
278 282 end
279 283 end
280 284 end
281 285
282 286 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
283 287 #
284 288 # Examples:
285 289 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
286 290 # project.project_condition(false) => "projects.id = 1"
287 291 def project_condition(with_subprojects)
288 292 cond = "#{Project.table_name}.id = #{id}"
289 293 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
290 294 cond
291 295 end
292 296
293 297 def self.find(*args)
294 298 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
295 299 project = find_by_identifier(*args)
296 300 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
297 301 project
298 302 else
299 303 super
300 304 end
301 305 end
302 306
303 307 def self.find_by_param(*args)
304 308 self.find(*args)
305 309 end
306 310
307 311 alias :base_reload :reload
308 312 def reload(*args)
309 313 @principals = nil
310 314 @users = nil
311 315 @shared_versions = nil
312 316 @rolled_up_versions = nil
313 317 @rolled_up_trackers = nil
314 318 @all_issue_custom_fields = nil
315 319 @all_time_entry_custom_fields = nil
316 320 @to_param = nil
317 321 @allowed_parents = nil
318 322 @allowed_permissions = nil
319 323 @actions_allowed = nil
320 324 @start_date = nil
321 325 @due_date = nil
322 326 @override_members = nil
323 327 @assignable_users = nil
324 328 base_reload(*args)
325 329 end
326 330
327 331 def to_param
328 332 # id is used for projects with a numeric identifier (compatibility)
329 333 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
330 334 end
331 335
332 336 def active?
333 337 self.status == STATUS_ACTIVE
334 338 end
335 339
336 340 def archived?
337 341 self.status == STATUS_ARCHIVED
338 342 end
339 343
340 344 # Archives the project and its descendants
341 345 def archive
342 346 # Check that there is no issue of a non descendant project that is assigned
343 347 # to one of the project or descendant versions
344 348 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
345 349
346 350 if version_ids.any? &&
347 351 Issue.
348 352 includes(:project).
349 353 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
350 354 where(:fixed_version_id => version_ids).
351 355 exists?
352 356 return false
353 357 end
354 358 Project.transaction do
355 359 archive!
356 360 end
357 361 true
358 362 end
359 363
360 364 # Unarchives the project
361 365 # All its ancestors must be active
362 366 def unarchive
363 367 return false if ancestors.detect {|a| !a.active?}
364 368 update_attribute :status, STATUS_ACTIVE
365 369 end
366 370
367 371 def close
368 372 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
369 373 end
370 374
371 375 def reopen
372 376 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
373 377 end
374 378
375 379 # Returns an array of projects the project can be moved to
376 380 # by the current user
377 381 def allowed_parents(user=User.current)
378 382 return @allowed_parents if @allowed_parents
379 383 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
380 384 @allowed_parents = @allowed_parents - self_and_descendants
381 385 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
382 386 @allowed_parents << nil
383 387 end
384 388 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
385 389 @allowed_parents << parent
386 390 end
387 391 @allowed_parents
388 392 end
389 393
390 394 # Sets the parent of the project with authorization check
391 395 def set_allowed_parent!(p)
392 396 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
393 397 p = p.id if p.is_a?(Project)
394 398 send :safe_attributes, {:project_id => p}
395 399 save
396 400 end
397 401
398 402 # Sets the parent of the project and saves the project
399 403 # Argument can be either a Project, a String, a Fixnum or nil
400 404 def set_parent!(p)
401 405 if p.is_a?(Project)
402 406 self.parent = p
403 407 else
404 408 self.parent_id = p
405 409 end
406 410 save
407 411 end
408 412
409 413 # Returns an array of the trackers used by the project and its active sub projects
410 414 def rolled_up_trackers
411 415 @rolled_up_trackers ||=
412 416 Tracker.
413 417 joins(:projects).
414 418 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
415 419 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED).
416 420 uniq.
417 421 sorted.
418 422 to_a
419 423 end
420 424
421 425 # Closes open and locked project versions that are completed
422 426 def close_completed_versions
423 427 Version.transaction do
424 428 versions.where(:status => %w(open locked)).each do |version|
425 429 if version.completed?
426 430 version.update_attribute(:status, 'closed')
427 431 end
428 432 end
429 433 end
430 434 end
431 435
432 436 # Returns a scope of the Versions on subprojects
433 437 def rolled_up_versions
434 438 @rolled_up_versions ||=
435 439 Version.
436 440 joins(:project).
437 441 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
438 442 end
439 443
440 444 # Returns a scope of the Versions used by the project
441 445 def shared_versions
442 446 if new_record?
443 447 Version.
444 448 joins(:project).
445 449 preload(:project).
446 450 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
447 451 else
448 452 @shared_versions ||= begin
449 453 r = root? ? self : root
450 454 Version.
451 455 joins(:project).
452 456 preload(:project).
453 457 where("#{Project.table_name}.id = #{id}" +
454 458 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
455 459 " #{Version.table_name}.sharing = 'system'" +
456 460 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
457 461 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
458 462 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
459 463 "))")
460 464 end
461 465 end
462 466 end
463 467
464 468 # Returns a hash of project users grouped by role
465 469 def users_by_role
466 470 members.includes(:user, :roles).inject({}) do |h, m|
467 471 m.roles.each do |r|
468 472 h[r] ||= []
469 473 h[r] << m.user
470 474 end
471 475 h
472 476 end
473 477 end
474 478
475 479 # Adds user as a project member with the default role
476 480 # Used for when a non-admin user creates a project
477 481 def add_default_member(user)
478 482 role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
479 483 member = Member.new(:project => self, :principal => user, :roles => [role])
480 484 self.members << member
481 485 member
482 486 end
483 487
484 488 # Deletes all project's members
485 489 def delete_all_members
486 490 me, mr = Member.table_name, MemberRole.table_name
487 491 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
488 492 Member.delete_all(['project_id = ?', id])
489 493 end
490 494
491 495 # Return a Principal scope of users/groups issues can be assigned to
492 496 def assignable_users
493 497 types = ['User']
494 498 types << 'Group' if Setting.issue_group_assignment?
495 499
496 500 @assignable_users ||= Principal.
497 501 active.
498 502 joins(:members => :roles).
499 503 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
500 504 uniq.
501 505 sorted
502 506 end
503 507
504 508 # Returns the mail addresses of users that should be always notified on project events
505 509 def recipients
506 510 notified_users.collect {|user| user.mail}
507 511 end
508 512
509 513 # Returns the users that should be notified on project events
510 514 def notified_users
511 515 # TODO: User part should be extracted to User#notify_about?
512 516 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
513 517 end
514 518
515 519 # Returns a scope of all custom fields enabled for project issues
516 520 # (explicitly associated custom fields and custom fields enabled for all projects)
517 521 def all_issue_custom_fields
518 522 @all_issue_custom_fields ||= IssueCustomField.
519 523 sorted.
520 524 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
521 525 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
522 526 " WHERE cfp.project_id = ?)", true, id)
523 527 end
524 528
525 529 def project
526 530 self
527 531 end
528 532
529 533 def <=>(project)
530 534 name.downcase <=> project.name.downcase
531 535 end
532 536
533 537 def to_s
534 538 name
535 539 end
536 540
537 541 # Returns a short description of the projects (first lines)
538 542 def short_description(length = 255)
539 543 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
540 544 end
541 545
542 546 def css_classes
543 547 s = 'project'
544 548 s << ' root' if root?
545 549 s << ' child' if child?
546 550 s << (leaf? ? ' leaf' : ' parent')
547 551 unless active?
548 552 if archived?
549 553 s << ' archived'
550 554 else
551 555 s << ' closed'
552 556 end
553 557 end
554 558 s
555 559 end
556 560
557 561 # The earliest start date of a project, based on it's issues and versions
558 562 def start_date
559 563 @start_date ||= [
560 564 issues.minimum('start_date'),
561 565 shared_versions.minimum('effective_date'),
562 566 Issue.fixed_version(shared_versions).minimum('start_date')
563 567 ].compact.min
564 568 end
565 569
566 570 # The latest due date of an issue or version
567 571 def due_date
568 572 @due_date ||= [
569 573 issues.maximum('due_date'),
570 574 shared_versions.maximum('effective_date'),
571 575 Issue.fixed_version(shared_versions).maximum('due_date')
572 576 ].compact.max
573 577 end
574 578
575 579 def overdue?
576 580 active? && !due_date.nil? && (due_date < Date.today)
577 581 end
578 582
579 583 # Returns the percent completed for this project, based on the
580 584 # progress on it's versions.
581 585 def completed_percent(options={:include_subprojects => false})
582 586 if options.delete(:include_subprojects)
583 587 total = self_and_descendants.collect(&:completed_percent).sum
584 588
585 589 total / self_and_descendants.count
586 590 else
587 591 if versions.count > 0
588 592 total = versions.collect(&:completed_percent).sum
589 593
590 594 total / versions.count
591 595 else
592 596 100
593 597 end
594 598 end
595 599 end
596 600
597 601 # Return true if this project allows to do the specified action.
598 602 # action can be:
599 603 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
600 604 # * a permission Symbol (eg. :edit_project)
601 605 def allows_to?(action)
602 606 if archived?
603 607 # No action allowed on archived projects
604 608 return false
605 609 end
606 610 unless active? || Redmine::AccessControl.read_action?(action)
607 611 # No write action allowed on closed projects
608 612 return false
609 613 end
610 614 # No action allowed on disabled modules
611 615 if action.is_a? Hash
612 616 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
613 617 else
614 618 allowed_permissions.include? action
615 619 end
616 620 end
617 621
618 622 # Return the enabled module with the given name
619 623 # or nil if the module is not enabled for the project
620 624 def enabled_module(name)
621 625 name = name.to_s
622 626 enabled_modules.detect {|m| m.name == name}
623 627 end
624 628
625 629 # Return true if the module with the given name is enabled
626 630 def module_enabled?(name)
627 631 enabled_module(name).present?
628 632 end
629 633
630 634 def enabled_module_names=(module_names)
631 635 if module_names && module_names.is_a?(Array)
632 636 module_names = module_names.collect(&:to_s).reject(&:blank?)
633 637 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
634 638 else
635 639 enabled_modules.clear
636 640 end
637 641 end
638 642
639 643 # Returns an array of the enabled modules names
640 644 def enabled_module_names
641 645 enabled_modules.collect(&:name)
642 646 end
643 647
644 648 # Enable a specific module
645 649 #
646 650 # Examples:
647 651 # project.enable_module!(:issue_tracking)
648 652 # project.enable_module!("issue_tracking")
649 653 def enable_module!(name)
650 654 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
651 655 end
652 656
653 657 # Disable a module if it exists
654 658 #
655 659 # Examples:
656 660 # project.disable_module!(:issue_tracking)
657 661 # project.disable_module!("issue_tracking")
658 662 # project.disable_module!(project.enabled_modules.first)
659 663 def disable_module!(target)
660 664 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
661 665 target.destroy unless target.blank?
662 666 end
663 667
664 668 safe_attributes 'name',
665 669 'description',
666 670 'homepage',
667 671 'is_public',
668 672 'identifier',
669 673 'custom_field_values',
670 674 'custom_fields',
671 675 'tracker_ids',
672 676 'issue_custom_field_ids',
673 677 'parent_id'
674 678
675 679 safe_attributes 'enabled_module_names',
676 680 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
677 681
678 682 safe_attributes 'inherit_members',
679 683 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
680 684
681 685 def safe_attributes=(attrs, user=User.current)
682 686 return unless attrs.is_a?(Hash)
683 687 attrs = attrs.deep_dup
684 688
685 689 @unallowed_parent_id = nil
686 690 parent_id_param = attrs['parent_id'].to_s
687 691 if parent_id_param.blank? || parent_id_param != parent_id.to_s
688 692 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
689 693 unless allowed_parents(user).include?(p)
690 694 attrs.delete('parent_id')
691 695 @unallowed_parent_id = true
692 696 end
693 697 end
694 698
695 699 super(attrs, user)
696 700 end
697 701
698 702 # Returns an auto-generated project identifier based on the last identifier used
699 703 def self.next_identifier
700 704 p = Project.order('id DESC').first
701 705 p.nil? ? nil : p.identifier.to_s.succ
702 706 end
703 707
704 708 # Copies and saves the Project instance based on the +project+.
705 709 # Duplicates the source project's:
706 710 # * Wiki
707 711 # * Versions
708 712 # * Categories
709 713 # * Issues
710 714 # * Members
711 715 # * Queries
712 716 #
713 717 # Accepts an +options+ argument to specify what to copy
714 718 #
715 719 # Examples:
716 720 # project.copy(1) # => copies everything
717 721 # project.copy(1, :only => 'members') # => copies members only
718 722 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
719 723 def copy(project, options={})
720 724 project = project.is_a?(Project) ? project : Project.find(project)
721 725
722 726 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
723 727 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
724 728
725 729 Project.transaction do
726 730 if save
727 731 reload
728 732 to_be_copied.each do |name|
729 733 send "copy_#{name}", project
730 734 end
731 735 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
732 736 save
733 737 else
734 738 false
735 739 end
736 740 end
737 741 end
738 742
739 743 def member_principals
740 744 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
741 745 memberships.active
742 746 end
743 747
744 748 # Returns a new unsaved Project instance with attributes copied from +project+
745 749 def self.copy_from(project)
746 750 project = project.is_a?(Project) ? project : Project.find(project)
747 751 # clear unique attributes
748 752 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
749 753 copy = Project.new(attributes)
750 754 copy.enabled_modules = project.enabled_modules
751 755 copy.trackers = project.trackers
752 756 copy.custom_values = project.custom_values.collect {|v| v.clone}
753 757 copy.issue_custom_fields = project.issue_custom_fields
754 758 copy
755 759 end
756 760
757 761 # Yields the given block for each project with its level in the tree
758 762 def self.project_tree(projects, &block)
759 763 ancestors = []
760 764 projects.sort_by(&:lft).each do |project|
761 765 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
762 766 ancestors.pop
763 767 end
764 768 yield project, ancestors.size
765 769 ancestors << project
766 770 end
767 771 end
768 772
769 773 private
770 774
771 775 def update_inherited_members
772 776 if parent
773 777 if inherit_members? && !inherit_members_was
774 778 remove_inherited_member_roles
775 779 add_inherited_member_roles
776 780 elsif !inherit_members? && inherit_members_was
777 781 remove_inherited_member_roles
778 782 end
779 783 end
780 784 end
781 785
782 786 def remove_inherited_member_roles
783 787 member_roles = memberships.map(&:member_roles).flatten
784 788 member_role_ids = member_roles.map(&:id)
785 789 member_roles.each do |member_role|
786 790 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
787 791 member_role.destroy
788 792 end
789 793 end
790 794 end
791 795
792 796 def add_inherited_member_roles
793 797 if inherit_members? && parent
794 798 parent.memberships.each do |parent_member|
795 799 member = Member.find_or_new(self.id, parent_member.user_id)
796 800 parent_member.member_roles.each do |parent_member_role|
797 801 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
798 802 end
799 803 member.save!
800 804 end
801 805 memberships.reset
802 806 end
803 807 end
804 808
805 809 def update_versions_from_hierarchy_change
806 810 Issue.update_versions_from_hierarchy_change(self)
807 811 end
808 812
809 813 def validate_parent
810 814 if @unallowed_parent_id
811 815 errors.add(:parent_id, :invalid)
812 816 elsif parent_id_changed?
813 817 unless parent.nil? || (parent.active? && move_possible?(parent))
814 818 errors.add(:parent_id, :invalid)
815 819 end
816 820 end
817 821 end
818 822
819 823 # Copies wiki from +project+
820 824 def copy_wiki(project)
821 825 # Check that the source project has a wiki first
822 826 unless project.wiki.nil?
823 827 wiki = self.wiki || Wiki.new
824 828 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
825 829 wiki_pages_map = {}
826 830 project.wiki.pages.each do |page|
827 831 # Skip pages without content
828 832 next if page.content.nil?
829 833 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
830 834 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
831 835 new_wiki_page.content = new_wiki_content
832 836 wiki.pages << new_wiki_page
833 837 wiki_pages_map[page.id] = new_wiki_page
834 838 end
835 839
836 840 self.wiki = wiki
837 841 wiki.save
838 842 # Reproduce page hierarchy
839 843 project.wiki.pages.each do |page|
840 844 if page.parent_id && wiki_pages_map[page.id]
841 845 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
842 846 wiki_pages_map[page.id].save
843 847 end
844 848 end
845 849 end
846 850 end
847 851
848 852 # Copies versions from +project+
849 853 def copy_versions(project)
850 854 project.versions.each do |version|
851 855 new_version = Version.new
852 856 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
853 857 self.versions << new_version
854 858 end
855 859 end
856 860
857 861 # Copies issue categories from +project+
858 862 def copy_issue_categories(project)
859 863 project.issue_categories.each do |issue_category|
860 864 new_issue_category = IssueCategory.new
861 865 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
862 866 self.issue_categories << new_issue_category
863 867 end
864 868 end
865 869
866 870 # Copies issues from +project+
867 871 def copy_issues(project)
868 872 # Stores the source issue id as a key and the copied issues as the
869 873 # value. Used to map the two together for issue relations.
870 874 issues_map = {}
871 875
872 876 # Store status and reopen locked/closed versions
873 877 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
874 878 version_statuses.each do |version, status|
875 879 version.update_attribute :status, 'open'
876 880 end
877 881
878 882 # Get issues sorted by root_id, lft so that parent issues
879 883 # get copied before their children
880 884 project.issues.reorder('root_id, lft').each do |issue|
881 885 new_issue = Issue.new
882 886 new_issue.copy_from(issue, :subtasks => false, :link => false)
883 887 new_issue.project = self
884 888 # Changing project resets the custom field values
885 889 # TODO: handle this in Issue#project=
886 890 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
887 891 # Reassign fixed_versions by name, since names are unique per project
888 892 if issue.fixed_version && issue.fixed_version.project == project
889 893 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
890 894 end
891 895 # Reassign the category by name, since names are unique per project
892 896 if issue.category
893 897 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
894 898 end
895 899 # Parent issue
896 900 if issue.parent_id
897 901 if copied_parent = issues_map[issue.parent_id]
898 902 new_issue.parent_issue_id = copied_parent.id
899 903 end
900 904 end
901 905
902 906 self.issues << new_issue
903 907 if new_issue.new_record?
904 908 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
905 909 else
906 910 issues_map[issue.id] = new_issue unless new_issue.new_record?
907 911 end
908 912 end
909 913
910 914 # Restore locked/closed version statuses
911 915 version_statuses.each do |version, status|
912 916 version.update_attribute :status, status
913 917 end
914 918
915 919 # Relations after in case issues related each other
916 920 project.issues.each do |issue|
917 921 new_issue = issues_map[issue.id]
918 922 unless new_issue
919 923 # Issue was not copied
920 924 next
921 925 end
922 926
923 927 # Relations
924 928 issue.relations_from.each do |source_relation|
925 929 new_issue_relation = IssueRelation.new
926 930 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
927 931 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
928 932 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
929 933 new_issue_relation.issue_to = source_relation.issue_to
930 934 end
931 935 new_issue.relations_from << new_issue_relation
932 936 end
933 937
934 938 issue.relations_to.each do |source_relation|
935 939 new_issue_relation = IssueRelation.new
936 940 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
937 941 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
938 942 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
939 943 new_issue_relation.issue_from = source_relation.issue_from
940 944 end
941 945 new_issue.relations_to << new_issue_relation
942 946 end
943 947 end
944 948 end
945 949
946 950 # Copies members from +project+
947 951 def copy_members(project)
948 952 # Copy users first, then groups to handle members with inherited and given roles
949 953 members_to_copy = []
950 954 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
951 955 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
952 956
953 957 members_to_copy.each do |member|
954 958 new_member = Member.new
955 959 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
956 960 # only copy non inherited roles
957 961 # inherited roles will be added when copying the group membership
958 962 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
959 963 next if role_ids.empty?
960 964 new_member.role_ids = role_ids
961 965 new_member.project = self
962 966 self.members << new_member
963 967 end
964 968 end
965 969
966 970 # Copies queries from +project+
967 971 def copy_queries(project)
968 972 project.queries.each do |query|
969 973 new_query = IssueQuery.new
970 974 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
971 975 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
972 976 new_query.project = self
973 977 new_query.user_id = query.user_id
974 978 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
975 979 self.queries << new_query
976 980 end
977 981 end
978 982
979 983 # Copies boards from +project+
980 984 def copy_boards(project)
981 985 project.boards.each do |board|
982 986 new_board = Board.new
983 987 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
984 988 new_board.project = self
985 989 self.boards << new_board
986 990 end
987 991 end
988 992
989 993 def allowed_permissions
990 994 @allowed_permissions ||= begin
991 995 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
992 996 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
993 997 end
994 998 end
995 999
996 1000 def allowed_actions
997 1001 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
998 1002 end
999 1003
1000 1004 # Archives subprojects recursively
1001 1005 def archive!
1002 1006 children.each do |subproject|
1003 1007 subproject.send :archive!
1004 1008 end
1005 1009 update_attribute :status, STATUS_ARCHIVED
1006 1010 end
1007 1011 end
@@ -1,2704 +1,2713
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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, :email_addresses, :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 setup
35 35 set_language_if_valid 'en'
36 36 end
37 37
38 38 def teardown
39 39 User.current = nil
40 40 end
41 41
42 42 def test_initialize
43 43 issue = Issue.new
44 44
45 45 assert_nil issue.project_id
46 46 assert_nil issue.tracker_id
47 47 assert_nil issue.status_id
48 48 assert_nil issue.author_id
49 49 assert_nil issue.assigned_to_id
50 50 assert_nil issue.category_id
51 51
52 52 assert_equal IssuePriority.default, issue.priority
53 53 end
54 54
55 55 def test_create
56 56 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
57 57 :status_id => 1, :priority => IssuePriority.all.first,
58 58 :subject => 'test_create',
59 59 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
60 60 assert issue.save
61 61 issue.reload
62 62 assert_equal 1.5, issue.estimated_hours
63 63 end
64 64
65 65 def test_create_minimal
66 66 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :subject => 'test_create')
67 67 assert issue.save
68 68 assert_equal issue.tracker.default_status, issue.status
69 69 assert issue.description.nil?
70 70 assert_nil issue.estimated_hours
71 71 end
72 72
73 73 def test_create_with_all_fields_disabled
74 74 tracker = Tracker.find(1)
75 75 tracker.core_fields = []
76 76 tracker.save!
77 77
78 78 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :subject => 'test_create_with_all_fields_disabled')
79 79 assert_save issue
80 80 end
81 81
82 82 def test_start_date_format_should_be_validated
83 83 set_language_if_valid 'en'
84 84 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
85 85 issue = Issue.new(:start_date => invalid_date)
86 86 assert !issue.valid?
87 87 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
88 88 end
89 89 end
90 90
91 91 def test_due_date_format_should_be_validated
92 92 set_language_if_valid 'en'
93 93 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
94 94 issue = Issue.new(:due_date => invalid_date)
95 95 assert !issue.valid?
96 96 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
97 97 end
98 98 end
99 99
100 100 def test_due_date_lesser_than_start_date_should_not_validate
101 101 set_language_if_valid 'en'
102 102 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
103 103 assert !issue.valid?
104 104 assert_include 'Due date must be greater than start date', issue.errors.full_messages
105 105 end
106 106
107 107 def test_start_date_lesser_than_soonest_start_should_not_validate_on_create
108 108 issue = Issue.generate(:start_date => '2013-06-04')
109 109 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
110 110 assert !issue.valid?
111 111 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
112 112 end
113 113
114 114 def test_start_date_lesser_than_soonest_start_should_not_validate_on_update_if_changed
115 115 issue = Issue.generate!(:start_date => '2013-06-04')
116 116 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
117 117 issue.start_date = '2013-06-07'
118 118 assert !issue.valid?
119 119 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
120 120 end
121 121
122 122 def test_start_date_lesser_than_soonest_start_should_validate_on_update_if_unchanged
123 123 issue = Issue.generate!(:start_date => '2013-06-04')
124 124 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
125 125 assert issue.valid?
126 126 end
127 127
128 128 def test_estimated_hours_should_be_validated
129 129 set_language_if_valid 'en'
130 130 ['-2'].each do |invalid|
131 131 issue = Issue.new(:estimated_hours => invalid)
132 132 assert !issue.valid?
133 133 assert_include 'Estimated time is invalid', issue.errors.full_messages
134 134 end
135 135 end
136 136
137 137 def test_create_with_required_custom_field
138 138 set_language_if_valid 'en'
139 139 field = IssueCustomField.find_by_name('Database')
140 140 field.update_attribute(:is_required, true)
141 141
142 142 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
143 143 :status_id => 1, :subject => 'test_create',
144 144 :description => 'IssueTest#test_create_with_required_custom_field')
145 145 assert issue.available_custom_fields.include?(field)
146 146 # No value for the custom field
147 147 assert !issue.save
148 148 assert_equal ["Database cannot be blank"], issue.errors.full_messages
149 149 # Blank value
150 150 issue.custom_field_values = { field.id => '' }
151 151 assert !issue.save
152 152 assert_equal ["Database cannot be blank"], issue.errors.full_messages
153 153 # Invalid value
154 154 issue.custom_field_values = { field.id => 'SQLServer' }
155 155 assert !issue.save
156 156 assert_equal ["Database is not included in the list"], issue.errors.full_messages
157 157 # Valid value
158 158 issue.custom_field_values = { field.id => 'PostgreSQL' }
159 159 assert issue.save
160 160 issue.reload
161 161 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
162 162 end
163 163
164 164 def test_create_with_group_assignment
165 165 with_settings :issue_group_assignment => '1' do
166 166 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
167 167 :subject => 'Group assignment',
168 168 :assigned_to_id => 11).save
169 169 issue = Issue.order('id DESC').first
170 170 assert_kind_of Group, issue.assigned_to
171 171 assert_equal Group.find(11), issue.assigned_to
172 172 end
173 173 end
174 174
175 175 def test_create_with_parent_issue_id
176 176 issue = Issue.new(:project_id => 1, :tracker_id => 1,
177 177 :author_id => 1, :subject => 'Group assignment',
178 178 :parent_issue_id => 1)
179 179 assert_save issue
180 180 assert_equal 1, issue.parent_issue_id
181 181 assert_equal Issue.find(1), issue.parent
182 182 end
183 183
184 184 def test_create_with_sharp_parent_issue_id
185 185 issue = Issue.new(:project_id => 1, :tracker_id => 1,
186 186 :author_id => 1, :subject => 'Group assignment',
187 187 :parent_issue_id => "#1")
188 188 assert_save issue
189 189 assert_equal 1, issue.parent_issue_id
190 190 assert_equal Issue.find(1), issue.parent
191 191 end
192 192
193 193 def test_create_with_invalid_parent_issue_id
194 194 set_language_if_valid 'en'
195 195 issue = Issue.new(:project_id => 1, :tracker_id => 1,
196 196 :author_id => 1, :subject => 'Group assignment',
197 197 :parent_issue_id => '01ABC')
198 198 assert !issue.save
199 199 assert_equal '01ABC', issue.parent_issue_id
200 200 assert_include 'Parent task is invalid', issue.errors.full_messages
201 201 end
202 202
203 203 def test_create_with_invalid_sharp_parent_issue_id
204 204 set_language_if_valid 'en'
205 205 issue = Issue.new(:project_id => 1, :tracker_id => 1,
206 206 :author_id => 1, :subject => 'Group assignment',
207 207 :parent_issue_id => '#01ABC')
208 208 assert !issue.save
209 209 assert_equal '#01ABC', issue.parent_issue_id
210 210 assert_include 'Parent task is invalid', issue.errors.full_messages
211 211 end
212 212
213 213 def assert_visibility_match(user, issues)
214 214 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
215 215 end
216 216
217 217 def test_visible_scope_for_anonymous
218 218 # Anonymous user should see issues of public projects only
219 219 issues = Issue.visible(User.anonymous).to_a
220 220 assert issues.any?
221 221 assert_nil issues.detect {|issue| !issue.project.is_public?}
222 222 assert_nil issues.detect {|issue| issue.is_private?}
223 223 assert_visibility_match User.anonymous, issues
224 224 end
225 225
226 226 def test_visible_scope_for_anonymous_without_view_issues_permissions
227 227 # Anonymous user should not see issues without permission
228 228 Role.anonymous.remove_permission!(:view_issues)
229 229 issues = Issue.visible(User.anonymous).to_a
230 230 assert issues.empty?
231 231 assert_visibility_match User.anonymous, issues
232 232 end
233 233
234 234 def test_visible_scope_for_anonymous_without_view_issues_permissions_and_membership
235 235 Role.anonymous.remove_permission!(:view_issues)
236 236 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [2])
237 237
238 238 issues = Issue.visible(User.anonymous).all
239 239 assert issues.any?
240 240 assert_equal [1], issues.map(&:project_id).uniq.sort
241 241 assert_visibility_match User.anonymous, issues
242 242 end
243 243
244 244 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
245 245 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
246 246 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
247 247 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
248 248 assert !issue.visible?(User.anonymous)
249 249 end
250 250
251 251 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
252 252 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
253 253 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
254 254 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
255 255 assert !issue.visible?(User.anonymous)
256 256 end
257 257
258 258 def test_visible_scope_for_non_member
259 259 user = User.find(9)
260 260 assert user.projects.empty?
261 261 # Non member user should see issues of public projects only
262 262 issues = Issue.visible(user).to_a
263 263 assert issues.any?
264 264 assert_nil issues.detect {|issue| !issue.project.is_public?}
265 265 assert_nil issues.detect {|issue| issue.is_private?}
266 266 assert_visibility_match user, issues
267 267 end
268 268
269 269 def test_visible_scope_for_non_member_with_own_issues_visibility
270 270 Role.non_member.update_attribute :issues_visibility, 'own'
271 271 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
272 272 user = User.find(9)
273 273
274 274 issues = Issue.visible(user).to_a
275 275 assert issues.any?
276 276 assert_nil issues.detect {|issue| issue.author != user}
277 277 assert_visibility_match user, issues
278 278 end
279 279
280 280 def test_visible_scope_for_non_member_without_view_issues_permissions
281 281 # Non member user should not see issues without permission
282 282 Role.non_member.remove_permission!(:view_issues)
283 283 user = User.find(9)
284 284 assert user.projects.empty?
285 285 issues = Issue.visible(user).to_a
286 286 assert issues.empty?
287 287 assert_visibility_match user, issues
288 288 end
289 289
290 290 def test_visible_scope_for_non_member_without_view_issues_permissions_and_membership
291 291 Role.non_member.remove_permission!(:view_issues)
292 292 Member.create!(:project_id => 1, :principal => Group.non_member, :role_ids => [2])
293 293 user = User.find(9)
294 294
295 295 issues = Issue.visible(user).all
296 296 assert issues.any?
297 297 assert_equal [1], issues.map(&:project_id).uniq.sort
298 298 assert_visibility_match user, issues
299 299 end
300 300
301 301 def test_visible_scope_for_member
302 302 user = User.find(9)
303 303 # User should see issues of projects for which user has view_issues permissions only
304 304 Role.non_member.remove_permission!(:view_issues)
305 305 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
306 306 issues = Issue.visible(user).to_a
307 307 assert issues.any?
308 308 assert_nil issues.detect {|issue| issue.project_id != 3}
309 309 assert_nil issues.detect {|issue| issue.is_private?}
310 310 assert_visibility_match user, issues
311 311 end
312 312
313 def test_visible_scope_for_member_without_view_issues_permission_and_non_member_role_having_the_permission
314 Role.non_member.add_permission!(:view_issues)
315 Role.find(1).remove_permission!(:view_issues)
316 user = User.find(2)
317
318 assert_equal 0, Issue.where(:project_id => 1).visible(user).count
319 assert_equal false, Issue.where(:project_id => 1).first.visible?(user)
320 end
321
313 322 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
314 323 user = User.find(8)
315 324 assert user.groups.any?
316 325 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
317 326 Role.non_member.remove_permission!(:view_issues)
318 327
319 328 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 3,
320 329 :status_id => 1, :priority => IssuePriority.all.first,
321 330 :subject => 'Assignment test',
322 331 :assigned_to => user.groups.first,
323 332 :is_private => true)
324 333
325 334 Role.find(2).update_attribute :issues_visibility, 'default'
326 335 issues = Issue.visible(User.find(8)).to_a
327 336 assert issues.any?
328 337 assert issues.include?(issue)
329 338
330 339 Role.find(2).update_attribute :issues_visibility, 'own'
331 340 issues = Issue.visible(User.find(8)).to_a
332 341 assert issues.any?
333 342 assert_include issue, issues
334 343 end
335 344
336 345 def test_visible_scope_for_admin
337 346 user = User.find(1)
338 347 user.members.each(&:destroy)
339 348 assert user.projects.empty?
340 349 issues = Issue.visible(user).to_a
341 350 assert issues.any?
342 351 # Admin should see issues on private projects that admin does not belong to
343 352 assert issues.detect {|issue| !issue.project.is_public?}
344 353 # Admin should see private issues of other users
345 354 assert issues.detect {|issue| issue.is_private? && issue.author != user}
346 355 assert_visibility_match user, issues
347 356 end
348 357
349 358 def test_visible_scope_with_project
350 359 project = Project.find(1)
351 360 issues = Issue.visible(User.find(2), :project => project).to_a
352 361 projects = issues.collect(&:project).uniq
353 362 assert_equal 1, projects.size
354 363 assert_equal project, projects.first
355 364 end
356 365
357 366 def test_visible_scope_with_project_and_subprojects
358 367 project = Project.find(1)
359 368 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).to_a
360 369 projects = issues.collect(&:project).uniq
361 370 assert projects.size > 1
362 371 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
363 372 end
364 373
365 374 def test_visible_and_nested_set_scopes
366 375 user = User.generate!
367 376 parent = Issue.generate!(:assigned_to => user)
368 377 assert parent.visible?(user)
369 378 child1 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
370 379 child2 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
371 380 parent.reload
372 381 child1.reload
373 382 child2.reload
374 383 assert child1.visible?(user)
375 384 assert child2.visible?(user)
376 385 assert_equal 2, parent.descendants.count
377 386 assert_equal 2, parent.descendants.visible(user).count
378 387 # awesome_nested_set 2-1-stable branch has regression.
379 388 # https://github.com/collectiveidea/awesome_nested_set/commit/3d5ac746542b564f6586c2316180254b088bebb6
380 389 # ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous column name: lft:
381 390 assert_equal 2, parent.descendants.collect{|i| i}.size
382 391 assert_equal 2, parent.descendants.visible(user).collect{|i| i}.size
383 392 end
384 393
385 394 def test_visible_scope_with_unsaved_user_should_not_raise_an_error
386 395 user = User.new
387 396 assert_nothing_raised do
388 397 Issue.visible(user).to_a
389 398 end
390 399 end
391 400
392 401 def test_open_scope
393 402 issues = Issue.open.to_a
394 403 assert_nil issues.detect(&:closed?)
395 404 end
396 405
397 406 def test_open_scope_with_arg
398 407 issues = Issue.open(false).to_a
399 408 assert_equal issues, issues.select(&:closed?)
400 409 end
401 410
402 411 def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues
403 412 version = Version.find(2)
404 413 assert version.fixed_issues.any?
405 414 assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort
406 415 end
407 416
408 417 def test_fixed_version_scope_with_empty_array_should_return_no_result
409 418 assert_equal 0, Issue.fixed_version([]).count
410 419 end
411 420
412 421 def test_errors_full_messages_should_include_custom_fields_errors
413 422 field = IssueCustomField.find_by_name('Database')
414 423
415 424 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
416 425 :status_id => 1, :subject => 'test_create',
417 426 :description => 'IssueTest#test_create_with_required_custom_field')
418 427 assert issue.available_custom_fields.include?(field)
419 428 # Invalid value
420 429 issue.custom_field_values = { field.id => 'SQLServer' }
421 430
422 431 assert !issue.valid?
423 432 assert_equal 1, issue.errors.full_messages.size
424 433 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
425 434 issue.errors.full_messages.first
426 435 end
427 436
428 437 def test_update_issue_with_required_custom_field
429 438 field = IssueCustomField.find_by_name('Database')
430 439 field.update_attribute(:is_required, true)
431 440
432 441 issue = Issue.find(1)
433 442 assert_nil issue.custom_value_for(field)
434 443 assert issue.available_custom_fields.include?(field)
435 444 # No change to custom values, issue can be saved
436 445 assert issue.save
437 446 # Blank value
438 447 issue.custom_field_values = { field.id => '' }
439 448 assert !issue.save
440 449 # Valid value
441 450 issue.custom_field_values = { field.id => 'PostgreSQL' }
442 451 assert issue.save
443 452 issue.reload
444 453 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
445 454 end
446 455
447 456 def test_should_not_update_attributes_if_custom_fields_validation_fails
448 457 issue = Issue.find(1)
449 458 field = IssueCustomField.find_by_name('Database')
450 459 assert issue.available_custom_fields.include?(field)
451 460
452 461 issue.custom_field_values = { field.id => 'Invalid' }
453 462 issue.subject = 'Should be not be saved'
454 463 assert !issue.save
455 464
456 465 issue.reload
457 466 assert_equal "Cannot print recipes", issue.subject
458 467 end
459 468
460 469 def test_should_not_recreate_custom_values_objects_on_update
461 470 field = IssueCustomField.find_by_name('Database')
462 471
463 472 issue = Issue.find(1)
464 473 issue.custom_field_values = { field.id => 'PostgreSQL' }
465 474 assert issue.save
466 475 custom_value = issue.custom_value_for(field)
467 476 issue.reload
468 477 issue.custom_field_values = { field.id => 'MySQL' }
469 478 assert issue.save
470 479 issue.reload
471 480 assert_equal custom_value.id, issue.custom_value_for(field).id
472 481 end
473 482
474 483 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
475 484 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
476 485 :status_id => 1, :subject => 'Test',
477 486 :custom_field_values => {'2' => 'Test'})
478 487 assert !Tracker.find(2).custom_field_ids.include?(2)
479 488
480 489 issue = Issue.find(issue.id)
481 490 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
482 491
483 492 issue = Issue.find(issue.id)
484 493 custom_value = issue.custom_value_for(2)
485 494 assert_not_nil custom_value
486 495 assert_equal 'Test', custom_value.value
487 496 end
488 497
489 498 def test_assigning_tracker_id_should_reload_custom_fields_values
490 499 issue = Issue.new(:project => Project.find(1))
491 500 assert issue.custom_field_values.empty?
492 501 issue.tracker_id = 1
493 502 assert issue.custom_field_values.any?
494 503 end
495 504
496 505 def test_assigning_attributes_should_assign_project_and_tracker_first
497 506 seq = sequence('seq')
498 507 issue = Issue.new
499 508 issue.expects(:project_id=).in_sequence(seq)
500 509 issue.expects(:tracker_id=).in_sequence(seq)
501 510 issue.expects(:subject=).in_sequence(seq)
502 511 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
503 512 end
504 513
505 514 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
506 515 attributes = ActiveSupport::OrderedHash.new
507 516 attributes['custom_field_values'] = { '1' => 'MySQL' }
508 517 attributes['tracker_id'] = '1'
509 518 issue = Issue.new(:project => Project.find(1))
510 519 issue.attributes = attributes
511 520 assert_equal 'MySQL', issue.custom_field_value(1)
512 521 end
513 522
514 523 def test_changing_tracker_should_clear_disabled_core_fields
515 524 tracker = Tracker.find(2)
516 525 tracker.core_fields = tracker.core_fields - %w(due_date)
517 526 tracker.save!
518 527
519 528 issue = Issue.generate!(:tracker_id => 1, :start_date => Date.today, :due_date => Date.today)
520 529 issue.save!
521 530
522 531 issue.tracker_id = 2
523 532 issue.save!
524 533 assert_not_nil issue.start_date
525 534 assert_nil issue.due_date
526 535 end
527 536
528 537 def test_changing_tracker_should_not_add_cleared_fields_to_journal
529 538 tracker = Tracker.find(2)
530 539 tracker.core_fields = tracker.core_fields - %w(due_date)
531 540 tracker.save!
532 541
533 542 issue = Issue.generate!(:tracker_id => 1, :due_date => Date.today)
534 543 issue.save!
535 544
536 545 assert_difference 'Journal.count' do
537 546 issue.init_journal User.find(1)
538 547 issue.tracker_id = 2
539 548 issue.save!
540 549 assert_nil issue.due_date
541 550 end
542 551 journal = Journal.order('id DESC').first
543 552 assert_equal 1, journal.details.count
544 553 end
545 554
546 555 def test_reload_should_reload_custom_field_values
547 556 issue = Issue.generate!
548 557 issue.custom_field_values = {'2' => 'Foo'}
549 558 issue.save!
550 559
551 560 issue = Issue.order('id desc').first
552 561 assert_equal 'Foo', issue.custom_field_value(2)
553 562
554 563 issue.custom_field_values = {'2' => 'Bar'}
555 564 assert_equal 'Bar', issue.custom_field_value(2)
556 565
557 566 issue.reload
558 567 assert_equal 'Foo', issue.custom_field_value(2)
559 568 end
560 569
561 570 def test_should_update_issue_with_disabled_tracker
562 571 p = Project.find(1)
563 572 issue = Issue.find(1)
564 573
565 574 p.trackers.delete(issue.tracker)
566 575 assert !p.trackers.include?(issue.tracker)
567 576
568 577 issue.reload
569 578 issue.subject = 'New subject'
570 579 assert issue.save
571 580 end
572 581
573 582 def test_should_not_set_a_disabled_tracker
574 583 p = Project.find(1)
575 584 p.trackers.delete(Tracker.find(2))
576 585
577 586 issue = Issue.find(1)
578 587 issue.tracker_id = 2
579 588 issue.subject = 'New subject'
580 589 assert !issue.save
581 590 assert_not_equal [], issue.errors[:tracker_id]
582 591 end
583 592
584 593 def test_category_based_assignment
585 594 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
586 595 :status_id => 1, :priority => IssuePriority.all.first,
587 596 :subject => 'Assignment test',
588 597 :description => 'Assignment test', :category_id => 1)
589 598 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
590 599 end
591 600
592 601 def test_new_statuses_allowed_to
593 602 WorkflowTransition.delete_all
594 603 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
595 604 :old_status_id => 1, :new_status_id => 2,
596 605 :author => false, :assignee => false)
597 606 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
598 607 :old_status_id => 1, :new_status_id => 3,
599 608 :author => true, :assignee => false)
600 609 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
601 610 :old_status_id => 1, :new_status_id => 4,
602 611 :author => false, :assignee => true)
603 612 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
604 613 :old_status_id => 1, :new_status_id => 5,
605 614 :author => true, :assignee => true)
606 615 status = IssueStatus.find(1)
607 616 role = Role.find(1)
608 617 tracker = Tracker.find(1)
609 618 user = User.find(2)
610 619
611 620 issue = Issue.generate!(:tracker => tracker, :status => status,
612 621 :project_id => 1, :author_id => 1)
613 622 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
614 623
615 624 issue = Issue.generate!(:tracker => tracker, :status => status,
616 625 :project_id => 1, :author => user)
617 626 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
618 627
619 628 issue = Issue.generate!(:tracker => tracker, :status => status,
620 629 :project_id => 1, :author_id => 1,
621 630 :assigned_to => user)
622 631 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
623 632
624 633 issue = Issue.generate!(:tracker => tracker, :status => status,
625 634 :project_id => 1, :author => user,
626 635 :assigned_to => user)
627 636 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
628 637
629 638 group = Group.generate!
630 639 group.users << user
631 640 issue = Issue.generate!(:tracker => tracker, :status => status,
632 641 :project_id => 1, :author => user,
633 642 :assigned_to => group)
634 643 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
635 644 end
636 645
637 646 def test_new_statuses_allowed_to_should_consider_group_assignment
638 647 WorkflowTransition.delete_all
639 648 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
640 649 :old_status_id => 1, :new_status_id => 4,
641 650 :author => false, :assignee => true)
642 651 user = User.find(2)
643 652 group = Group.generate!
644 653 group.users << user
645 654
646 655 issue = Issue.generate!(:author_id => 1, :assigned_to => group)
647 656 assert_include 4, issue.new_statuses_allowed_to(user).map(&:id)
648 657 end
649 658
650 659 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
651 660 admin = User.find(1)
652 661 issue = Issue.find(1)
653 662 assert !admin.member_of?(issue.project)
654 663 expected_statuses = [issue.status] +
655 664 WorkflowTransition.where(:old_status_id => issue.status_id).
656 665 map(&:new_status).uniq.sort
657 666 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
658 667 end
659 668
660 669 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
661 670 issue = Issue.find(1).copy
662 671 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
663 672
664 673 issue = Issue.find(2).copy
665 674 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
666 675 end
667 676
668 677 def test_safe_attributes_names_should_not_include_disabled_field
669 678 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
670 679
671 680 issue = Issue.new(:tracker => tracker)
672 681 assert_include 'tracker_id', issue.safe_attribute_names
673 682 assert_include 'status_id', issue.safe_attribute_names
674 683 assert_include 'subject', issue.safe_attribute_names
675 684 assert_include 'description', issue.safe_attribute_names
676 685 assert_include 'custom_field_values', issue.safe_attribute_names
677 686 assert_include 'custom_fields', issue.safe_attribute_names
678 687 assert_include 'lock_version', issue.safe_attribute_names
679 688
680 689 tracker.core_fields.each do |field|
681 690 assert_include field, issue.safe_attribute_names
682 691 end
683 692
684 693 tracker.disabled_core_fields.each do |field|
685 694 assert_not_include field, issue.safe_attribute_names
686 695 end
687 696 end
688 697
689 698 def test_safe_attributes_should_ignore_disabled_fields
690 699 tracker = Tracker.find(1)
691 700 tracker.core_fields = %w(assigned_to_id due_date)
692 701 tracker.save!
693 702
694 703 issue = Issue.new(:tracker => tracker)
695 704 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
696 705 assert_nil issue.start_date
697 706 assert_equal Date.parse('2012-07-14'), issue.due_date
698 707 end
699 708
700 709 def test_safe_attributes_should_accept_target_tracker_enabled_fields
701 710 source = Tracker.find(1)
702 711 source.core_fields = []
703 712 source.save!
704 713 target = Tracker.find(2)
705 714 target.core_fields = %w(assigned_to_id due_date)
706 715 target.save!
707 716
708 717 issue = Issue.new(:tracker => source)
709 718 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
710 719 assert_equal target, issue.tracker
711 720 assert_equal Date.parse('2012-07-14'), issue.due_date
712 721 end
713 722
714 723 def test_safe_attributes_should_not_include_readonly_fields
715 724 WorkflowPermission.delete_all
716 725 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
717 726 :role_id => 1, :field_name => 'due_date',
718 727 :rule => 'readonly')
719 728 user = User.find(2)
720 729
721 730 issue = Issue.new(:project_id => 1, :tracker_id => 1)
722 731 assert_equal %w(due_date), issue.read_only_attribute_names(user)
723 732 assert_not_include 'due_date', issue.safe_attribute_names(user)
724 733
725 734 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
726 735 assert_equal Date.parse('2012-07-14'), issue.start_date
727 736 assert_nil issue.due_date
728 737 end
729 738
730 739 def test_safe_attributes_should_not_include_readonly_custom_fields
731 740 cf1 = IssueCustomField.create!(:name => 'Writable field',
732 741 :field_format => 'string',
733 742 :is_for_all => true, :tracker_ids => [1])
734 743 cf2 = IssueCustomField.create!(:name => 'Readonly field',
735 744 :field_format => 'string',
736 745 :is_for_all => true, :tracker_ids => [1])
737 746 WorkflowPermission.delete_all
738 747 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
739 748 :role_id => 1, :field_name => cf2.id.to_s,
740 749 :rule => 'readonly')
741 750 user = User.find(2)
742 751 issue = Issue.new(:project_id => 1, :tracker_id => 1)
743 752 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
744 753 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
745 754
746 755 issue.send :safe_attributes=, {'custom_field_values' => {
747 756 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
748 757 }}, user
749 758 assert_equal 'value1', issue.custom_field_value(cf1)
750 759 assert_nil issue.custom_field_value(cf2)
751 760
752 761 issue.send :safe_attributes=, {'custom_fields' => [
753 762 {'id' => cf1.id.to_s, 'value' => 'valuea'},
754 763 {'id' => cf2.id.to_s, 'value' => 'valueb'}
755 764 ]}, user
756 765 assert_equal 'valuea', issue.custom_field_value(cf1)
757 766 assert_nil issue.custom_field_value(cf2)
758 767 end
759 768
760 769 def test_editable_custom_field_values_should_return_non_readonly_custom_values
761 770 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
762 771 :is_for_all => true, :tracker_ids => [1, 2])
763 772 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
764 773 :is_for_all => true, :tracker_ids => [1, 2])
765 774 WorkflowPermission.delete_all
766 775 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
767 776 :field_name => cf2.id.to_s, :rule => 'readonly')
768 777 user = User.find(2)
769 778
770 779 issue = Issue.new(:project_id => 1, :tracker_id => 1)
771 780 values = issue.editable_custom_field_values(user)
772 781 assert values.detect {|value| value.custom_field == cf1}
773 782 assert_nil values.detect {|value| value.custom_field == cf2}
774 783
775 784 issue.tracker_id = 2
776 785 values = issue.editable_custom_field_values(user)
777 786 assert values.detect {|value| value.custom_field == cf1}
778 787 assert values.detect {|value| value.custom_field == cf2}
779 788 end
780 789
781 790 def test_editable_custom_fields_should_return_custom_field_that_is_enabled_for_the_role_only
782 791 enabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [1,2])
783 792 disabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [2])
784 793 user = User.find(2)
785 794 issue = Issue.new(:project_id => 1, :tracker_id => 1)
786 795
787 796 assert_include enabled_cf, issue.editable_custom_fields(user)
788 797 assert_not_include disabled_cf, issue.editable_custom_fields(user)
789 798 end
790 799
791 800 def test_safe_attributes_should_accept_target_tracker_writable_fields
792 801 WorkflowPermission.delete_all
793 802 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
794 803 :role_id => 1, :field_name => 'due_date',
795 804 :rule => 'readonly')
796 805 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
797 806 :role_id => 1, :field_name => 'start_date',
798 807 :rule => 'readonly')
799 808 user = User.find(2)
800 809
801 810 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
802 811
803 812 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
804 813 'due_date' => '2012-07-14'}, user
805 814 assert_equal Date.parse('2012-07-12'), issue.start_date
806 815 assert_nil issue.due_date
807 816
808 817 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
809 818 'due_date' => '2012-07-16',
810 819 'tracker_id' => 2}, user
811 820 assert_equal Date.parse('2012-07-12'), issue.start_date
812 821 assert_equal Date.parse('2012-07-16'), issue.due_date
813 822 end
814 823
815 824 def test_safe_attributes_should_accept_target_status_writable_fields
816 825 WorkflowPermission.delete_all
817 826 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
818 827 :role_id => 1, :field_name => 'due_date',
819 828 :rule => 'readonly')
820 829 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
821 830 :role_id => 1, :field_name => 'start_date',
822 831 :rule => 'readonly')
823 832 user = User.find(2)
824 833
825 834 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
826 835
827 836 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
828 837 'due_date' => '2012-07-14'},
829 838 user
830 839 assert_equal Date.parse('2012-07-12'), issue.start_date
831 840 assert_nil issue.due_date
832 841
833 842 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
834 843 'due_date' => '2012-07-16',
835 844 'status_id' => 2},
836 845 user
837 846 assert_equal Date.parse('2012-07-12'), issue.start_date
838 847 assert_equal Date.parse('2012-07-16'), issue.due_date
839 848 end
840 849
841 850 def test_required_attributes_should_be_validated
842 851 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
843 852 :is_for_all => true, :tracker_ids => [1, 2])
844 853
845 854 WorkflowPermission.delete_all
846 855 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
847 856 :role_id => 1, :field_name => 'due_date',
848 857 :rule => 'required')
849 858 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
850 859 :role_id => 1, :field_name => 'category_id',
851 860 :rule => 'required')
852 861 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
853 862 :role_id => 1, :field_name => cf.id.to_s,
854 863 :rule => 'required')
855 864
856 865 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
857 866 :role_id => 1, :field_name => 'start_date',
858 867 :rule => 'required')
859 868 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
860 869 :role_id => 1, :field_name => cf.id.to_s,
861 870 :rule => 'required')
862 871 user = User.find(2)
863 872
864 873 issue = Issue.new(:project_id => 1, :tracker_id => 1,
865 874 :status_id => 1, :subject => 'Required fields',
866 875 :author => user)
867 876 assert_equal [cf.id.to_s, "category_id", "due_date"],
868 877 issue.required_attribute_names(user).sort
869 878 assert !issue.save, "Issue was saved"
870 879 assert_equal ["Category cannot be blank", "Due date cannot be blank", "Foo cannot be blank"],
871 880 issue.errors.full_messages.sort
872 881
873 882 issue.tracker_id = 2
874 883 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
875 884 assert !issue.save, "Issue was saved"
876 885 assert_equal ["Foo cannot be blank", "Start date cannot be blank"],
877 886 issue.errors.full_messages.sort
878 887
879 888 issue.start_date = Date.today
880 889 issue.custom_field_values = {cf.id.to_s => 'bar'}
881 890 assert issue.save
882 891 end
883 892
884 893 def test_required_attribute_that_is_disabled_for_the_tracker_should_not_be_required
885 894 WorkflowPermission.delete_all
886 895 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
887 896 :role_id => 1, :field_name => 'start_date',
888 897 :rule => 'required')
889 898 user = User.find(2)
890 899
891 900 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
892 901 :subject => 'Required fields', :author => user)
893 902 assert !issue.save
894 903 assert_include "Start date cannot be blank", issue.errors.full_messages
895 904
896 905 tracker = Tracker.find(1)
897 906 tracker.core_fields -= %w(start_date)
898 907 tracker.save!
899 908 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
900 909 :subject => 'Required fields', :author => user)
901 910 assert issue.save
902 911 end
903 912
904 913 def test_required_custom_field_that_is_not_visible_for_the_user_should_not_be_required
905 914 CustomField.delete_all
906 915 field = IssueCustomField.generate!(:is_required => true, :visible => false, :role_ids => [1], :trackers => Tracker.all, :is_for_all => true)
907 916 user = User.generate!
908 917 User.add_to_project(user, Project.find(1), Role.find(2))
909 918
910 919 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
911 920 :subject => 'Required fields', :author => user)
912 921 assert_save issue
913 922 end
914 923
915 924 def test_required_custom_field_that_is_visible_for_the_user_should_be_required
916 925 CustomField.delete_all
917 926 field = IssueCustomField.generate!(:is_required => true, :visible => false, :role_ids => [1], :trackers => Tracker.all, :is_for_all => true)
918 927 user = User.generate!
919 928 User.add_to_project(user, Project.find(1), Role.find(1))
920 929
921 930 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
922 931 :subject => 'Required fields', :author => user)
923 932 assert !issue.save
924 933 assert_include "#{field.name} cannot be blank", issue.errors.full_messages
925 934 end
926 935
927 936 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
928 937 WorkflowPermission.delete_all
929 938 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
930 939 :role_id => 1, :field_name => 'due_date',
931 940 :rule => 'required')
932 941 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
933 942 :role_id => 1, :field_name => 'start_date',
934 943 :rule => 'required')
935 944 user = User.find(2)
936 945 member = Member.find(1)
937 946 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
938 947
939 948 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
940 949
941 950 member.role_ids = [1, 2]
942 951 member.save!
943 952 assert_equal [], issue.required_attribute_names(user.reload)
944 953
945 954 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
946 955 :role_id => 2, :field_name => 'due_date',
947 956 :rule => 'required')
948 957 assert_equal %w(due_date), issue.required_attribute_names(user)
949 958
950 959 member.role_ids = [1, 2, 3]
951 960 member.save!
952 961 assert_equal [], issue.required_attribute_names(user.reload)
953 962
954 963 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
955 964 :role_id => 3, :field_name => 'due_date',
956 965 :rule => 'readonly')
957 966 # required + readonly => required
958 967 assert_equal %w(due_date), issue.required_attribute_names(user)
959 968 end
960 969
961 970 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
962 971 WorkflowPermission.delete_all
963 972 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
964 973 :role_id => 1, :field_name => 'due_date',
965 974 :rule => 'readonly')
966 975 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
967 976 :role_id => 1, :field_name => 'start_date',
968 977 :rule => 'readonly')
969 978 user = User.find(2)
970 979 member = Member.find(1)
971 980 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
972 981
973 982 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
974 983
975 984 member.role_ids = [1, 2]
976 985 member.save!
977 986 assert_equal [], issue.read_only_attribute_names(user.reload)
978 987
979 988 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
980 989 :role_id => 2, :field_name => 'due_date',
981 990 :rule => 'readonly')
982 991 assert_equal %w(due_date), issue.read_only_attribute_names(user)
983 992 end
984 993
985 994 # A field that is not visible by role 2 and readonly by role 1 should be readonly for user with role 1 and 2
986 995 def test_read_only_attribute_names_should_include_custom_fields_that_combine_readonly_and_not_visible_for_roles
987 996 field = IssueCustomField.generate!(
988 997 :is_for_all => true, :trackers => Tracker.all, :visible => false, :role_ids => [1]
989 998 )
990 999 WorkflowPermission.delete_all
991 1000 WorkflowPermission.create!(
992 1001 :old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => field.id, :rule => 'readonly'
993 1002 )
994 1003 user = User.generate!
995 1004 project = Project.find(1)
996 1005 User.add_to_project(user, project, Role.where(:id => [1, 2]))
997 1006
998 1007 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
999 1008 assert_equal [field.id.to_s], issue.read_only_attribute_names(user)
1000 1009 end
1001 1010
1002 1011 def test_workflow_rules_should_ignore_roles_without_issue_permissions
1003 1012 role = Role.generate! :permissions => [:view_issues, :edit_issues]
1004 1013 ignored_role = Role.generate! :permissions => [:view_issues]
1005 1014
1006 1015 WorkflowPermission.delete_all
1007 1016 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1008 1017 :role => role, :field_name => 'due_date',
1009 1018 :rule => 'required')
1010 1019 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1011 1020 :role => role, :field_name => 'start_date',
1012 1021 :rule => 'readonly')
1013 1022 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1014 1023 :role => role, :field_name => 'done_ratio',
1015 1024 :rule => 'readonly')
1016 1025 user = User.generate!
1017 1026 User.add_to_project user, Project.find(1), [role, ignored_role]
1018 1027
1019 1028 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1020 1029
1021 1030 assert_equal %w(due_date), issue.required_attribute_names(user)
1022 1031 assert_equal %w(done_ratio start_date), issue.read_only_attribute_names(user).sort
1023 1032 end
1024 1033
1025 1034 def test_workflow_rules_should_work_for_member_with_duplicate_role
1026 1035 WorkflowPermission.delete_all
1027 1036 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1028 1037 :role_id => 1, :field_name => 'due_date',
1029 1038 :rule => 'required')
1030 1039 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1031 1040 :role_id => 1, :field_name => 'start_date',
1032 1041 :rule => 'readonly')
1033 1042
1034 1043 user = User.generate!
1035 1044 m = Member.new(:user_id => user.id, :project_id => 1)
1036 1045 m.member_roles.build(:role_id => 1)
1037 1046 m.member_roles.build(:role_id => 1)
1038 1047 m.save!
1039 1048
1040 1049 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1041 1050
1042 1051 assert_equal %w(due_date), issue.required_attribute_names(user)
1043 1052 assert_equal %w(start_date), issue.read_only_attribute_names(user)
1044 1053 end
1045 1054
1046 1055 def test_copy
1047 1056 issue = Issue.new.copy_from(1)
1048 1057 assert issue.copy?
1049 1058 assert issue.save
1050 1059 issue.reload
1051 1060 orig = Issue.find(1)
1052 1061 assert_equal orig.subject, issue.subject
1053 1062 assert_equal orig.tracker, issue.tracker
1054 1063 assert_equal "125", issue.custom_value_for(2).value
1055 1064 end
1056 1065
1057 1066 def test_copy_should_copy_status
1058 1067 orig = Issue.find(8)
1059 1068 assert orig.status != orig.default_status
1060 1069
1061 1070 issue = Issue.new.copy_from(orig)
1062 1071 assert issue.save
1063 1072 issue.reload
1064 1073 assert_equal orig.status, issue.status
1065 1074 end
1066 1075
1067 1076 def test_copy_should_add_relation_with_copied_issue
1068 1077 copied = Issue.find(1)
1069 1078 issue = Issue.new.copy_from(copied)
1070 1079 assert issue.save
1071 1080 issue.reload
1072 1081
1073 1082 assert_equal 1, issue.relations.size
1074 1083 relation = issue.relations.first
1075 1084 assert_equal 'copied_to', relation.relation_type
1076 1085 assert_equal copied, relation.issue_from
1077 1086 assert_equal issue, relation.issue_to
1078 1087 end
1079 1088
1080 1089 def test_copy_should_copy_subtasks
1081 1090 issue = Issue.generate_with_descendants!
1082 1091
1083 1092 copy = issue.reload.copy
1084 1093 copy.author = User.find(7)
1085 1094 assert_difference 'Issue.count', 1+issue.descendants.count do
1086 1095 assert copy.save
1087 1096 end
1088 1097 copy.reload
1089 1098 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
1090 1099 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
1091 1100 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
1092 1101 assert_equal copy.author, child_copy.author
1093 1102 end
1094 1103
1095 1104 def test_copy_as_a_child_of_copied_issue_should_not_copy_itself
1096 1105 parent = Issue.generate!
1097 1106 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
1098 1107 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
1099 1108
1100 1109 copy = parent.reload.copy
1101 1110 copy.parent_issue_id = parent.id
1102 1111 copy.author = User.find(7)
1103 1112 assert_difference 'Issue.count', 3 do
1104 1113 assert copy.save
1105 1114 end
1106 1115 parent.reload
1107 1116 copy.reload
1108 1117 assert_equal parent, copy.parent
1109 1118 assert_equal 3, parent.children.count
1110 1119 assert_equal 5, parent.descendants.count
1111 1120 assert_equal 2, copy.children.count
1112 1121 assert_equal 2, copy.descendants.count
1113 1122 end
1114 1123
1115 1124 def test_copy_as_a_descendant_of_copied_issue_should_not_copy_itself
1116 1125 parent = Issue.generate!
1117 1126 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
1118 1127 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
1119 1128
1120 1129 copy = parent.reload.copy
1121 1130 copy.parent_issue_id = child1.id
1122 1131 copy.author = User.find(7)
1123 1132 assert_difference 'Issue.count', 3 do
1124 1133 assert copy.save
1125 1134 end
1126 1135 parent.reload
1127 1136 child1.reload
1128 1137 copy.reload
1129 1138 assert_equal child1, copy.parent
1130 1139 assert_equal 2, parent.children.count
1131 1140 assert_equal 5, parent.descendants.count
1132 1141 assert_equal 1, child1.children.count
1133 1142 assert_equal 3, child1.descendants.count
1134 1143 assert_equal 2, copy.children.count
1135 1144 assert_equal 2, copy.descendants.count
1136 1145 end
1137 1146
1138 1147 def test_copy_should_copy_subtasks_to_target_project
1139 1148 issue = Issue.generate_with_descendants!
1140 1149
1141 1150 copy = issue.copy(:project_id => 3)
1142 1151 assert_difference 'Issue.count', 1+issue.descendants.count do
1143 1152 assert copy.save
1144 1153 end
1145 1154 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
1146 1155 end
1147 1156
1148 1157 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
1149 1158 issue = Issue.generate_with_descendants!
1150 1159
1151 1160 copy = issue.reload.copy
1152 1161 assert_difference 'Issue.count', 1+issue.descendants.count do
1153 1162 assert copy.save
1154 1163 assert copy.save
1155 1164 end
1156 1165 end
1157 1166
1158 1167 def test_should_not_call_after_project_change_on_creation
1159 1168 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1160 1169 :subject => 'Test', :author_id => 1)
1161 1170 issue.expects(:after_project_change).never
1162 1171 issue.save!
1163 1172 end
1164 1173
1165 1174 def test_should_not_call_after_project_change_on_update
1166 1175 issue = Issue.find(1)
1167 1176 issue.project = Project.find(1)
1168 1177 issue.subject = 'No project change'
1169 1178 issue.expects(:after_project_change).never
1170 1179 issue.save!
1171 1180 end
1172 1181
1173 1182 def test_should_call_after_project_change_on_project_change
1174 1183 issue = Issue.find(1)
1175 1184 issue.project = Project.find(2)
1176 1185 issue.expects(:after_project_change).once
1177 1186 issue.save!
1178 1187 end
1179 1188
1180 1189 def test_adding_journal_should_update_timestamp
1181 1190 issue = Issue.find(1)
1182 1191 updated_on_was = issue.updated_on
1183 1192
1184 1193 issue.init_journal(User.first, "Adding notes")
1185 1194 assert_difference 'Journal.count' do
1186 1195 assert issue.save
1187 1196 end
1188 1197 issue.reload
1189 1198
1190 1199 assert_not_equal updated_on_was, issue.updated_on
1191 1200 end
1192 1201
1193 1202 def test_should_close_duplicates
1194 1203 # Create 3 issues
1195 1204 issue1 = Issue.generate!
1196 1205 issue2 = Issue.generate!
1197 1206 issue3 = Issue.generate!
1198 1207
1199 1208 # 2 is a dupe of 1
1200 1209 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
1201 1210 :relation_type => IssueRelation::TYPE_DUPLICATES)
1202 1211 # And 3 is a dupe of 2
1203 1212 # IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1204 1213 # :relation_type => IssueRelation::TYPE_DUPLICATES)
1205 1214 # And 3 is a dupe of 1 (circular duplicates)
1206 1215 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
1207 1216 :relation_type => IssueRelation::TYPE_DUPLICATES)
1208 1217
1209 1218 assert issue1.reload.duplicates.include?(issue2)
1210 1219
1211 1220 # Closing issue 1
1212 1221 issue1.init_journal(User.first, "Closing issue1")
1213 1222 issue1.status = IssueStatus.where(:is_closed => true).first
1214 1223 assert issue1.save
1215 1224 # 2 and 3 should be also closed
1216 1225 assert issue2.reload.closed?
1217 1226 assert issue3.reload.closed?
1218 1227 end
1219 1228
1220 1229 def test_should_not_close_duplicated_issue
1221 1230 issue1 = Issue.generate!
1222 1231 issue2 = Issue.generate!
1223 1232
1224 1233 # 2 is a dupe of 1
1225 1234 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
1226 1235 :relation_type => IssueRelation::TYPE_DUPLICATES)
1227 1236 # 2 is a dup of 1 but 1 is not a duplicate of 2
1228 1237 assert !issue2.reload.duplicates.include?(issue1)
1229 1238
1230 1239 # Closing issue 2
1231 1240 issue2.init_journal(User.first, "Closing issue2")
1232 1241 issue2.status = IssueStatus.where(:is_closed => true).first
1233 1242 assert issue2.save
1234 1243 # 1 should not be also closed
1235 1244 assert !issue1.reload.closed?
1236 1245 end
1237 1246
1238 1247 def test_assignable_versions
1239 1248 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1240 1249 :status_id => 1, :fixed_version_id => 1,
1241 1250 :subject => 'New issue')
1242 1251 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
1243 1252 end
1244 1253
1245 1254 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
1246 1255 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1247 1256 :status_id => 1, :fixed_version_id => 1,
1248 1257 :subject => 'New issue')
1249 1258 assert !issue.save
1250 1259 assert_not_equal [], issue.errors[:fixed_version_id]
1251 1260 end
1252 1261
1253 1262 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
1254 1263 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1255 1264 :status_id => 1, :fixed_version_id => 2,
1256 1265 :subject => 'New issue')
1257 1266 assert !issue.save
1258 1267 assert_not_equal [], issue.errors[:fixed_version_id]
1259 1268 end
1260 1269
1261 1270 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
1262 1271 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1263 1272 :status_id => 1, :fixed_version_id => 3,
1264 1273 :subject => 'New issue')
1265 1274 assert issue.save
1266 1275 end
1267 1276
1268 1277 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
1269 1278 issue = Issue.find(11)
1270 1279 assert_equal 'closed', issue.fixed_version.status
1271 1280 issue.subject = 'Subject changed'
1272 1281 assert issue.save
1273 1282 end
1274 1283
1275 1284 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
1276 1285 issue = Issue.find(11)
1277 1286 issue.status_id = 1
1278 1287 assert !issue.save
1279 1288 assert_not_equal [], issue.errors[:base]
1280 1289 end
1281 1290
1282 1291 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
1283 1292 issue = Issue.find(11)
1284 1293 issue.status_id = 1
1285 1294 issue.fixed_version_id = 3
1286 1295 assert issue.save
1287 1296 end
1288 1297
1289 1298 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
1290 1299 issue = Issue.find(12)
1291 1300 assert_equal 'locked', issue.fixed_version.status
1292 1301 issue.status_id = 1
1293 1302 assert issue.save
1294 1303 end
1295 1304
1296 1305 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
1297 1306 issue = Issue.find(2)
1298 1307 assert_equal 2, issue.fixed_version_id
1299 1308 issue.project_id = 3
1300 1309 assert_nil issue.fixed_version_id
1301 1310 issue.fixed_version_id = 2
1302 1311 assert !issue.save
1303 1312 assert_include 'Target version is not included in the list', issue.errors.full_messages
1304 1313 end
1305 1314
1306 1315 def test_should_keep_shared_version_when_changing_project
1307 1316 Version.find(2).update_attribute :sharing, 'tree'
1308 1317
1309 1318 issue = Issue.find(2)
1310 1319 assert_equal 2, issue.fixed_version_id
1311 1320 issue.project_id = 3
1312 1321 assert_equal 2, issue.fixed_version_id
1313 1322 assert issue.save
1314 1323 end
1315 1324
1316 1325 def test_allowed_target_projects_should_include_projects_with_issue_tracking_enabled
1317 1326 assert_include Project.find(2), Issue.allowed_target_projects(User.find(2))
1318 1327 end
1319 1328
1320 1329 def test_allowed_target_projects_should_not_include_projects_with_issue_tracking_disabled
1321 1330 Project.find(2).disable_module! :issue_tracking
1322 1331 assert_not_include Project.find(2), Issue.allowed_target_projects(User.find(2))
1323 1332 end
1324 1333
1325 1334 def test_move_to_another_project_with_same_category
1326 1335 issue = Issue.find(1)
1327 1336 issue.project = Project.find(2)
1328 1337 assert issue.save
1329 1338 issue.reload
1330 1339 assert_equal 2, issue.project_id
1331 1340 # Category changes
1332 1341 assert_equal 4, issue.category_id
1333 1342 # Make sure time entries were move to the target project
1334 1343 assert_equal 2, issue.time_entries.first.project_id
1335 1344 end
1336 1345
1337 1346 def test_move_to_another_project_without_same_category
1338 1347 issue = Issue.find(2)
1339 1348 issue.project = Project.find(2)
1340 1349 assert issue.save
1341 1350 issue.reload
1342 1351 assert_equal 2, issue.project_id
1343 1352 # Category cleared
1344 1353 assert_nil issue.category_id
1345 1354 end
1346 1355
1347 1356 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1348 1357 issue = Issue.find(1)
1349 1358 issue.update_attribute(:fixed_version_id, 1)
1350 1359 issue.project = Project.find(2)
1351 1360 assert issue.save
1352 1361 issue.reload
1353 1362 assert_equal 2, issue.project_id
1354 1363 # Cleared fixed_version
1355 1364 assert_equal nil, issue.fixed_version
1356 1365 end
1357 1366
1358 1367 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1359 1368 issue = Issue.find(1)
1360 1369 issue.update_attribute(:fixed_version_id, 4)
1361 1370 issue.project = Project.find(5)
1362 1371 assert issue.save
1363 1372 issue.reload
1364 1373 assert_equal 5, issue.project_id
1365 1374 # Keep fixed_version
1366 1375 assert_equal 4, issue.fixed_version_id
1367 1376 end
1368 1377
1369 1378 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1370 1379 issue = Issue.find(1)
1371 1380 issue.update_attribute(:fixed_version_id, 1)
1372 1381 issue.project = Project.find(5)
1373 1382 assert issue.save
1374 1383 issue.reload
1375 1384 assert_equal 5, issue.project_id
1376 1385 # Cleared fixed_version
1377 1386 assert_equal nil, issue.fixed_version
1378 1387 end
1379 1388
1380 1389 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1381 1390 issue = Issue.find(1)
1382 1391 issue.update_attribute(:fixed_version_id, 7)
1383 1392 issue.project = Project.find(2)
1384 1393 assert issue.save
1385 1394 issue.reload
1386 1395 assert_equal 2, issue.project_id
1387 1396 # Keep fixed_version
1388 1397 assert_equal 7, issue.fixed_version_id
1389 1398 end
1390 1399
1391 1400 def test_move_to_another_project_should_keep_parent_if_valid
1392 1401 issue = Issue.find(1)
1393 1402 issue.update_attribute(:parent_issue_id, 2)
1394 1403 issue.project = Project.find(3)
1395 1404 assert issue.save
1396 1405 issue.reload
1397 1406 assert_equal 2, issue.parent_id
1398 1407 end
1399 1408
1400 1409 def test_move_to_another_project_should_clear_parent_if_not_valid
1401 1410 issue = Issue.find(1)
1402 1411 issue.update_attribute(:parent_issue_id, 2)
1403 1412 issue.project = Project.find(2)
1404 1413 assert issue.save
1405 1414 issue.reload
1406 1415 assert_nil issue.parent_id
1407 1416 end
1408 1417
1409 1418 def test_move_to_another_project_with_disabled_tracker
1410 1419 issue = Issue.find(1)
1411 1420 target = Project.find(2)
1412 1421 target.tracker_ids = [3]
1413 1422 target.save
1414 1423 issue.project = target
1415 1424 assert issue.save
1416 1425 issue.reload
1417 1426 assert_equal 2, issue.project_id
1418 1427 assert_equal 3, issue.tracker_id
1419 1428 end
1420 1429
1421 1430 def test_copy_to_the_same_project
1422 1431 issue = Issue.find(1)
1423 1432 copy = issue.copy
1424 1433 assert_difference 'Issue.count' do
1425 1434 copy.save!
1426 1435 end
1427 1436 assert_kind_of Issue, copy
1428 1437 assert_equal issue.project, copy.project
1429 1438 assert_equal "125", copy.custom_value_for(2).value
1430 1439 end
1431 1440
1432 1441 def test_copy_to_another_project_and_tracker
1433 1442 issue = Issue.find(1)
1434 1443 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1435 1444 assert_difference 'Issue.count' do
1436 1445 copy.save!
1437 1446 end
1438 1447 copy.reload
1439 1448 assert_kind_of Issue, copy
1440 1449 assert_equal Project.find(3), copy.project
1441 1450 assert_equal Tracker.find(2), copy.tracker
1442 1451 # Custom field #2 is not associated with target tracker
1443 1452 assert_nil copy.custom_value_for(2)
1444 1453 end
1445 1454
1446 1455 test "#copy should not create a journal" do
1447 1456 copy = Issue.find(1).copy({:project_id => 3, :tracker_id => 2, :assigned_to_id => 3}, :link => false)
1448 1457 copy.save!
1449 1458 assert_equal 0, copy.reload.journals.size
1450 1459 end
1451 1460
1452 1461 test "#copy should allow assigned_to changes" do
1453 1462 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1454 1463 assert_equal 3, copy.assigned_to_id
1455 1464 end
1456 1465
1457 1466 test "#copy should allow status changes" do
1458 1467 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1459 1468 assert_equal 2, copy.status_id
1460 1469 end
1461 1470
1462 1471 test "#copy should allow start date changes" do
1463 1472 date = Date.today
1464 1473 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1465 1474 assert_equal date, copy.start_date
1466 1475 end
1467 1476
1468 1477 test "#copy should allow due date changes" do
1469 1478 date = Date.today
1470 1479 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1471 1480 assert_equal date, copy.due_date
1472 1481 end
1473 1482
1474 1483 test "#copy should set current user as author" do
1475 1484 User.current = User.find(9)
1476 1485 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2)
1477 1486 assert_equal User.current, copy.author
1478 1487 end
1479 1488
1480 1489 test "#copy should create a journal with notes" do
1481 1490 date = Date.today
1482 1491 notes = "Notes added when copying"
1483 1492 copy = Issue.find(1).copy({:project_id => 3, :tracker_id => 2, :start_date => date}, :link => false)
1484 1493 copy.init_journal(User.current, notes)
1485 1494 copy.save!
1486 1495
1487 1496 assert_equal 1, copy.journals.size
1488 1497 journal = copy.journals.first
1489 1498 assert_equal 0, journal.details.size
1490 1499 assert_equal notes, journal.notes
1491 1500 end
1492 1501
1493 1502 def test_valid_parent_project
1494 1503 issue = Issue.find(1)
1495 1504 issue_in_same_project = Issue.find(2)
1496 1505 issue_in_child_project = Issue.find(5)
1497 1506 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1498 1507 issue_in_other_child_project = Issue.find(6)
1499 1508 issue_in_different_tree = Issue.find(4)
1500 1509
1501 1510 with_settings :cross_project_subtasks => '' do
1502 1511 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1503 1512 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1504 1513 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1505 1514 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1506 1515 end
1507 1516
1508 1517 with_settings :cross_project_subtasks => 'system' do
1509 1518 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1510 1519 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1511 1520 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1512 1521 end
1513 1522
1514 1523 with_settings :cross_project_subtasks => 'tree' do
1515 1524 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1516 1525 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1517 1526 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1518 1527 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1519 1528
1520 1529 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1521 1530 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1522 1531 end
1523 1532
1524 1533 with_settings :cross_project_subtasks => 'descendants' do
1525 1534 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1526 1535 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1527 1536 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1528 1537 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1529 1538
1530 1539 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1531 1540 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1532 1541 end
1533 1542 end
1534 1543
1535 1544 def test_recipients_should_include_previous_assignee
1536 1545 user = User.find(3)
1537 1546 user.members.update_all ["mail_notification = ?", false]
1538 1547 user.update_attribute :mail_notification, 'only_assigned'
1539 1548
1540 1549 issue = Issue.find(2)
1541 1550 issue.assigned_to = nil
1542 1551 assert_include user.mail, issue.recipients
1543 1552 issue.save!
1544 1553 assert !issue.recipients.include?(user.mail)
1545 1554 end
1546 1555
1547 1556 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1548 1557 issue = Issue.find(12)
1549 1558 assert issue.recipients.include?(issue.author.mail)
1550 1559 # copy the issue to a private project
1551 1560 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1552 1561 # author is not a member of project anymore
1553 1562 assert !copy.recipients.include?(copy.author.mail)
1554 1563 end
1555 1564
1556 1565 def test_recipients_should_include_the_assigned_group_members
1557 1566 group_member = User.generate!
1558 1567 group = Group.generate!
1559 1568 group.users << group_member
1560 1569
1561 1570 issue = Issue.find(12)
1562 1571 issue.assigned_to = group
1563 1572 assert issue.recipients.include?(group_member.mail)
1564 1573 end
1565 1574
1566 1575 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1567 1576 user = User.find(3)
1568 1577 issue = Issue.find(9)
1569 1578 Watcher.create!(:user => user, :watchable => issue)
1570 1579 assert issue.watched_by?(user)
1571 1580 assert !issue.watcher_recipients.include?(user.mail)
1572 1581 end
1573 1582
1574 1583 def test_issue_destroy
1575 1584 Issue.find(1).destroy
1576 1585 assert_nil Issue.find_by_id(1)
1577 1586 assert_nil TimeEntry.find_by_issue_id(1)
1578 1587 end
1579 1588
1580 1589 def test_destroy_should_delete_time_entries_custom_values
1581 1590 issue = Issue.generate!
1582 1591 time_entry = TimeEntry.generate!(:issue => issue, :custom_field_values => {10 => '1'})
1583 1592
1584 1593 assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do
1585 1594 assert issue.destroy
1586 1595 end
1587 1596 end
1588 1597
1589 1598 def test_destroying_a_deleted_issue_should_not_raise_an_error
1590 1599 issue = Issue.find(1)
1591 1600 Issue.find(1).destroy
1592 1601
1593 1602 assert_nothing_raised do
1594 1603 assert_no_difference 'Issue.count' do
1595 1604 issue.destroy
1596 1605 end
1597 1606 assert issue.destroyed?
1598 1607 end
1599 1608 end
1600 1609
1601 1610 def test_destroying_a_stale_issue_should_not_raise_an_error
1602 1611 issue = Issue.find(1)
1603 1612 Issue.find(1).update_attribute :subject, "Updated"
1604 1613
1605 1614 assert_nothing_raised do
1606 1615 assert_difference 'Issue.count', -1 do
1607 1616 issue.destroy
1608 1617 end
1609 1618 assert issue.destroyed?
1610 1619 end
1611 1620 end
1612 1621
1613 1622 def test_blocked
1614 1623 blocked_issue = Issue.find(9)
1615 1624 blocking_issue = Issue.find(10)
1616 1625
1617 1626 assert blocked_issue.blocked?
1618 1627 assert !blocking_issue.blocked?
1619 1628 end
1620 1629
1621 1630 def test_blocked_issues_dont_allow_closed_statuses
1622 1631 blocked_issue = Issue.find(9)
1623 1632
1624 1633 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1625 1634 assert !allowed_statuses.empty?
1626 1635 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1627 1636 assert closed_statuses.empty?
1628 1637 end
1629 1638
1630 1639 def test_unblocked_issues_allow_closed_statuses
1631 1640 blocking_issue = Issue.find(10)
1632 1641
1633 1642 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1634 1643 assert !allowed_statuses.empty?
1635 1644 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1636 1645 assert !closed_statuses.empty?
1637 1646 end
1638 1647
1639 1648 def test_reschedule_an_issue_without_dates
1640 1649 with_settings :non_working_week_days => [] do
1641 1650 issue = Issue.new(:start_date => nil, :due_date => nil)
1642 1651 issue.reschedule_on '2012-10-09'.to_date
1643 1652 assert_equal '2012-10-09'.to_date, issue.start_date
1644 1653 assert_equal '2012-10-09'.to_date, issue.due_date
1645 1654 end
1646 1655
1647 1656 with_settings :non_working_week_days => %w(6 7) do
1648 1657 issue = Issue.new(:start_date => nil, :due_date => nil)
1649 1658 issue.reschedule_on '2012-10-09'.to_date
1650 1659 assert_equal '2012-10-09'.to_date, issue.start_date
1651 1660 assert_equal '2012-10-09'.to_date, issue.due_date
1652 1661
1653 1662 issue = Issue.new(:start_date => nil, :due_date => nil)
1654 1663 issue.reschedule_on '2012-10-13'.to_date
1655 1664 assert_equal '2012-10-15'.to_date, issue.start_date
1656 1665 assert_equal '2012-10-15'.to_date, issue.due_date
1657 1666 end
1658 1667 end
1659 1668
1660 1669 def test_reschedule_an_issue_with_start_date
1661 1670 with_settings :non_working_week_days => [] do
1662 1671 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1663 1672 issue.reschedule_on '2012-10-13'.to_date
1664 1673 assert_equal '2012-10-13'.to_date, issue.start_date
1665 1674 assert_equal '2012-10-13'.to_date, issue.due_date
1666 1675 end
1667 1676
1668 1677 with_settings :non_working_week_days => %w(6 7) do
1669 1678 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1670 1679 issue.reschedule_on '2012-10-11'.to_date
1671 1680 assert_equal '2012-10-11'.to_date, issue.start_date
1672 1681 assert_equal '2012-10-11'.to_date, issue.due_date
1673 1682
1674 1683 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1675 1684 issue.reschedule_on '2012-10-13'.to_date
1676 1685 assert_equal '2012-10-15'.to_date, issue.start_date
1677 1686 assert_equal '2012-10-15'.to_date, issue.due_date
1678 1687 end
1679 1688 end
1680 1689
1681 1690 def test_reschedule_an_issue_with_start_and_due_dates
1682 1691 with_settings :non_working_week_days => [] do
1683 1692 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1684 1693 issue.reschedule_on '2012-10-13'.to_date
1685 1694 assert_equal '2012-10-13'.to_date, issue.start_date
1686 1695 assert_equal '2012-10-19'.to_date, issue.due_date
1687 1696 end
1688 1697
1689 1698 with_settings :non_working_week_days => %w(6 7) do
1690 1699 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1691 1700 issue.reschedule_on '2012-10-11'.to_date
1692 1701 assert_equal '2012-10-11'.to_date, issue.start_date
1693 1702 assert_equal '2012-10-23'.to_date, issue.due_date
1694 1703
1695 1704 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1696 1705 issue.reschedule_on '2012-10-13'.to_date
1697 1706 assert_equal '2012-10-15'.to_date, issue.start_date
1698 1707 assert_equal '2012-10-25'.to_date, issue.due_date
1699 1708 end
1700 1709 end
1701 1710
1702 1711 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1703 1712 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1704 1713 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1705 1714 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1706 1715 :relation_type => IssueRelation::TYPE_PRECEDES)
1707 1716 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1708 1717
1709 1718 issue1.reload
1710 1719 issue1.due_date = '2012-10-23'
1711 1720 issue1.save!
1712 1721 issue2.reload
1713 1722 assert_equal Date.parse('2012-10-24'), issue2.start_date
1714 1723 assert_equal Date.parse('2012-10-26'), issue2.due_date
1715 1724 end
1716 1725
1717 1726 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1718 1727 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1719 1728 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1720 1729 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1721 1730 :relation_type => IssueRelation::TYPE_PRECEDES)
1722 1731 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1723 1732
1724 1733 issue1.reload
1725 1734 issue1.start_date = '2012-09-17'
1726 1735 issue1.due_date = '2012-09-18'
1727 1736 issue1.save!
1728 1737 issue2.reload
1729 1738 assert_equal Date.parse('2012-09-19'), issue2.start_date
1730 1739 assert_equal Date.parse('2012-09-21'), issue2.due_date
1731 1740 end
1732 1741
1733 1742 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1734 1743 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1735 1744 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1736 1745 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
1737 1746 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1738 1747 :relation_type => IssueRelation::TYPE_PRECEDES)
1739 1748 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1740 1749 :relation_type => IssueRelation::TYPE_PRECEDES)
1741 1750 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1742 1751
1743 1752 issue1.reload
1744 1753 issue1.start_date = '2012-09-17'
1745 1754 issue1.due_date = '2012-09-18'
1746 1755 issue1.save!
1747 1756 issue2.reload
1748 1757 # Issue 2 must start after Issue 3
1749 1758 assert_equal Date.parse('2012-10-03'), issue2.start_date
1750 1759 assert_equal Date.parse('2012-10-05'), issue2.due_date
1751 1760 end
1752 1761
1753 1762 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1754 1763 with_settings :non_working_week_days => [] do
1755 1764 stale = Issue.find(1)
1756 1765 issue = Issue.find(1)
1757 1766 issue.subject = "Updated"
1758 1767 issue.save!
1759 1768 date = 10.days.from_now.to_date
1760 1769 assert_nothing_raised do
1761 1770 stale.reschedule_on!(date)
1762 1771 end
1763 1772 assert_equal date, stale.reload.start_date
1764 1773 end
1765 1774 end
1766 1775
1767 1776 def test_child_issue_should_consider_parent_soonest_start_on_create
1768 1777 set_language_if_valid 'en'
1769 1778 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1770 1779 issue2 = Issue.generate!(:start_date => '2012-10-18', :due_date => '2012-10-20')
1771 1780 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1772 1781 :relation_type => IssueRelation::TYPE_PRECEDES)
1773 1782 issue1.reload
1774 1783 issue2.reload
1775 1784 assert_equal Date.parse('2012-10-18'), issue2.start_date
1776 1785
1777 1786 with_settings :date_format => '%m/%d/%Y' do
1778 1787 child = Issue.new(:parent_issue_id => issue2.id, :start_date => '2012-10-16',
1779 1788 :project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Child', :author_id => 1)
1780 1789 assert !child.valid?
1781 1790 assert_include 'Start date cannot be earlier than 10/18/2012 because of preceding issues', child.errors.full_messages
1782 1791 assert_equal Date.parse('2012-10-18'), child.soonest_start
1783 1792 child.start_date = '2012-10-18'
1784 1793 assert child.save
1785 1794 end
1786 1795 end
1787 1796
1788 1797 def test_setting_parent_to_a_dependent_issue_should_not_validate
1789 1798 set_language_if_valid 'en'
1790 1799 issue1 = Issue.generate!
1791 1800 issue2 = Issue.generate!
1792 1801 issue3 = Issue.generate!
1793 1802 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1794 1803 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_PRECEDES)
1795 1804 issue3.reload
1796 1805 issue3.parent_issue_id = issue2.id
1797 1806 assert !issue3.valid?
1798 1807 assert_include 'Parent task is invalid', issue3.errors.full_messages
1799 1808 end
1800 1809
1801 1810 def test_setting_parent_should_not_allow_circular_dependency
1802 1811 set_language_if_valid 'en'
1803 1812 issue1 = Issue.generate!
1804 1813 issue2 = Issue.generate!
1805 1814 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1806 1815 issue3 = Issue.generate!
1807 1816 issue2.reload
1808 1817 issue2.parent_issue_id = issue3.id
1809 1818 issue2.save!
1810 1819 issue4 = Issue.generate!
1811 1820 IssueRelation.create!(:issue_from => issue3, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
1812 1821 issue4.reload
1813 1822 issue4.parent_issue_id = issue1.id
1814 1823 assert !issue4.valid?
1815 1824 assert_include 'Parent task is invalid', issue4.errors.full_messages
1816 1825 end
1817 1826
1818 1827 def test_overdue
1819 1828 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1820 1829 assert !Issue.new(:due_date => Date.today).overdue?
1821 1830 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1822 1831 assert !Issue.new(:due_date => nil).overdue?
1823 1832 assert !Issue.new(:due_date => 1.day.ago.to_date,
1824 1833 :status => IssueStatus.where(:is_closed => true).first
1825 1834 ).overdue?
1826 1835 end
1827 1836
1828 1837 test "#behind_schedule? should be false if the issue has no start_date" do
1829 1838 assert !Issue.new(:start_date => nil,
1830 1839 :due_date => 1.day.from_now.to_date,
1831 1840 :done_ratio => 0).behind_schedule?
1832 1841 end
1833 1842
1834 1843 test "#behind_schedule? should be false if the issue has no end_date" do
1835 1844 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1836 1845 :due_date => nil,
1837 1846 :done_ratio => 0).behind_schedule?
1838 1847 end
1839 1848
1840 1849 test "#behind_schedule? should be false if the issue has more done than it's calendar time" do
1841 1850 assert !Issue.new(:start_date => 50.days.ago.to_date,
1842 1851 :due_date => 50.days.from_now.to_date,
1843 1852 :done_ratio => 90).behind_schedule?
1844 1853 end
1845 1854
1846 1855 test "#behind_schedule? should be true if the issue hasn't been started at all" do
1847 1856 assert Issue.new(:start_date => 1.day.ago.to_date,
1848 1857 :due_date => 1.day.from_now.to_date,
1849 1858 :done_ratio => 0).behind_schedule?
1850 1859 end
1851 1860
1852 1861 test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do
1853 1862 assert Issue.new(:start_date => 100.days.ago.to_date,
1854 1863 :due_date => Date.today,
1855 1864 :done_ratio => 90).behind_schedule?
1856 1865 end
1857 1866
1858 1867 test "#assignable_users should be Users" do
1859 1868 assert_kind_of User, Issue.find(1).assignable_users.first
1860 1869 end
1861 1870
1862 1871 test "#assignable_users should include the issue author" do
1863 1872 non_project_member = User.generate!
1864 1873 issue = Issue.generate!(:author => non_project_member)
1865 1874
1866 1875 assert issue.assignable_users.include?(non_project_member)
1867 1876 end
1868 1877
1869 1878 test "#assignable_users should include the current assignee" do
1870 1879 user = User.generate!
1871 1880 issue = Issue.generate!(:assigned_to => user)
1872 1881 user.lock!
1873 1882
1874 1883 assert Issue.find(issue.id).assignable_users.include?(user)
1875 1884 end
1876 1885
1877 1886 test "#assignable_users should not show the issue author twice" do
1878 1887 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1879 1888 assert_equal 2, assignable_user_ids.length
1880 1889
1881 1890 assignable_user_ids.each do |user_id|
1882 1891 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1883 1892 "User #{user_id} appears more or less than once"
1884 1893 end
1885 1894 end
1886 1895
1887 1896 test "#assignable_users with issue_group_assignment should include groups" do
1888 1897 issue = Issue.new(:project => Project.find(2))
1889 1898
1890 1899 with_settings :issue_group_assignment => '1' do
1891 1900 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1892 1901 assert issue.assignable_users.include?(Group.find(11))
1893 1902 end
1894 1903 end
1895 1904
1896 1905 test "#assignable_users without issue_group_assignment should not include groups" do
1897 1906 issue = Issue.new(:project => Project.find(2))
1898 1907
1899 1908 with_settings :issue_group_assignment => '0' do
1900 1909 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1901 1910 assert !issue.assignable_users.include?(Group.find(11))
1902 1911 end
1903 1912 end
1904 1913
1905 1914 def test_assignable_users_should_not_include_builtin_groups
1906 1915 Member.create!(:project_id => 1, :principal => Group.non_member, :role_ids => [1])
1907 1916 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [1])
1908 1917 issue = Issue.new(:project => Project.find(1))
1909 1918
1910 1919 with_settings :issue_group_assignment => '1' do
1911 1920 assert_nil issue.assignable_users.detect {|u| u.is_a?(GroupBuiltin)}
1912 1921 end
1913 1922 end
1914 1923
1915 1924 def test_create_should_send_email_notification
1916 1925 ActionMailer::Base.deliveries.clear
1917 1926 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1918 1927 :author_id => 3, :status_id => 1,
1919 1928 :priority => IssuePriority.all.first,
1920 1929 :subject => 'test_create', :estimated_hours => '1:30')
1921 1930 with_settings :notified_events => %w(issue_added) do
1922 1931 assert issue.save
1923 1932 assert_equal 1, ActionMailer::Base.deliveries.size
1924 1933 end
1925 1934 end
1926 1935
1927 1936 def test_create_should_send_one_email_notification_with_both_settings
1928 1937 ActionMailer::Base.deliveries.clear
1929 1938 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1930 1939 :author_id => 3, :status_id => 1,
1931 1940 :priority => IssuePriority.all.first,
1932 1941 :subject => 'test_create', :estimated_hours => '1:30')
1933 1942 with_settings :notified_events => %w(issue_added issue_updated) do
1934 1943 assert issue.save
1935 1944 assert_equal 1, ActionMailer::Base.deliveries.size
1936 1945 end
1937 1946 end
1938 1947
1939 1948 def test_create_should_not_send_email_notification_with_no_setting
1940 1949 ActionMailer::Base.deliveries.clear
1941 1950 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1942 1951 :author_id => 3, :status_id => 1,
1943 1952 :priority => IssuePriority.all.first,
1944 1953 :subject => 'test_create', :estimated_hours => '1:30')
1945 1954 with_settings :notified_events => [] do
1946 1955 assert issue.save
1947 1956 assert_equal 0, ActionMailer::Base.deliveries.size
1948 1957 end
1949 1958 end
1950 1959
1951 1960 def test_update_should_notify_previous_assignee
1952 1961 ActionMailer::Base.deliveries.clear
1953 1962 user = User.find(3)
1954 1963 user.members.update_all ["mail_notification = ?", false]
1955 1964 user.update_attribute :mail_notification, 'only_assigned'
1956 1965
1957 1966 with_settings :notified_events => %w(issue_updated) do
1958 1967 issue = Issue.find(2)
1959 1968 issue.init_journal User.find(1)
1960 1969 issue.assigned_to = nil
1961 1970 issue.save!
1962 1971 assert_include user.mail, ActionMailer::Base.deliveries.last.bcc
1963 1972 end
1964 1973 end
1965 1974
1966 1975 def test_stale_issue_should_not_send_email_notification
1967 1976 ActionMailer::Base.deliveries.clear
1968 1977 issue = Issue.find(1)
1969 1978 stale = Issue.find(1)
1970 1979
1971 1980 issue.init_journal(User.find(1))
1972 1981 issue.subject = 'Subjet update'
1973 1982 with_settings :notified_events => %w(issue_updated) do
1974 1983 assert issue.save
1975 1984 assert_equal 1, ActionMailer::Base.deliveries.size
1976 1985 ActionMailer::Base.deliveries.clear
1977 1986
1978 1987 stale.init_journal(User.find(1))
1979 1988 stale.subject = 'Another subjet update'
1980 1989 assert_raise ActiveRecord::StaleObjectError do
1981 1990 stale.save
1982 1991 end
1983 1992 assert ActionMailer::Base.deliveries.empty?
1984 1993 end
1985 1994 end
1986 1995
1987 1996 def test_journalized_description
1988 1997 IssueCustomField.delete_all
1989 1998
1990 1999 i = Issue.first
1991 2000 old_description = i.description
1992 2001 new_description = "This is the new description"
1993 2002
1994 2003 i.init_journal(User.find(2))
1995 2004 i.description = new_description
1996 2005 assert_difference 'Journal.count', 1 do
1997 2006 assert_difference 'JournalDetail.count', 1 do
1998 2007 i.save!
1999 2008 end
2000 2009 end
2001 2010
2002 2011 detail = JournalDetail.order('id DESC').first
2003 2012 assert_equal i, detail.journal.journalized
2004 2013 assert_equal 'attr', detail.property
2005 2014 assert_equal 'description', detail.prop_key
2006 2015 assert_equal old_description, detail.old_value
2007 2016 assert_equal new_description, detail.value
2008 2017 end
2009 2018
2010 2019 def test_blank_descriptions_should_not_be_journalized
2011 2020 IssueCustomField.delete_all
2012 2021 Issue.where(:id => 1).update_all("description = NULL")
2013 2022
2014 2023 i = Issue.find(1)
2015 2024 i.init_journal(User.find(2))
2016 2025 i.subject = "blank description"
2017 2026 i.description = "\r\n"
2018 2027
2019 2028 assert_difference 'Journal.count', 1 do
2020 2029 assert_difference 'JournalDetail.count', 1 do
2021 2030 i.save!
2022 2031 end
2023 2032 end
2024 2033 end
2025 2034
2026 2035 def test_journalized_multi_custom_field
2027 2036 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
2028 2037 :is_filter => true, :is_for_all => true,
2029 2038 :tracker_ids => [1],
2030 2039 :possible_values => ['value1', 'value2', 'value3'],
2031 2040 :multiple => true)
2032 2041
2033 2042 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
2034 2043 :subject => 'Test', :author_id => 1)
2035 2044
2036 2045 assert_difference 'Journal.count' do
2037 2046 assert_difference 'JournalDetail.count' do
2038 2047 issue.init_journal(User.first)
2039 2048 issue.custom_field_values = {field.id => ['value1']}
2040 2049 issue.save!
2041 2050 end
2042 2051 assert_difference 'JournalDetail.count' do
2043 2052 issue.init_journal(User.first)
2044 2053 issue.custom_field_values = {field.id => ['value1', 'value2']}
2045 2054 issue.save!
2046 2055 end
2047 2056 assert_difference 'JournalDetail.count', 2 do
2048 2057 issue.init_journal(User.first)
2049 2058 issue.custom_field_values = {field.id => ['value3', 'value2']}
2050 2059 issue.save!
2051 2060 end
2052 2061 assert_difference 'JournalDetail.count', 2 do
2053 2062 issue.init_journal(User.first)
2054 2063 issue.custom_field_values = {field.id => nil}
2055 2064 issue.save!
2056 2065 end
2057 2066 end
2058 2067 end
2059 2068
2060 2069 def test_description_eol_should_be_normalized
2061 2070 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
2062 2071 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
2063 2072 end
2064 2073
2065 2074 def test_saving_twice_should_not_duplicate_journal_details
2066 2075 i = Issue.first
2067 2076 i.init_journal(User.find(2), 'Some notes')
2068 2077 # initial changes
2069 2078 i.subject = 'New subject'
2070 2079 i.done_ratio = i.done_ratio + 10
2071 2080 assert_difference 'Journal.count' do
2072 2081 assert i.save
2073 2082 end
2074 2083 # 1 more change
2075 2084 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
2076 2085 assert_no_difference 'Journal.count' do
2077 2086 assert_difference 'JournalDetail.count', 1 do
2078 2087 i.save
2079 2088 end
2080 2089 end
2081 2090 # no more change
2082 2091 assert_no_difference 'Journal.count' do
2083 2092 assert_no_difference 'JournalDetail.count' do
2084 2093 i.save
2085 2094 end
2086 2095 end
2087 2096 end
2088 2097
2089 2098 def test_all_dependent_issues
2090 2099 IssueRelation.delete_all
2091 2100 assert IssueRelation.create!(:issue_from => Issue.find(1),
2092 2101 :issue_to => Issue.find(2),
2093 2102 :relation_type => IssueRelation::TYPE_PRECEDES)
2094 2103 assert IssueRelation.create!(:issue_from => Issue.find(2),
2095 2104 :issue_to => Issue.find(3),
2096 2105 :relation_type => IssueRelation::TYPE_PRECEDES)
2097 2106 assert IssueRelation.create!(:issue_from => Issue.find(3),
2098 2107 :issue_to => Issue.find(8),
2099 2108 :relation_type => IssueRelation::TYPE_PRECEDES)
2100 2109
2101 2110 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
2102 2111 end
2103 2112
2104 2113 def test_all_dependent_issues_with_subtask
2105 2114 IssueRelation.delete_all
2106 2115
2107 2116 project = Project.generate!(:name => "testproject")
2108 2117
2109 2118 parentIssue = Issue.generate!(:project => project)
2110 2119 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
2111 2120 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
2112 2121
2113 2122 assert_equal [childIssue1.id, childIssue2.id].sort, parentIssue.all_dependent_issues.collect(&:id).uniq.sort
2114 2123 end
2115 2124
2116 2125 def test_all_dependent_issues_does_not_include_self
2117 2126 IssueRelation.delete_all
2118 2127
2119 2128 project = Project.generate!(:name => "testproject")
2120 2129
2121 2130 parentIssue = Issue.generate!(:project => project)
2122 2131 childIssue = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
2123 2132
2124 2133 assert_equal [childIssue.id], parentIssue.all_dependent_issues.collect(&:id)
2125 2134 end
2126 2135
2127 2136 def test_all_dependent_issues_with_parenttask_and_sibling
2128 2137 IssueRelation.delete_all
2129 2138
2130 2139 project = Project.generate!(:name => "testproject")
2131 2140
2132 2141 parentIssue = Issue.generate!(:project => project)
2133 2142 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
2134 2143 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
2135 2144
2136 2145 assert_equal [parentIssue.id].sort, childIssue1.all_dependent_issues.collect(&:id)
2137 2146 end
2138 2147
2139 2148 def test_all_dependent_issues_with_relation_to_leaf_in_other_tree
2140 2149 IssueRelation.delete_all
2141 2150
2142 2151 project = Project.generate!(:name => "testproject")
2143 2152
2144 2153 parentIssue1 = Issue.generate!(:project => project)
2145 2154 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2146 2155 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2147 2156
2148 2157 parentIssue2 = Issue.generate!(:project => project)
2149 2158 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2150 2159 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2151 2160
2152 2161
2153 2162 assert IssueRelation.create(:issue_from => parentIssue1,
2154 2163 :issue_to => childIssue2_2,
2155 2164 :relation_type => IssueRelation::TYPE_BLOCKS)
2156 2165
2157 2166 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_2.id].sort,
2158 2167 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
2159 2168 end
2160 2169
2161 2170 def test_all_dependent_issues_with_relation_to_parent_in_other_tree
2162 2171 IssueRelation.delete_all
2163 2172
2164 2173 project = Project.generate!(:name => "testproject")
2165 2174
2166 2175 parentIssue1 = Issue.generate!(:project => project)
2167 2176 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2168 2177 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2169 2178
2170 2179 parentIssue2 = Issue.generate!(:project => project)
2171 2180 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2172 2181 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2173 2182
2174 2183
2175 2184 assert IssueRelation.create(:issue_from => parentIssue1,
2176 2185 :issue_to => parentIssue2,
2177 2186 :relation_type => IssueRelation::TYPE_BLOCKS)
2178 2187
2179 2188 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_1.id, childIssue2_2.id].sort,
2180 2189 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
2181 2190 end
2182 2191
2183 2192 def test_all_dependent_issues_with_transitive_relation
2184 2193 IssueRelation.delete_all
2185 2194
2186 2195 project = Project.generate!(:name => "testproject")
2187 2196
2188 2197 parentIssue1 = Issue.generate!(:project => project)
2189 2198 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2190 2199
2191 2200 parentIssue2 = Issue.generate!(:project => project)
2192 2201 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2193 2202
2194 2203 independentIssue = Issue.generate!(:project => project)
2195 2204
2196 2205 assert IssueRelation.create(:issue_from => parentIssue1,
2197 2206 :issue_to => childIssue2_1,
2198 2207 :relation_type => IssueRelation::TYPE_RELATES)
2199 2208
2200 2209 assert IssueRelation.create(:issue_from => childIssue2_1,
2201 2210 :issue_to => independentIssue,
2202 2211 :relation_type => IssueRelation::TYPE_RELATES)
2203 2212
2204 2213 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
2205 2214 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
2206 2215 end
2207 2216
2208 2217 def test_all_dependent_issues_with_transitive_relation2
2209 2218 IssueRelation.delete_all
2210 2219
2211 2220 project = Project.generate!(:name => "testproject")
2212 2221
2213 2222 parentIssue1 = Issue.generate!(:project => project)
2214 2223 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2215 2224
2216 2225 parentIssue2 = Issue.generate!(:project => project)
2217 2226 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2218 2227
2219 2228 independentIssue = Issue.generate!(:project => project)
2220 2229
2221 2230 assert IssueRelation.create(:issue_from => parentIssue1,
2222 2231 :issue_to => independentIssue,
2223 2232 :relation_type => IssueRelation::TYPE_RELATES)
2224 2233
2225 2234 assert IssueRelation.create(:issue_from => independentIssue,
2226 2235 :issue_to => childIssue2_1,
2227 2236 :relation_type => IssueRelation::TYPE_RELATES)
2228 2237
2229 2238 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
2230 2239 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
2231 2240
2232 2241 end
2233 2242
2234 2243 def test_all_dependent_issues_with_persistent_circular_dependency
2235 2244 IssueRelation.delete_all
2236 2245 assert IssueRelation.create!(:issue_from => Issue.find(1),
2237 2246 :issue_to => Issue.find(2),
2238 2247 :relation_type => IssueRelation::TYPE_PRECEDES)
2239 2248 assert IssueRelation.create!(:issue_from => Issue.find(2),
2240 2249 :issue_to => Issue.find(3),
2241 2250 :relation_type => IssueRelation::TYPE_PRECEDES)
2242 2251
2243 2252 r = IssueRelation.create!(:issue_from => Issue.find(3),
2244 2253 :issue_to => Issue.find(7),
2245 2254 :relation_type => IssueRelation::TYPE_PRECEDES)
2246 2255 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 1")
2247 2256
2248 2257 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
2249 2258 end
2250 2259
2251 2260 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
2252 2261 IssueRelation.delete_all
2253 2262 assert IssueRelation.create!(:issue_from => Issue.find(1),
2254 2263 :issue_to => Issue.find(2),
2255 2264 :relation_type => IssueRelation::TYPE_RELATES)
2256 2265 assert IssueRelation.create!(:issue_from => Issue.find(2),
2257 2266 :issue_to => Issue.find(3),
2258 2267 :relation_type => IssueRelation::TYPE_RELATES)
2259 2268 assert IssueRelation.create!(:issue_from => Issue.find(3),
2260 2269 :issue_to => Issue.find(8),
2261 2270 :relation_type => IssueRelation::TYPE_RELATES)
2262 2271
2263 2272 r = IssueRelation.create!(:issue_from => Issue.find(8),
2264 2273 :issue_to => Issue.find(7),
2265 2274 :relation_type => IssueRelation::TYPE_RELATES)
2266 2275 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 2")
2267 2276
2268 2277 r = IssueRelation.create!(:issue_from => Issue.find(3),
2269 2278 :issue_to => Issue.find(7),
2270 2279 :relation_type => IssueRelation::TYPE_RELATES)
2271 2280 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 1")
2272 2281
2273 2282 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
2274 2283 end
2275 2284
2276 2285 test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do
2277 2286 @issue = Issue.find(1)
2278 2287 @issue_status = IssueStatus.find(1)
2279 2288 @issue_status.update_attribute(:default_done_ratio, 50)
2280 2289 @issue2 = Issue.find(2)
2281 2290 @issue_status2 = IssueStatus.find(2)
2282 2291 @issue_status2.update_attribute(:default_done_ratio, 0)
2283 2292
2284 2293 with_settings :issue_done_ratio => 'issue_field' do
2285 2294 assert_equal 0, @issue.done_ratio
2286 2295 assert_equal 30, @issue2.done_ratio
2287 2296 end
2288 2297
2289 2298 with_settings :issue_done_ratio => 'issue_status' do
2290 2299 assert_equal 50, @issue.done_ratio
2291 2300 assert_equal 0, @issue2.done_ratio
2292 2301 end
2293 2302 end
2294 2303
2295 2304 test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
2296 2305 @issue = Issue.find(1)
2297 2306 @issue_status = IssueStatus.find(1)
2298 2307 @issue_status.update_attribute(:default_done_ratio, 50)
2299 2308 @issue2 = Issue.find(2)
2300 2309 @issue_status2 = IssueStatus.find(2)
2301 2310 @issue_status2.update_attribute(:default_done_ratio, 0)
2302 2311
2303 2312 with_settings :issue_done_ratio => 'issue_field' do
2304 2313 @issue.update_done_ratio_from_issue_status
2305 2314 @issue2.update_done_ratio_from_issue_status
2306 2315
2307 2316 assert_equal 0, @issue.read_attribute(:done_ratio)
2308 2317 assert_equal 30, @issue2.read_attribute(:done_ratio)
2309 2318 end
2310 2319
2311 2320 with_settings :issue_done_ratio => 'issue_status' do
2312 2321 @issue.update_done_ratio_from_issue_status
2313 2322 @issue2.update_done_ratio_from_issue_status
2314 2323
2315 2324 assert_equal 50, @issue.read_attribute(:done_ratio)
2316 2325 assert_equal 0, @issue2.read_attribute(:done_ratio)
2317 2326 end
2318 2327 end
2319 2328
2320 2329 test "#by_tracker" do
2321 2330 User.current = User.anonymous
2322 2331 groups = Issue.by_tracker(Project.find(1))
2323 2332 assert_equal 3, groups.count
2324 2333 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2325 2334 end
2326 2335
2327 2336 test "#by_version" do
2328 2337 User.current = User.anonymous
2329 2338 groups = Issue.by_version(Project.find(1))
2330 2339 assert_equal 3, groups.count
2331 2340 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2332 2341 end
2333 2342
2334 2343 test "#by_priority" do
2335 2344 User.current = User.anonymous
2336 2345 groups = Issue.by_priority(Project.find(1))
2337 2346 assert_equal 4, groups.count
2338 2347 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2339 2348 end
2340 2349
2341 2350 test "#by_category" do
2342 2351 User.current = User.anonymous
2343 2352 groups = Issue.by_category(Project.find(1))
2344 2353 assert_equal 2, groups.count
2345 2354 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2346 2355 end
2347 2356
2348 2357 test "#by_assigned_to" do
2349 2358 User.current = User.anonymous
2350 2359 groups = Issue.by_assigned_to(Project.find(1))
2351 2360 assert_equal 2, groups.count
2352 2361 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2353 2362 end
2354 2363
2355 2364 test "#by_author" do
2356 2365 User.current = User.anonymous
2357 2366 groups = Issue.by_author(Project.find(1))
2358 2367 assert_equal 4, groups.count
2359 2368 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2360 2369 end
2361 2370
2362 2371 test "#by_subproject" do
2363 2372 User.current = User.anonymous
2364 2373 groups = Issue.by_subproject(Project.find(1))
2365 2374 # Private descendant not visible
2366 2375 assert_equal 1, groups.count
2367 2376 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2368 2377 end
2369 2378
2370 2379 def test_recently_updated_scope
2371 2380 #should return the last updated issue
2372 2381 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
2373 2382 end
2374 2383
2375 2384 def test_on_active_projects_scope
2376 2385 assert Project.find(2).archive
2377 2386
2378 2387 before = Issue.on_active_project.length
2379 2388 # test inclusion to results
2380 2389 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
2381 2390 assert_equal before + 1, Issue.on_active_project.length
2382 2391
2383 2392 # Move to an archived project
2384 2393 issue.project = Project.find(2)
2385 2394 assert issue.save
2386 2395 assert_equal before, Issue.on_active_project.length
2387 2396 end
2388 2397
2389 2398 test "Issue#recipients should include project recipients" do
2390 2399 issue = Issue.generate!
2391 2400 assert issue.project.recipients.present?
2392 2401 issue.project.recipients.each do |project_recipient|
2393 2402 assert issue.recipients.include?(project_recipient)
2394 2403 end
2395 2404 end
2396 2405
2397 2406 test "Issue#recipients should include the author if the author is active" do
2398 2407 issue = Issue.generate!(:author => User.generate!)
2399 2408 assert issue.author, "No author set for Issue"
2400 2409 assert issue.recipients.include?(issue.author.mail)
2401 2410 end
2402 2411
2403 2412 test "Issue#recipients should include the assigned to user if the assigned to user is active" do
2404 2413 issue = Issue.generate!(:assigned_to => User.generate!)
2405 2414 assert issue.assigned_to, "No assigned_to set for Issue"
2406 2415 assert issue.recipients.include?(issue.assigned_to.mail)
2407 2416 end
2408 2417
2409 2418 test "Issue#recipients should not include users who opt out of all email" do
2410 2419 issue = Issue.generate!(:author => User.generate!)
2411 2420 issue.author.update_attribute(:mail_notification, :none)
2412 2421 assert !issue.recipients.include?(issue.author.mail)
2413 2422 end
2414 2423
2415 2424 test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do
2416 2425 issue = Issue.generate!(:author => User.generate!)
2417 2426 issue.author.update_attribute(:mail_notification, :only_assigned)
2418 2427 assert !issue.recipients.include?(issue.author.mail)
2419 2428 end
2420 2429
2421 2430 test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do
2422 2431 issue = Issue.generate!(:assigned_to => User.generate!)
2423 2432 issue.assigned_to.update_attribute(:mail_notification, :only_owner)
2424 2433 assert !issue.recipients.include?(issue.assigned_to.mail)
2425 2434 end
2426 2435
2427 2436 def test_last_journal_id_with_journals_should_return_the_journal_id
2428 2437 assert_equal 2, Issue.find(1).last_journal_id
2429 2438 end
2430 2439
2431 2440 def test_last_journal_id_without_journals_should_return_nil
2432 2441 assert_nil Issue.find(3).last_journal_id
2433 2442 end
2434 2443
2435 2444 def test_journals_after_should_return_journals_with_greater_id
2436 2445 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
2437 2446 assert_equal [], Issue.find(1).journals_after('2')
2438 2447 end
2439 2448
2440 2449 def test_journals_after_with_blank_arg_should_return_all_journals
2441 2450 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
2442 2451 end
2443 2452
2444 2453 def test_css_classes_should_include_tracker
2445 2454 issue = Issue.new(:tracker => Tracker.find(2))
2446 2455 classes = issue.css_classes.split(' ')
2447 2456 assert_include 'tracker-2', classes
2448 2457 end
2449 2458
2450 2459 def test_css_classes_should_include_priority
2451 2460 issue = Issue.new(:priority => IssuePriority.find(8))
2452 2461 classes = issue.css_classes.split(' ')
2453 2462 assert_include 'priority-8', classes
2454 2463 assert_include 'priority-highest', classes
2455 2464 end
2456 2465
2457 2466 def test_css_classes_should_include_user_and_group_assignment
2458 2467 project = Project.first
2459 2468 user = User.generate!
2460 2469 group = Group.generate!
2461 2470 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
2462 2471 group.users << user
2463 2472 assert user.member_of?(project)
2464 2473 issue1 = Issue.generate(:assigned_to_id => group.id)
2465 2474 assert_include 'assigned-to-my-group', issue1.css_classes(user)
2466 2475 assert_not_include 'assigned-to-me', issue1.css_classes(user)
2467 2476 issue2 = Issue.generate(:assigned_to_id => user.id)
2468 2477 assert_not_include 'assigned-to-my-group', issue2.css_classes(user)
2469 2478 assert_include 'assigned-to-me', issue2.css_classes(user)
2470 2479 end
2471 2480
2472 2481 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
2473 2482 set_tmp_attachments_directory
2474 2483 issue = Issue.generate!
2475 2484 issue.save_attachments({
2476 2485 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
2477 2486 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
2478 2487 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
2479 2488 })
2480 2489 issue.attach_saved_attachments
2481 2490
2482 2491 assert_equal 3, issue.reload.attachments.count
2483 2492 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
2484 2493 end
2485 2494
2486 2495 def test_closed_on_should_be_nil_when_creating_an_open_issue
2487 2496 issue = Issue.generate!(:status_id => 1).reload
2488 2497 assert !issue.closed?
2489 2498 assert_nil issue.closed_on
2490 2499 end
2491 2500
2492 2501 def test_closed_on_should_be_set_when_creating_a_closed_issue
2493 2502 issue = Issue.generate!(:status_id => 5).reload
2494 2503 assert issue.closed?
2495 2504 assert_not_nil issue.closed_on
2496 2505 assert_equal issue.updated_on, issue.closed_on
2497 2506 assert_equal issue.created_on, issue.closed_on
2498 2507 end
2499 2508
2500 2509 def test_closed_on_should_be_nil_when_updating_an_open_issue
2501 2510 issue = Issue.find(1)
2502 2511 issue.subject = 'Not closed yet'
2503 2512 issue.save!
2504 2513 issue.reload
2505 2514 assert_nil issue.closed_on
2506 2515 end
2507 2516
2508 2517 def test_closed_on_should_be_set_when_closing_an_open_issue
2509 2518 issue = Issue.find(1)
2510 2519 issue.subject = 'Now closed'
2511 2520 issue.status_id = 5
2512 2521 issue.save!
2513 2522 issue.reload
2514 2523 assert_not_nil issue.closed_on
2515 2524 assert_equal issue.updated_on, issue.closed_on
2516 2525 end
2517 2526
2518 2527 def test_closed_on_should_not_be_updated_when_updating_a_closed_issue
2519 2528 issue = Issue.open(false).first
2520 2529 was_closed_on = issue.closed_on
2521 2530 assert_not_nil was_closed_on
2522 2531 issue.subject = 'Updating a closed issue'
2523 2532 issue.save!
2524 2533 issue.reload
2525 2534 assert_equal was_closed_on, issue.closed_on
2526 2535 end
2527 2536
2528 2537 def test_closed_on_should_be_preserved_when_reopening_a_closed_issue
2529 2538 issue = Issue.open(false).first
2530 2539 was_closed_on = issue.closed_on
2531 2540 assert_not_nil was_closed_on
2532 2541 issue.subject = 'Reopening a closed issue'
2533 2542 issue.status_id = 1
2534 2543 issue.save!
2535 2544 issue.reload
2536 2545 assert !issue.closed?
2537 2546 assert_equal was_closed_on, issue.closed_on
2538 2547 end
2539 2548
2540 2549 def test_status_was_should_return_nil_for_new_issue
2541 2550 issue = Issue.new
2542 2551 assert_nil issue.status_was
2543 2552 end
2544 2553
2545 2554 def test_status_was_should_return_status_before_change
2546 2555 issue = Issue.find(1)
2547 2556 issue.status = IssueStatus.find(2)
2548 2557 assert_equal IssueStatus.find(1), issue.status_was
2549 2558 end
2550 2559
2551 2560 def test_status_was_should_return_status_before_change_with_status_id
2552 2561 issue = Issue.find(1)
2553 2562 assert_equal IssueStatus.find(1), issue.status
2554 2563 issue.status_id = 2
2555 2564 assert_equal IssueStatus.find(1), issue.status_was
2556 2565 end
2557 2566
2558 2567 def test_status_was_should_be_reset_on_save
2559 2568 issue = Issue.find(1)
2560 2569 issue.status = IssueStatus.find(2)
2561 2570 assert_equal IssueStatus.find(1), issue.status_was
2562 2571 assert issue.save!
2563 2572 assert_equal IssueStatus.find(2), issue.status_was
2564 2573 end
2565 2574
2566 2575 def test_closing_should_return_true_when_closing_an_issue
2567 2576 issue = Issue.find(1)
2568 2577 issue.status = IssueStatus.find(2)
2569 2578 assert_equal false, issue.closing?
2570 2579 issue.status = IssueStatus.find(5)
2571 2580 assert_equal true, issue.closing?
2572 2581 end
2573 2582
2574 2583 def test_closing_should_return_true_when_closing_an_issue_with_status_id
2575 2584 issue = Issue.find(1)
2576 2585 issue.status_id = 2
2577 2586 assert_equal false, issue.closing?
2578 2587 issue.status_id = 5
2579 2588 assert_equal true, issue.closing?
2580 2589 end
2581 2590
2582 2591 def test_closing_should_return_true_for_new_closed_issue
2583 2592 issue = Issue.new
2584 2593 assert_equal false, issue.closing?
2585 2594 issue.status = IssueStatus.find(5)
2586 2595 assert_equal true, issue.closing?
2587 2596 end
2588 2597
2589 2598 def test_closing_should_return_true_for_new_closed_issue_with_status_id
2590 2599 issue = Issue.new
2591 2600 assert_equal false, issue.closing?
2592 2601 issue.status_id = 5
2593 2602 assert_equal true, issue.closing?
2594 2603 end
2595 2604
2596 2605 def test_closing_should_be_reset_after_save
2597 2606 issue = Issue.find(1)
2598 2607 issue.status_id = 5
2599 2608 assert_equal true, issue.closing?
2600 2609 issue.save!
2601 2610 assert_equal false, issue.closing?
2602 2611 end
2603 2612
2604 2613 def test_reopening_should_return_true_when_reopening_an_issue
2605 2614 issue = Issue.find(8)
2606 2615 issue.status = IssueStatus.find(6)
2607 2616 assert_equal false, issue.reopening?
2608 2617 issue.status = IssueStatus.find(2)
2609 2618 assert_equal true, issue.reopening?
2610 2619 end
2611 2620
2612 2621 def test_reopening_should_return_true_when_reopening_an_issue_with_status_id
2613 2622 issue = Issue.find(8)
2614 2623 issue.status_id = 6
2615 2624 assert_equal false, issue.reopening?
2616 2625 issue.status_id = 2
2617 2626 assert_equal true, issue.reopening?
2618 2627 end
2619 2628
2620 2629 def test_reopening_should_return_false_for_new_open_issue
2621 2630 issue = Issue.new
2622 2631 issue.status = IssueStatus.find(1)
2623 2632 assert_equal false, issue.reopening?
2624 2633 end
2625 2634
2626 2635 def test_reopening_should_be_reset_after_save
2627 2636 issue = Issue.find(8)
2628 2637 issue.status_id = 2
2629 2638 assert_equal true, issue.reopening?
2630 2639 issue.save!
2631 2640 assert_equal false, issue.reopening?
2632 2641 end
2633 2642
2634 2643 def test_default_status_without_tracker_should_be_nil
2635 2644 issue = Issue.new
2636 2645 assert_nil issue.tracker
2637 2646 assert_nil issue.default_status
2638 2647 end
2639 2648
2640 2649 def test_default_status_should_be_tracker_default_status
2641 2650 issue = Issue.new(:tracker_id => 1)
2642 2651 assert_not_nil issue.status
2643 2652 assert_equal issue.tracker.default_status, issue.default_status
2644 2653 end
2645 2654
2646 2655 def test_initializing_with_tracker_should_set_default_status
2647 2656 issue = Issue.new(:tracker => Tracker.find(1))
2648 2657 assert_not_nil issue.status
2649 2658 assert_equal issue.default_status, issue.status
2650 2659 end
2651 2660
2652 2661 def test_initializing_with_tracker_id_should_set_default_status
2653 2662 issue = Issue.new(:tracker_id => 1)
2654 2663 assert_not_nil issue.status
2655 2664 assert_equal issue.default_status, issue.status
2656 2665 end
2657 2666
2658 2667 def test_setting_tracker_should_set_default_status
2659 2668 issue = Issue.new
2660 2669 issue.tracker = Tracker.find(1)
2661 2670 assert_not_nil issue.status
2662 2671 assert_equal issue.default_status, issue.status
2663 2672 end
2664 2673
2665 2674 def test_changing_tracker_should_set_default_status_if_status_was_default
2666 2675 WorkflowTransition.delete_all
2667 2676 WorkflowTransition.create! :role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1
2668 2677 Tracker.find(2).update! :default_status_id => 2
2669 2678
2670 2679 issue = Issue.new(:tracker_id => 1, :status_id => 1)
2671 2680 assert_equal IssueStatus.find(1), issue.status
2672 2681 issue.tracker = Tracker.find(2)
2673 2682 assert_equal IssueStatus.find(2), issue.status
2674 2683 end
2675 2684
2676 2685 def test_changing_tracker_should_set_default_status_if_status_is_not_used_by_tracker
2677 2686 WorkflowTransition.delete_all
2678 2687 Tracker.find(2).update! :default_status_id => 2
2679 2688
2680 2689 issue = Issue.new(:tracker_id => 1, :status_id => 3)
2681 2690 assert_equal IssueStatus.find(3), issue.status
2682 2691 issue.tracker = Tracker.find(2)
2683 2692 assert_equal IssueStatus.find(2), issue.status
2684 2693 end
2685 2694
2686 2695 def test_changing_tracker_should_keep_status_if_status_was_not_default_and_is_used_by_tracker
2687 2696 WorkflowTransition.delete_all
2688 2697 WorkflowTransition.create! :role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3
2689 2698 Tracker.find(2).update! :default_status_id => 2
2690 2699
2691 2700 issue = Issue.new(:tracker_id => 1, :status_id => 3)
2692 2701 assert_equal IssueStatus.find(3), issue.status
2693 2702 issue.tracker = Tracker.find(2)
2694 2703 assert_equal IssueStatus.find(3), issue.status
2695 2704 end
2696 2705
2697 2706 def test_assigned_to_was_with_a_group
2698 2707 group = Group.find(10)
2699 2708
2700 2709 issue = Issue.generate!(:assigned_to => group)
2701 2710 issue.reload.assigned_to = nil
2702 2711 assert_equal group, issue.assigned_to_was
2703 2712 end
2704 2713 end
General Comments 0
You need to be logged in to leave comments. Login now