##// END OF EJS Templates
Fixed: children projects are deleted instead of being destroyed when destroying parent project (#7904)....
Jean-Philippe Lang -
r5051:50cb77cfbb09
parent child
Show More
@@ -1,847 +1,840
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 # Project statuses
22 22 STATUS_ACTIVE = 1
23 23 STATUS_ARCHIVED = 9
24 24
25 25 # Maximum length for project identifiers
26 26 IDENTIFIER_MAX_LENGTH = 100
27 27
28 28 # Specific overidden Activities
29 29 has_many :time_entry_activities
30 30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 31 has_many :memberships, :class_name => 'Member'
32 32 has_many :member_principals, :class_name => 'Member',
33 33 :include => :principal,
34 34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 35 has_many :users, :through => :members
36 36 has_many :principals, :through => :member_principals, :source => :principal
37 37
38 38 has_many :enabled_modules, :dependent => :delete_all
39 39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 41 has_many :issue_changes, :through => :issues, :source => :journals
42 42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 43 has_many :time_entries, :dependent => :delete_all
44 44 has_many :queries, :dependent => :delete_all
45 45 has_many :documents, :dependent => :destroy
46 46 has_many :news, :dependent => :delete_all, :include => :author
47 47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 49 has_one :repository, :dependent => :destroy
50 50 has_many :changesets, :through => :repository
51 51 has_one :wiki, :dependent => :destroy
52 52 # Custom field for the project issues
53 53 has_and_belongs_to_many :issue_custom_fields,
54 54 :class_name => 'IssueCustomField',
55 55 :order => "#{CustomField.table_name}.position",
56 56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 57 :association_foreign_key => 'custom_field_id'
58 58
59 acts_as_nested_set :order => 'name'
59 acts_as_nested_set :order => 'name', :dependent => :destroy
60 60 acts_as_attachable :view_permission => :view_files,
61 61 :delete_permission => :manage_files
62 62
63 63 acts_as_customizable
64 64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 67 :author => nil
68 68
69 69 attr_protected :status
70 70
71 71 validates_presence_of :name, :identifier
72 72 validates_uniqueness_of :identifier
73 73 validates_associated :repository, :wiki
74 74 validates_length_of :name, :maximum => 255
75 75 validates_length_of :homepage, :maximum => 255
76 76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 77 # donwcase letters, digits, dashes but not digits only
78 78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 79 # reserved words
80 80 validates_exclusion_of :identifier, :in => %w( new )
81 81
82 before_destroy :delete_all_members, :destroy_children
82 before_destroy :delete_all_members
83 83
84 84 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
85 85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 86 named_scope :all_public, { :conditions => { :is_public => true } }
87 87 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
88 88
89 89 def initialize(attributes = nil)
90 90 super
91 91
92 92 initialized = (attributes || {}).stringify_keys
93 93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
94 94 self.identifier = Project.next_identifier
95 95 end
96 96 if !initialized.key?('is_public')
97 97 self.is_public = Setting.default_projects_public?
98 98 end
99 99 if !initialized.key?('enabled_module_names')
100 100 self.enabled_module_names = Setting.default_projects_modules
101 101 end
102 102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
103 103 self.trackers = Tracker.all
104 104 end
105 105 end
106 106
107 107 def identifier=(identifier)
108 108 super unless identifier_frozen?
109 109 end
110 110
111 111 def identifier_frozen?
112 112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
113 113 end
114 114
115 115 # returns latest created projects
116 116 # non public projects will be returned only if user is a member of those
117 117 def self.latest(user=nil, count=5)
118 118 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
119 119 end
120 120
121 121 # Returns a SQL :conditions string used to find all active projects for the specified user.
122 122 #
123 123 # Examples:
124 124 # Projects.visible_by(admin) => "projects.status = 1"
125 125 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
126 126 def self.visible_by(user=nil)
127 127 user ||= User.current
128 128 if user && user.admin?
129 129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
130 130 elsif user && user.memberships.any?
131 131 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
132 132 else
133 133 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
134 134 end
135 135 end
136 136
137 137 def self.allowed_to_condition(user, permission, options={})
138 138 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
139 139 if perm = Redmine::AccessControl.permission(permission)
140 140 unless perm.project_module.nil?
141 141 # If the permission belongs to a project module, make sure the module is enabled
142 142 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
143 143 end
144 144 end
145 145 if options[:project]
146 146 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
147 147 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
148 148 base_statement = "(#{project_statement}) AND (#{base_statement})"
149 149 end
150 150
151 151 if user.admin?
152 152 base_statement
153 153 else
154 154 statement_by_role = {}
155 155 if user.logged?
156 156 if Role.non_member.allowed_to?(permission) && !options[:member]
157 157 statement_by_role[Role.non_member] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
158 158 end
159 159 user.projects_by_role.each do |role, projects|
160 160 if role.allowed_to?(permission)
161 161 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
162 162 end
163 163 end
164 164 else
165 165 if Role.anonymous.allowed_to?(permission) && !options[:member]
166 166 statement_by_role[Role.anonymous] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
167 167 end
168 168 end
169 169 if statement_by_role.empty?
170 170 "1=0"
171 171 else
172 172 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
173 173 end
174 174 end
175 175 end
176 176
177 177 # Returns the Systemwide and project specific activities
178 178 def activities(include_inactive=false)
179 179 if include_inactive
180 180 return all_activities
181 181 else
182 182 return active_activities
183 183 end
184 184 end
185 185
186 186 # Will create a new Project specific Activity or update an existing one
187 187 #
188 188 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
189 189 # does not successfully save.
190 190 def update_or_create_time_entry_activity(id, activity_hash)
191 191 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
192 192 self.create_time_entry_activity_if_needed(activity_hash)
193 193 else
194 194 activity = project.time_entry_activities.find_by_id(id.to_i)
195 195 activity.update_attributes(activity_hash) if activity
196 196 end
197 197 end
198 198
199 199 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
200 200 #
201 201 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
202 202 # does not successfully save.
203 203 def create_time_entry_activity_if_needed(activity)
204 204 if activity['parent_id']
205 205
206 206 parent_activity = TimeEntryActivity.find(activity['parent_id'])
207 207 activity['name'] = parent_activity.name
208 208 activity['position'] = parent_activity.position
209 209
210 210 if Enumeration.overridding_change?(activity, parent_activity)
211 211 project_activity = self.time_entry_activities.create(activity)
212 212
213 213 if project_activity.new_record?
214 214 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
215 215 else
216 216 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
217 217 end
218 218 end
219 219 end
220 220 end
221 221
222 222 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
223 223 #
224 224 # Examples:
225 225 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
226 226 # project.project_condition(false) => "projects.id = 1"
227 227 def project_condition(with_subprojects)
228 228 cond = "#{Project.table_name}.id = #{id}"
229 229 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
230 230 cond
231 231 end
232 232
233 233 def self.find(*args)
234 234 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
235 235 project = find_by_identifier(*args)
236 236 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
237 237 project
238 238 else
239 239 super
240 240 end
241 241 end
242 242
243 243 def to_param
244 244 # id is used for projects with a numeric identifier (compatibility)
245 245 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
246 246 end
247 247
248 248 def active?
249 249 self.status == STATUS_ACTIVE
250 250 end
251 251
252 252 def archived?
253 253 self.status == STATUS_ARCHIVED
254 254 end
255 255
256 256 # Archives the project and its descendants
257 257 def archive
258 258 # Check that there is no issue of a non descendant project that is assigned
259 259 # to one of the project or descendant versions
260 260 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
261 261 if v_ids.any? && Issue.find(:first, :include => :project,
262 262 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
263 263 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
264 264 return false
265 265 end
266 266 Project.transaction do
267 267 archive!
268 268 end
269 269 true
270 270 end
271 271
272 272 # Unarchives the project
273 273 # All its ancestors must be active
274 274 def unarchive
275 275 return false if ancestors.detect {|a| !a.active?}
276 276 update_attribute :status, STATUS_ACTIVE
277 277 end
278 278
279 279 # Returns an array of projects the project can be moved to
280 280 # by the current user
281 281 def allowed_parents
282 282 return @allowed_parents if @allowed_parents
283 283 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
284 284 @allowed_parents = @allowed_parents - self_and_descendants
285 285 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
286 286 @allowed_parents << nil
287 287 end
288 288 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
289 289 @allowed_parents << parent
290 290 end
291 291 @allowed_parents
292 292 end
293 293
294 294 # Sets the parent of the project with authorization check
295 295 def set_allowed_parent!(p)
296 296 unless p.nil? || p.is_a?(Project)
297 297 if p.to_s.blank?
298 298 p = nil
299 299 else
300 300 p = Project.find_by_id(p)
301 301 return false unless p
302 302 end
303 303 end
304 304 if p.nil?
305 305 if !new_record? && allowed_parents.empty?
306 306 return false
307 307 end
308 308 elsif !allowed_parents.include?(p)
309 309 return false
310 310 end
311 311 set_parent!(p)
312 312 end
313 313
314 314 # Sets the parent of the project
315 315 # Argument can be either a Project, a String, a Fixnum or nil
316 316 def set_parent!(p)
317 317 unless p.nil? || p.is_a?(Project)
318 318 if p.to_s.blank?
319 319 p = nil
320 320 else
321 321 p = Project.find_by_id(p)
322 322 return false unless p
323 323 end
324 324 end
325 325 if p == parent && !p.nil?
326 326 # Nothing to do
327 327 true
328 328 elsif p.nil? || (p.active? && move_possible?(p))
329 329 # Insert the project so that target's children or root projects stay alphabetically sorted
330 330 sibs = (p.nil? ? self.class.roots : p.children)
331 331 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
332 332 if to_be_inserted_before
333 333 move_to_left_of(to_be_inserted_before)
334 334 elsif p.nil?
335 335 if sibs.empty?
336 336 # move_to_root adds the project in first (ie. left) position
337 337 move_to_root
338 338 else
339 339 move_to_right_of(sibs.last) unless self == sibs.last
340 340 end
341 341 else
342 342 # move_to_child_of adds the project in last (ie.right) position
343 343 move_to_child_of(p)
344 344 end
345 345 Issue.update_versions_from_hierarchy_change(self)
346 346 true
347 347 else
348 348 # Can not move to the given target
349 349 false
350 350 end
351 351 end
352 352
353 353 # Returns an array of the trackers used by the project and its active sub projects
354 354 def rolled_up_trackers
355 355 @rolled_up_trackers ||=
356 356 Tracker.find(:all, :include => :projects,
357 357 :select => "DISTINCT #{Tracker.table_name}.*",
358 358 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
359 359 :order => "#{Tracker.table_name}.position")
360 360 end
361 361
362 362 # Closes open and locked project versions that are completed
363 363 def close_completed_versions
364 364 Version.transaction do
365 365 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
366 366 if version.completed?
367 367 version.update_attribute(:status, 'closed')
368 368 end
369 369 end
370 370 end
371 371 end
372 372
373 373 # Returns a scope of the Versions on subprojects
374 374 def rolled_up_versions
375 375 @rolled_up_versions ||=
376 376 Version.scoped(:include => :project,
377 377 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
378 378 end
379 379
380 380 # Returns a scope of the Versions used by the project
381 381 def shared_versions
382 382 @shared_versions ||=
383 383 Version.scoped(:include => :project,
384 384 :conditions => "#{Project.table_name}.id = #{id}" +
385 385 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
386 386 " #{Version.table_name}.sharing = 'system'" +
387 387 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
388 388 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
389 389 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
390 390 "))")
391 391 end
392 392
393 393 # Returns a hash of project users grouped by role
394 394 def users_by_role
395 395 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
396 396 m.roles.each do |r|
397 397 h[r] ||= []
398 398 h[r] << m.user
399 399 end
400 400 h
401 401 end
402 402 end
403 403
404 404 # Deletes all project's members
405 405 def delete_all_members
406 406 me, mr = Member.table_name, MemberRole.table_name
407 407 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
408 408 Member.delete_all(['project_id = ?', id])
409 409 end
410 410
411 411 # Users issues can be assigned to
412 412 def assignable_users
413 413 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
414 414 end
415 415
416 416 # Returns the mail adresses of users that should be always notified on project events
417 417 def recipients
418 418 notified_users.collect {|user| user.mail}
419 419 end
420 420
421 421 # Returns the users that should be notified on project events
422 422 def notified_users
423 423 # TODO: User part should be extracted to User#notify_about?
424 424 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
425 425 end
426 426
427 427 # Returns an array of all custom fields enabled for project issues
428 428 # (explictly associated custom fields and custom fields enabled for all projects)
429 429 def all_issue_custom_fields
430 430 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
431 431 end
432 432
433 433 def project
434 434 self
435 435 end
436 436
437 437 def <=>(project)
438 438 name.downcase <=> project.name.downcase
439 439 end
440 440
441 441 def to_s
442 442 name
443 443 end
444 444
445 445 # Returns a short description of the projects (first lines)
446 446 def short_description(length = 255)
447 447 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
448 448 end
449 449
450 450 def css_classes
451 451 s = 'project'
452 452 s << ' root' if root?
453 453 s << ' child' if child?
454 454 s << (leaf? ? ' leaf' : ' parent')
455 455 s
456 456 end
457 457
458 458 # The earliest start date of a project, based on it's issues and versions
459 459 def start_date
460 460 [
461 461 issues.minimum('start_date'),
462 462 shared_versions.collect(&:effective_date),
463 463 shared_versions.collect(&:start_date)
464 464 ].flatten.compact.min
465 465 end
466 466
467 467 # The latest due date of an issue or version
468 468 def due_date
469 469 [
470 470 issues.maximum('due_date'),
471 471 shared_versions.collect(&:effective_date),
472 472 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
473 473 ].flatten.compact.max
474 474 end
475 475
476 476 def overdue?
477 477 active? && !due_date.nil? && (due_date < Date.today)
478 478 end
479 479
480 480 # Returns the percent completed for this project, based on the
481 481 # progress on it's versions.
482 482 def completed_percent(options={:include_subprojects => false})
483 483 if options.delete(:include_subprojects)
484 484 total = self_and_descendants.collect(&:completed_percent).sum
485 485
486 486 total / self_and_descendants.count
487 487 else
488 488 if versions.count > 0
489 489 total = versions.collect(&:completed_pourcent).sum
490 490
491 491 total / versions.count
492 492 else
493 493 100
494 494 end
495 495 end
496 496 end
497 497
498 498 # Return true if this project is allowed to do the specified action.
499 499 # action can be:
500 500 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
501 501 # * a permission Symbol (eg. :edit_project)
502 502 def allows_to?(action)
503 503 if action.is_a? Hash
504 504 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
505 505 else
506 506 allowed_permissions.include? action
507 507 end
508 508 end
509 509
510 510 def module_enabled?(module_name)
511 511 module_name = module_name.to_s
512 512 enabled_modules.detect {|m| m.name == module_name}
513 513 end
514 514
515 515 def enabled_module_names=(module_names)
516 516 if module_names && module_names.is_a?(Array)
517 517 module_names = module_names.collect(&:to_s).reject(&:blank?)
518 518 # remove disabled modules
519 519 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
520 520 # add new modules
521 521 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
522 522 else
523 523 enabled_modules.clear
524 524 end
525 525 end
526 526
527 527 # Returns an array of the enabled modules names
528 528 def enabled_module_names
529 529 enabled_modules.collect(&:name)
530 530 end
531 531
532 532 safe_attributes 'name',
533 533 'description',
534 534 'homepage',
535 535 'is_public',
536 536 'identifier',
537 537 'custom_field_values',
538 538 'custom_fields',
539 539 'tracker_ids',
540 540 'issue_custom_field_ids'
541 541
542 542 safe_attributes 'enabled_module_names',
543 543 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
544 544
545 545 # Returns an array of projects that are in this project's hierarchy
546 546 #
547 547 # Example: parents, children, siblings
548 548 def hierarchy
549 549 parents = project.self_and_ancestors || []
550 550 descendants = project.descendants || []
551 551 project_hierarchy = parents | descendants # Set union
552 552 end
553 553
554 554 # Returns an auto-generated project identifier based on the last identifier used
555 555 def self.next_identifier
556 556 p = Project.find(:first, :order => 'created_on DESC')
557 557 p.nil? ? nil : p.identifier.to_s.succ
558 558 end
559 559
560 560 # Copies and saves the Project instance based on the +project+.
561 561 # Duplicates the source project's:
562 562 # * Wiki
563 563 # * Versions
564 564 # * Categories
565 565 # * Issues
566 566 # * Members
567 567 # * Queries
568 568 #
569 569 # Accepts an +options+ argument to specify what to copy
570 570 #
571 571 # Examples:
572 572 # project.copy(1) # => copies everything
573 573 # project.copy(1, :only => 'members') # => copies members only
574 574 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
575 575 def copy(project, options={})
576 576 project = project.is_a?(Project) ? project : Project.find(project)
577 577
578 578 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
579 579 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
580 580
581 581 Project.transaction do
582 582 if save
583 583 reload
584 584 to_be_copied.each do |name|
585 585 send "copy_#{name}", project
586 586 end
587 587 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
588 588 save
589 589 end
590 590 end
591 591 end
592 592
593 593
594 594 # Copies +project+ and returns the new instance. This will not save
595 595 # the copy
596 596 def self.copy_from(project)
597 597 begin
598 598 project = project.is_a?(Project) ? project : Project.find(project)
599 599 if project
600 600 # clear unique attributes
601 601 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
602 602 copy = Project.new(attributes)
603 603 copy.enabled_modules = project.enabled_modules
604 604 copy.trackers = project.trackers
605 605 copy.custom_values = project.custom_values.collect {|v| v.clone}
606 606 copy.issue_custom_fields = project.issue_custom_fields
607 607 return copy
608 608 else
609 609 return nil
610 610 end
611 611 rescue ActiveRecord::RecordNotFound
612 612 return nil
613 613 end
614 614 end
615 615
616 616 # Yields the given block for each project with its level in the tree
617 617 def self.project_tree(projects, &block)
618 618 ancestors = []
619 619 projects.sort_by(&:lft).each do |project|
620 620 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
621 621 ancestors.pop
622 622 end
623 623 yield project, ancestors.size
624 624 ancestors << project
625 625 end
626 626 end
627 627
628 628 private
629 629
630 # Destroys children before destroying self
631 def destroy_children
632 children.each do |child|
633 child.destroy
634 end
635 end
636
637 630 # Copies wiki from +project+
638 631 def copy_wiki(project)
639 632 # Check that the source project has a wiki first
640 633 unless project.wiki.nil?
641 634 self.wiki ||= Wiki.new
642 635 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
643 636 wiki_pages_map = {}
644 637 project.wiki.pages.each do |page|
645 638 # Skip pages without content
646 639 next if page.content.nil?
647 640 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
648 641 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
649 642 new_wiki_page.content = new_wiki_content
650 643 wiki.pages << new_wiki_page
651 644 wiki_pages_map[page.id] = new_wiki_page
652 645 end
653 646 wiki.save
654 647 # Reproduce page hierarchy
655 648 project.wiki.pages.each do |page|
656 649 if page.parent_id && wiki_pages_map[page.id]
657 650 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
658 651 wiki_pages_map[page.id].save
659 652 end
660 653 end
661 654 end
662 655 end
663 656
664 657 # Copies versions from +project+
665 658 def copy_versions(project)
666 659 project.versions.each do |version|
667 660 new_version = Version.new
668 661 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
669 662 self.versions << new_version
670 663 end
671 664 end
672 665
673 666 # Copies issue categories from +project+
674 667 def copy_issue_categories(project)
675 668 project.issue_categories.each do |issue_category|
676 669 new_issue_category = IssueCategory.new
677 670 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
678 671 self.issue_categories << new_issue_category
679 672 end
680 673 end
681 674
682 675 # Copies issues from +project+
683 676 def copy_issues(project)
684 677 # Stores the source issue id as a key and the copied issues as the
685 678 # value. Used to map the two togeather for issue relations.
686 679 issues_map = {}
687 680
688 681 # Get issues sorted by root_id, lft so that parent issues
689 682 # get copied before their children
690 683 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
691 684 new_issue = Issue.new
692 685 new_issue.copy_from(issue)
693 686 new_issue.project = self
694 687 # Reassign fixed_versions by name, since names are unique per
695 688 # project and the versions for self are not yet saved
696 689 if issue.fixed_version
697 690 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
698 691 end
699 692 # Reassign the category by name, since names are unique per
700 693 # project and the categories for self are not yet saved
701 694 if issue.category
702 695 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
703 696 end
704 697 # Parent issue
705 698 if issue.parent_id
706 699 if copied_parent = issues_map[issue.parent_id]
707 700 new_issue.parent_issue_id = copied_parent.id
708 701 end
709 702 end
710 703
711 704 self.issues << new_issue
712 705 if new_issue.new_record?
713 706 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
714 707 else
715 708 issues_map[issue.id] = new_issue unless new_issue.new_record?
716 709 end
717 710 end
718 711
719 712 # Relations after in case issues related each other
720 713 project.issues.each do |issue|
721 714 new_issue = issues_map[issue.id]
722 715 unless new_issue
723 716 # Issue was not copied
724 717 next
725 718 end
726 719
727 720 # Relations
728 721 issue.relations_from.each do |source_relation|
729 722 new_issue_relation = IssueRelation.new
730 723 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
731 724 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
732 725 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
733 726 new_issue_relation.issue_to = source_relation.issue_to
734 727 end
735 728 new_issue.relations_from << new_issue_relation
736 729 end
737 730
738 731 issue.relations_to.each do |source_relation|
739 732 new_issue_relation = IssueRelation.new
740 733 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
741 734 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
742 735 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
743 736 new_issue_relation.issue_from = source_relation.issue_from
744 737 end
745 738 new_issue.relations_to << new_issue_relation
746 739 end
747 740 end
748 741 end
749 742
750 743 # Copies members from +project+
751 744 def copy_members(project)
752 745 # Copy users first, then groups to handle members with inherited and given roles
753 746 members_to_copy = []
754 747 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
755 748 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
756 749
757 750 members_to_copy.each do |member|
758 751 new_member = Member.new
759 752 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
760 753 # only copy non inherited roles
761 754 # inherited roles will be added when copying the group membership
762 755 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
763 756 next if role_ids.empty?
764 757 new_member.role_ids = role_ids
765 758 new_member.project = self
766 759 self.members << new_member
767 760 end
768 761 end
769 762
770 763 # Copies queries from +project+
771 764 def copy_queries(project)
772 765 project.queries.each do |query|
773 766 new_query = Query.new
774 767 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
775 768 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
776 769 new_query.project = self
777 770 self.queries << new_query
778 771 end
779 772 end
780 773
781 774 # Copies boards from +project+
782 775 def copy_boards(project)
783 776 project.boards.each do |board|
784 777 new_board = Board.new
785 778 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
786 779 new_board.project = self
787 780 self.boards << new_board
788 781 end
789 782 end
790 783
791 784 def allowed_permissions
792 785 @allowed_permissions ||= begin
793 786 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
794 787 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
795 788 end
796 789 end
797 790
798 791 def allowed_actions
799 792 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
800 793 end
801 794
802 795 # Returns all the active Systemwide and project specific activities
803 796 def active_activities
804 797 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
805 798
806 799 if overridden_activity_ids.empty?
807 800 return TimeEntryActivity.shared.active
808 801 else
809 802 return system_activities_and_project_overrides
810 803 end
811 804 end
812 805
813 806 # Returns all the Systemwide and project specific activities
814 807 # (inactive and active)
815 808 def all_activities
816 809 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
817 810
818 811 if overridden_activity_ids.empty?
819 812 return TimeEntryActivity.shared
820 813 else
821 814 return system_activities_and_project_overrides(true)
822 815 end
823 816 end
824 817
825 818 # Returns the systemwide active activities merged with the project specific overrides
826 819 def system_activities_and_project_overrides(include_inactive=false)
827 820 if include_inactive
828 821 return TimeEntryActivity.shared.
829 822 find(:all,
830 823 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
831 824 self.time_entry_activities
832 825 else
833 826 return TimeEntryActivity.shared.active.
834 827 find(:all,
835 828 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
836 829 self.time_entry_activities.active
837 830 end
838 831 end
839 832
840 833 # Archives subprojects recursively
841 834 def archive!
842 835 children.each do |subproject|
843 836 subproject.send :archive!
844 837 end
845 838 update_attribute :status, STATUS_ARCHIVED
846 839 end
847 840 end
@@ -1,1047 +1,1082
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class ProjectTest < ActiveSupport::TestCase
21 21 fixtures :all
22 22
23 23 def setup
24 24 @ecookbook = Project.find(1)
25 25 @ecookbook_sub1 = Project.find(3)
26 26 User.current = nil
27 27 end
28 28
29 29 should_validate_presence_of :name
30 30 should_validate_presence_of :identifier
31 31
32 32 should_validate_uniqueness_of :identifier
33 33
34 34 context "associations" do
35 35 should_have_many :members
36 36 should_have_many :users, :through => :members
37 37 should_have_many :member_principals
38 38 should_have_many :principals, :through => :member_principals
39 39 should_have_many :enabled_modules
40 40 should_have_many :issues
41 41 should_have_many :issue_changes, :through => :issues
42 42 should_have_many :versions
43 43 should_have_many :time_entries
44 44 should_have_many :queries
45 45 should_have_many :documents
46 46 should_have_many :news
47 47 should_have_many :issue_categories
48 48 should_have_many :boards
49 49 should_have_many :changesets, :through => :repository
50 50
51 51 should_have_one :repository
52 52 should_have_one :wiki
53 53
54 54 should_have_and_belong_to_many :trackers
55 55 should_have_and_belong_to_many :issue_custom_fields
56 56 end
57 57
58 58 def test_truth
59 59 assert_kind_of Project, @ecookbook
60 60 assert_equal "eCookbook", @ecookbook.name
61 61 end
62 62
63 63 def test_default_attributes
64 64 with_settings :default_projects_public => '1' do
65 65 assert_equal true, Project.new.is_public
66 66 assert_equal false, Project.new(:is_public => false).is_public
67 67 end
68 68
69 69 with_settings :default_projects_public => '0' do
70 70 assert_equal false, Project.new.is_public
71 71 assert_equal true, Project.new(:is_public => true).is_public
72 72 end
73 73
74 74 with_settings :sequential_project_identifiers => '1' do
75 75 assert !Project.new.identifier.blank?
76 76 assert Project.new(:identifier => '').identifier.blank?
77 77 end
78 78
79 79 with_settings :sequential_project_identifiers => '0' do
80 80 assert Project.new.identifier.blank?
81 81 assert !Project.new(:identifier => 'test').blank?
82 82 end
83 83
84 84 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
85 85 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
86 86 end
87 87
88 88 assert_equal Tracker.all, Project.new.trackers
89 89 assert_equal Tracker.find(1, 3), Project.new(:tracker_ids => [1, 3]).trackers
90 90 end
91 91
92 92 def test_update
93 93 assert_equal "eCookbook", @ecookbook.name
94 94 @ecookbook.name = "eCook"
95 95 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
96 96 @ecookbook.reload
97 97 assert_equal "eCook", @ecookbook.name
98 98 end
99 99
100 100 def test_validate_identifier
101 101 to_test = {"abc" => true,
102 102 "ab12" => true,
103 103 "ab-12" => true,
104 104 "12" => false,
105 105 "new" => false}
106 106
107 107 to_test.each do |identifier, valid|
108 108 p = Project.new
109 109 p.identifier = identifier
110 110 p.valid?
111 111 assert_equal valid, p.errors.on('identifier').nil?
112 112 end
113 113 end
114 114
115 115 def test_members_should_be_active_users
116 116 Project.all.each do |project|
117 117 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
118 118 end
119 119 end
120 120
121 121 def test_users_should_be_active_users
122 122 Project.all.each do |project|
123 123 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
124 124 end
125 125 end
126 126
127 127 def test_archive
128 128 user = @ecookbook.members.first.user
129 129 @ecookbook.archive
130 130 @ecookbook.reload
131 131
132 132 assert !@ecookbook.active?
133 133 assert @ecookbook.archived?
134 134 assert !user.projects.include?(@ecookbook)
135 135 # Subproject are also archived
136 136 assert !@ecookbook.children.empty?
137 137 assert @ecookbook.descendants.active.empty?
138 138 end
139 139
140 140 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
141 141 # Assign an issue of a project to a version of a child project
142 142 Issue.find(4).update_attribute :fixed_version_id, 4
143 143
144 144 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
145 145 assert_equal false, @ecookbook.archive
146 146 end
147 147 @ecookbook.reload
148 148 assert @ecookbook.active?
149 149 end
150 150
151 151 def test_unarchive
152 152 user = @ecookbook.members.first.user
153 153 @ecookbook.archive
154 154 # A subproject of an archived project can not be unarchived
155 155 assert !@ecookbook_sub1.unarchive
156 156
157 157 # Unarchive project
158 158 assert @ecookbook.unarchive
159 159 @ecookbook.reload
160 160 assert @ecookbook.active?
161 161 assert !@ecookbook.archived?
162 162 assert user.projects.include?(@ecookbook)
163 163 # Subproject can now be unarchived
164 164 @ecookbook_sub1.reload
165 165 assert @ecookbook_sub1.unarchive
166 166 end
167 167
168 168 def test_destroy
169 169 # 2 active members
170 170 assert_equal 2, @ecookbook.members.size
171 171 # and 1 is locked
172 172 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
173 173 # some boards
174 174 assert @ecookbook.boards.any?
175 175
176 176 @ecookbook.destroy
177 177 # make sure that the project non longer exists
178 178 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
179 179 # make sure related data was removed
180 180 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
181 181 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
182 182 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
183 183 end
184 184
185 def test_destroying_root_projects_should_clear_data
186 Project.roots.each do |root|
187 root.destroy
188 end
189
190 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
191 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
192 assert_equal 0, MemberRole.count
193 assert_equal 0, Issue.count
194 assert_equal 0, Journal.count
195 assert_equal 0, JournalDetail.count
196 assert_equal 0, Attachment.count
197 assert_equal 0, EnabledModule.count
198 assert_equal 0, IssueCategory.count
199 assert_equal 0, IssueRelation.count
200 assert_equal 0, Board.count
201 assert_equal 0, Message.count
202 assert_equal 0, News.count
203 assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL")
204 assert_equal 0, Repository.count
205 assert_equal 0, Changeset.count
206 assert_equal 0, Change.count
207 #assert_equal 0, Comment.count
208 assert_equal 0, TimeEntry.count
209 assert_equal 0, Version.count
210 assert_equal 0, Watcher.count
211 assert_equal 0, Wiki.count
212 assert_equal 0, WikiPage.count
213 assert_equal 0, WikiContent.count
214 assert_equal 0, WikiContent::Version.count
215 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
216 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
217 assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
218 end
219
185 220 def test_move_an_orphan_project_to_a_root_project
186 221 sub = Project.find(2)
187 222 sub.set_parent! @ecookbook
188 223 assert_equal @ecookbook.id, sub.parent.id
189 224 @ecookbook.reload
190 225 assert_equal 4, @ecookbook.children.size
191 226 end
192 227
193 228 def test_move_an_orphan_project_to_a_subproject
194 229 sub = Project.find(2)
195 230 assert sub.set_parent!(@ecookbook_sub1)
196 231 end
197 232
198 233 def test_move_a_root_project_to_a_project
199 234 sub = @ecookbook
200 235 assert sub.set_parent!(Project.find(2))
201 236 end
202 237
203 238 def test_should_not_move_a_project_to_its_children
204 239 sub = @ecookbook
205 240 assert !(sub.set_parent!(Project.find(3)))
206 241 end
207 242
208 243 def test_set_parent_should_add_roots_in_alphabetical_order
209 244 ProjectCustomField.delete_all
210 245 Project.delete_all
211 246 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
212 247 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
213 248 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
214 249 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
215 250
216 251 assert_equal 4, Project.count
217 252 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
218 253 end
219 254
220 255 def test_set_parent_should_add_children_in_alphabetical_order
221 256 ProjectCustomField.delete_all
222 257 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
223 258 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
224 259 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
225 260 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
226 261 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
227 262
228 263 parent.reload
229 264 assert_equal 4, parent.children.size
230 265 assert_equal parent.children.sort_by(&:name), parent.children
231 266 end
232 267
233 268 def test_rebuild_should_sort_children_alphabetically
234 269 ProjectCustomField.delete_all
235 270 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
236 271 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
237 272 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
238 273 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
239 274 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
240 275
241 276 Project.update_all("lft = NULL, rgt = NULL")
242 277 Project.rebuild!
243 278
244 279 parent.reload
245 280 assert_equal 4, parent.children.size
246 281 assert_equal parent.children.sort_by(&:name), parent.children
247 282 end
248 283
249 284
250 285 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
251 286 # Parent issue with a hierarchy project's fixed version
252 287 parent_issue = Issue.find(1)
253 288 parent_issue.update_attribute(:fixed_version_id, 4)
254 289 parent_issue.reload
255 290 assert_equal 4, parent_issue.fixed_version_id
256 291
257 292 # Should keep fixed versions for the issues
258 293 issue_with_local_fixed_version = Issue.find(5)
259 294 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
260 295 issue_with_local_fixed_version.reload
261 296 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
262 297
263 298 # Local issue with hierarchy fixed_version
264 299 issue_with_hierarchy_fixed_version = Issue.find(13)
265 300 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
266 301 issue_with_hierarchy_fixed_version.reload
267 302 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
268 303
269 304 # Move project out of the issue's hierarchy
270 305 moved_project = Project.find(3)
271 306 moved_project.set_parent!(Project.find(2))
272 307 parent_issue.reload
273 308 issue_with_local_fixed_version.reload
274 309 issue_with_hierarchy_fixed_version.reload
275 310
276 311 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
277 312 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
278 313 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
279 314 end
280 315
281 316 def test_parent
282 317 p = Project.find(6).parent
283 318 assert p.is_a?(Project)
284 319 assert_equal 5, p.id
285 320 end
286 321
287 322 def test_ancestors
288 323 a = Project.find(6).ancestors
289 324 assert a.first.is_a?(Project)
290 325 assert_equal [1, 5], a.collect(&:id)
291 326 end
292 327
293 328 def test_root
294 329 r = Project.find(6).root
295 330 assert r.is_a?(Project)
296 331 assert_equal 1, r.id
297 332 end
298 333
299 334 def test_children
300 335 c = Project.find(1).children
301 336 assert c.first.is_a?(Project)
302 337 assert_equal [5, 3, 4], c.collect(&:id)
303 338 end
304 339
305 340 def test_descendants
306 341 d = Project.find(1).descendants
307 342 assert d.first.is_a?(Project)
308 343 assert_equal [5, 6, 3, 4], d.collect(&:id)
309 344 end
310 345
311 346 def test_allowed_parents_should_be_empty_for_non_member_user
312 347 Role.non_member.add_permission!(:add_project)
313 348 user = User.find(9)
314 349 assert user.memberships.empty?
315 350 User.current = user
316 351 assert Project.new.allowed_parents.compact.empty?
317 352 end
318 353
319 354 def test_allowed_parents_with_add_subprojects_permission
320 355 Role.find(1).remove_permission!(:add_project)
321 356 Role.find(1).add_permission!(:add_subprojects)
322 357 User.current = User.find(2)
323 358 # new project
324 359 assert !Project.new.allowed_parents.include?(nil)
325 360 assert Project.new.allowed_parents.include?(Project.find(1))
326 361 # existing root project
327 362 assert Project.find(1).allowed_parents.include?(nil)
328 363 # existing child
329 364 assert Project.find(3).allowed_parents.include?(Project.find(1))
330 365 assert !Project.find(3).allowed_parents.include?(nil)
331 366 end
332 367
333 368 def test_allowed_parents_with_add_project_permission
334 369 Role.find(1).add_permission!(:add_project)
335 370 Role.find(1).remove_permission!(:add_subprojects)
336 371 User.current = User.find(2)
337 372 # new project
338 373 assert Project.new.allowed_parents.include?(nil)
339 374 assert !Project.new.allowed_parents.include?(Project.find(1))
340 375 # existing root project
341 376 assert Project.find(1).allowed_parents.include?(nil)
342 377 # existing child
343 378 assert Project.find(3).allowed_parents.include?(Project.find(1))
344 379 assert Project.find(3).allowed_parents.include?(nil)
345 380 end
346 381
347 382 def test_allowed_parents_with_add_project_and_subprojects_permission
348 383 Role.find(1).add_permission!(:add_project)
349 384 Role.find(1).add_permission!(:add_subprojects)
350 385 User.current = User.find(2)
351 386 # new project
352 387 assert Project.new.allowed_parents.include?(nil)
353 388 assert Project.new.allowed_parents.include?(Project.find(1))
354 389 # existing root project
355 390 assert Project.find(1).allowed_parents.include?(nil)
356 391 # existing child
357 392 assert Project.find(3).allowed_parents.include?(Project.find(1))
358 393 assert Project.find(3).allowed_parents.include?(nil)
359 394 end
360 395
361 396 def test_users_by_role
362 397 users_by_role = Project.find(1).users_by_role
363 398 assert_kind_of Hash, users_by_role
364 399 role = Role.find(1)
365 400 assert_kind_of Array, users_by_role[role]
366 401 assert users_by_role[role].include?(User.find(2))
367 402 end
368 403
369 404 def test_rolled_up_trackers
370 405 parent = Project.find(1)
371 406 parent.trackers = Tracker.find([1,2])
372 407 child = parent.children.find(3)
373 408
374 409 assert_equal [1, 2], parent.tracker_ids
375 410 assert_equal [2, 3], child.trackers.collect(&:id)
376 411
377 412 assert_kind_of Tracker, parent.rolled_up_trackers.first
378 413 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
379 414
380 415 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
381 416 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
382 417 end
383 418
384 419 def test_rolled_up_trackers_should_ignore_archived_subprojects
385 420 parent = Project.find(1)
386 421 parent.trackers = Tracker.find([1,2])
387 422 child = parent.children.find(3)
388 423 child.trackers = Tracker.find([1,3])
389 424 parent.children.each(&:archive)
390 425
391 426 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
392 427 end
393 428
394 429 context "#rolled_up_versions" do
395 430 setup do
396 431 @project = Project.generate!
397 432 @parent_version_1 = Version.generate!(:project => @project)
398 433 @parent_version_2 = Version.generate!(:project => @project)
399 434 end
400 435
401 436 should "include the versions for the current project" do
402 437 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
403 438 end
404 439
405 440 should "include versions for a subproject" do
406 441 @subproject = Project.generate!
407 442 @subproject.set_parent!(@project)
408 443 @subproject_version = Version.generate!(:project => @subproject)
409 444
410 445 assert_same_elements [
411 446 @parent_version_1,
412 447 @parent_version_2,
413 448 @subproject_version
414 449 ], @project.rolled_up_versions
415 450 end
416 451
417 452 should "include versions for a sub-subproject" do
418 453 @subproject = Project.generate!
419 454 @subproject.set_parent!(@project)
420 455 @sub_subproject = Project.generate!
421 456 @sub_subproject.set_parent!(@subproject)
422 457 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
423 458
424 459 @project.reload
425 460
426 461 assert_same_elements [
427 462 @parent_version_1,
428 463 @parent_version_2,
429 464 @sub_subproject_version
430 465 ], @project.rolled_up_versions
431 466 end
432 467
433 468
434 469 should "only check active projects" do
435 470 @subproject = Project.generate!
436 471 @subproject.set_parent!(@project)
437 472 @subproject_version = Version.generate!(:project => @subproject)
438 473 assert @subproject.archive
439 474
440 475 @project.reload
441 476
442 477 assert !@subproject.active?
443 478 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
444 479 end
445 480 end
446 481
447 482 def test_shared_versions_none_sharing
448 483 p = Project.find(5)
449 484 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
450 485 assert p.shared_versions.include?(v)
451 486 assert !p.children.first.shared_versions.include?(v)
452 487 assert !p.root.shared_versions.include?(v)
453 488 assert !p.siblings.first.shared_versions.include?(v)
454 489 assert !p.root.siblings.first.shared_versions.include?(v)
455 490 end
456 491
457 492 def test_shared_versions_descendants_sharing
458 493 p = Project.find(5)
459 494 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
460 495 assert p.shared_versions.include?(v)
461 496 assert p.children.first.shared_versions.include?(v)
462 497 assert !p.root.shared_versions.include?(v)
463 498 assert !p.siblings.first.shared_versions.include?(v)
464 499 assert !p.root.siblings.first.shared_versions.include?(v)
465 500 end
466 501
467 502 def test_shared_versions_hierarchy_sharing
468 503 p = Project.find(5)
469 504 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
470 505 assert p.shared_versions.include?(v)
471 506 assert p.children.first.shared_versions.include?(v)
472 507 assert p.root.shared_versions.include?(v)
473 508 assert !p.siblings.first.shared_versions.include?(v)
474 509 assert !p.root.siblings.first.shared_versions.include?(v)
475 510 end
476 511
477 512 def test_shared_versions_tree_sharing
478 513 p = Project.find(5)
479 514 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
480 515 assert p.shared_versions.include?(v)
481 516 assert p.children.first.shared_versions.include?(v)
482 517 assert p.root.shared_versions.include?(v)
483 518 assert p.siblings.first.shared_versions.include?(v)
484 519 assert !p.root.siblings.first.shared_versions.include?(v)
485 520 end
486 521
487 522 def test_shared_versions_system_sharing
488 523 p = Project.find(5)
489 524 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
490 525 assert p.shared_versions.include?(v)
491 526 assert p.children.first.shared_versions.include?(v)
492 527 assert p.root.shared_versions.include?(v)
493 528 assert p.siblings.first.shared_versions.include?(v)
494 529 assert p.root.siblings.first.shared_versions.include?(v)
495 530 end
496 531
497 532 def test_shared_versions
498 533 parent = Project.find(1)
499 534 child = parent.children.find(3)
500 535 private_child = parent.children.find(5)
501 536
502 537 assert_equal [1,2,3], parent.version_ids.sort
503 538 assert_equal [4], child.version_ids
504 539 assert_equal [6], private_child.version_ids
505 540 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
506 541
507 542 assert_equal 6, parent.shared_versions.size
508 543 parent.shared_versions.each do |version|
509 544 assert_kind_of Version, version
510 545 end
511 546
512 547 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
513 548 end
514 549
515 550 def test_shared_versions_should_ignore_archived_subprojects
516 551 parent = Project.find(1)
517 552 child = parent.children.find(3)
518 553 child.archive
519 554 parent.reload
520 555
521 556 assert_equal [1,2,3], parent.version_ids.sort
522 557 assert_equal [4], child.version_ids
523 558 assert !parent.shared_versions.collect(&:id).include?(4)
524 559 end
525 560
526 561 def test_shared_versions_visible_to_user
527 562 user = User.find(3)
528 563 parent = Project.find(1)
529 564 child = parent.children.find(5)
530 565
531 566 assert_equal [1,2,3], parent.version_ids.sort
532 567 assert_equal [6], child.version_ids
533 568
534 569 versions = parent.shared_versions.visible(user)
535 570
536 571 assert_equal 4, versions.size
537 572 versions.each do |version|
538 573 assert_kind_of Version, version
539 574 end
540 575
541 576 assert !versions.collect(&:id).include?(6)
542 577 end
543 578
544 579
545 580 def test_next_identifier
546 581 ProjectCustomField.delete_all
547 582 Project.create!(:name => 'last', :identifier => 'p2008040')
548 583 assert_equal 'p2008041', Project.next_identifier
549 584 end
550 585
551 586 def test_next_identifier_first_project
552 587 Project.delete_all
553 588 assert_nil Project.next_identifier
554 589 end
555 590
556 591
557 592 def test_enabled_module_names_should_not_recreate_enabled_modules
558 593 project = Project.find(1)
559 594 # Remove one module
560 595 modules = project.enabled_modules.slice(0..-2)
561 596 assert modules.any?
562 597 assert_difference 'EnabledModule.count', -1 do
563 598 project.enabled_module_names = modules.collect(&:name)
564 599 end
565 600 project.reload
566 601 # Ids should be preserved
567 602 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
568 603 end
569 604
570 605 def test_copy_from_existing_project
571 606 source_project = Project.find(1)
572 607 copied_project = Project.copy_from(1)
573 608
574 609 assert copied_project
575 610 # Cleared attributes
576 611 assert copied_project.id.blank?
577 612 assert copied_project.name.blank?
578 613 assert copied_project.identifier.blank?
579 614
580 615 # Duplicated attributes
581 616 assert_equal source_project.description, copied_project.description
582 617 assert_equal source_project.enabled_modules, copied_project.enabled_modules
583 618 assert_equal source_project.trackers, copied_project.trackers
584 619
585 620 # Default attributes
586 621 assert_equal 1, copied_project.status
587 622 end
588 623
589 624 def test_activities_should_use_the_system_activities
590 625 project = Project.find(1)
591 626 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
592 627 end
593 628
594 629
595 630 def test_activities_should_use_the_project_specific_activities
596 631 project = Project.find(1)
597 632 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
598 633 assert overridden_activity.save!
599 634
600 635 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
601 636 end
602 637
603 638 def test_activities_should_not_include_the_inactive_project_specific_activities
604 639 project = Project.find(1)
605 640 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
606 641 assert overridden_activity.save!
607 642
608 643 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
609 644 end
610 645
611 646 def test_activities_should_not_include_project_specific_activities_from_other_projects
612 647 project = Project.find(1)
613 648 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
614 649 assert overridden_activity.save!
615 650
616 651 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
617 652 end
618 653
619 654 def test_activities_should_handle_nils
620 655 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
621 656 TimeEntryActivity.delete_all
622 657
623 658 # No activities
624 659 project = Project.find(1)
625 660 assert project.activities.empty?
626 661
627 662 # No system, one overridden
628 663 assert overridden_activity.save!
629 664 project.reload
630 665 assert_equal [overridden_activity], project.activities
631 666 end
632 667
633 668 def test_activities_should_override_system_activities_with_project_activities
634 669 project = Project.find(1)
635 670 parent_activity = TimeEntryActivity.find(:first)
636 671 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
637 672 assert overridden_activity.save!
638 673
639 674 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
640 675 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
641 676 end
642 677
643 678 def test_activities_should_include_inactive_activities_if_specified
644 679 project = Project.find(1)
645 680 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
646 681 assert overridden_activity.save!
647 682
648 683 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
649 684 end
650 685
651 686 test 'activities should not include active System activities if the project has an override that is inactive' do
652 687 project = Project.find(1)
653 688 system_activity = TimeEntryActivity.find_by_name('Design')
654 689 assert system_activity.active?
655 690 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
656 691 assert overridden_activity.save!
657 692
658 693 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
659 694 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
660 695 end
661 696
662 697 def test_close_completed_versions
663 698 Version.update_all("status = 'open'")
664 699 project = Project.find(1)
665 700 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
666 701 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
667 702 project.close_completed_versions
668 703 project.reload
669 704 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
670 705 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
671 706 end
672 707
673 708 context "Project#copy" do
674 709 setup do
675 710 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
676 711 Project.destroy_all :identifier => "copy-test"
677 712 @source_project = Project.find(2)
678 713 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
679 714 @project.trackers = @source_project.trackers
680 715 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
681 716 end
682 717
683 718 should "copy issues" do
684 719 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
685 720 :subject => "copy issue status",
686 721 :tracker_id => 1,
687 722 :assigned_to_id => 2,
688 723 :project_id => @source_project.id)
689 724 assert @project.valid?
690 725 assert @project.issues.empty?
691 726 assert @project.copy(@source_project)
692 727
693 728 assert_equal @source_project.issues.size, @project.issues.size
694 729 @project.issues.each do |issue|
695 730 assert issue.valid?
696 731 assert ! issue.assigned_to.blank?
697 732 assert_equal @project, issue.project
698 733 end
699 734
700 735 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
701 736 assert copied_issue
702 737 assert copied_issue.status
703 738 assert_equal "Closed", copied_issue.status.name
704 739 end
705 740
706 741 should "change the new issues to use the copied version" do
707 742 User.current = User.find(1)
708 743 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
709 744 @source_project.versions << assigned_version
710 745 assert_equal 3, @source_project.versions.size
711 746 Issue.generate_for_project!(@source_project,
712 747 :fixed_version_id => assigned_version.id,
713 748 :subject => "change the new issues to use the copied version",
714 749 :tracker_id => 1,
715 750 :project_id => @source_project.id)
716 751
717 752 assert @project.copy(@source_project)
718 753 @project.reload
719 754 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
720 755
721 756 assert copied_issue
722 757 assert copied_issue.fixed_version
723 758 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
724 759 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
725 760 end
726 761
727 762 should "copy issue relations" do
728 763 Setting.cross_project_issue_relations = '1'
729 764
730 765 second_issue = Issue.generate!(:status_id => 5,
731 766 :subject => "copy issue relation",
732 767 :tracker_id => 1,
733 768 :assigned_to_id => 2,
734 769 :project_id => @source_project.id)
735 770 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
736 771 :issue_to => second_issue,
737 772 :relation_type => "relates")
738 773 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
739 774 :issue_to => second_issue,
740 775 :relation_type => "duplicates")
741 776
742 777 assert @project.copy(@source_project)
743 778 assert_equal @source_project.issues.count, @project.issues.count
744 779 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
745 780 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
746 781
747 782 # First issue with a relation on project
748 783 assert_equal 1, copied_issue.relations.size, "Relation not copied"
749 784 copied_relation = copied_issue.relations.first
750 785 assert_equal "relates", copied_relation.relation_type
751 786 assert_equal copied_second_issue.id, copied_relation.issue_to_id
752 787 assert_not_equal source_relation.id, copied_relation.id
753 788
754 789 # Second issue with a cross project relation
755 790 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
756 791 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
757 792 assert_equal "duplicates", copied_relation.relation_type
758 793 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
759 794 assert_not_equal source_relation_cross_project.id, copied_relation.id
760 795 end
761 796
762 797 should "copy memberships" do
763 798 assert @project.valid?
764 799 assert @project.members.empty?
765 800 assert @project.copy(@source_project)
766 801
767 802 assert_equal @source_project.memberships.size, @project.memberships.size
768 803 @project.memberships.each do |membership|
769 804 assert membership
770 805 assert_equal @project, membership.project
771 806 end
772 807 end
773 808
774 809 should "copy memberships with groups and additional roles" do
775 810 group = Group.create!(:lastname => "Copy group")
776 811 user = User.find(7)
777 812 group.users << user
778 813 # group role
779 814 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
780 815 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
781 816 # additional role
782 817 member.role_ids = [1]
783 818
784 819 assert @project.copy(@source_project)
785 820 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
786 821 assert_not_nil member
787 822 assert_equal [1, 2], member.role_ids.sort
788 823 end
789 824
790 825 should "copy project specific queries" do
791 826 assert @project.valid?
792 827 assert @project.queries.empty?
793 828 assert @project.copy(@source_project)
794 829
795 830 assert_equal @source_project.queries.size, @project.queries.size
796 831 @project.queries.each do |query|
797 832 assert query
798 833 assert_equal @project, query.project
799 834 end
800 835 end
801 836
802 837 should "copy versions" do
803 838 @source_project.versions << Version.generate!
804 839 @source_project.versions << Version.generate!
805 840
806 841 assert @project.versions.empty?
807 842 assert @project.copy(@source_project)
808 843
809 844 assert_equal @source_project.versions.size, @project.versions.size
810 845 @project.versions.each do |version|
811 846 assert version
812 847 assert_equal @project, version.project
813 848 end
814 849 end
815 850
816 851 should "copy wiki" do
817 852 assert_difference 'Wiki.count' do
818 853 assert @project.copy(@source_project)
819 854 end
820 855
821 856 assert @project.wiki
822 857 assert_not_equal @source_project.wiki, @project.wiki
823 858 assert_equal "Start page", @project.wiki.start_page
824 859 end
825 860
826 861 should "copy wiki pages and content with hierarchy" do
827 862 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
828 863 assert @project.copy(@source_project)
829 864 end
830 865
831 866 assert @project.wiki
832 867 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
833 868
834 869 @project.wiki.pages.each do |wiki_page|
835 870 assert wiki_page.content
836 871 assert !@source_project.wiki.pages.include?(wiki_page)
837 872 end
838 873
839 874 parent = @project.wiki.find_page('Parent_page')
840 875 child1 = @project.wiki.find_page('Child_page_1')
841 876 child2 = @project.wiki.find_page('Child_page_2')
842 877 assert_equal parent, child1.parent
843 878 assert_equal parent, child2.parent
844 879 end
845 880
846 881 should "copy issue categories" do
847 882 assert @project.copy(@source_project)
848 883
849 884 assert_equal 2, @project.issue_categories.size
850 885 @project.issue_categories.each do |issue_category|
851 886 assert !@source_project.issue_categories.include?(issue_category)
852 887 end
853 888 end
854 889
855 890 should "copy boards" do
856 891 assert @project.copy(@source_project)
857 892
858 893 assert_equal 1, @project.boards.size
859 894 @project.boards.each do |board|
860 895 assert !@source_project.boards.include?(board)
861 896 end
862 897 end
863 898
864 899 should "change the new issues to use the copied issue categories" do
865 900 issue = Issue.find(4)
866 901 issue.update_attribute(:category_id, 3)
867 902
868 903 assert @project.copy(@source_project)
869 904
870 905 @project.issues.each do |issue|
871 906 assert issue.category
872 907 assert_equal "Stock management", issue.category.name # Same name
873 908 assert_not_equal IssueCategory.find(3), issue.category # Different record
874 909 end
875 910 end
876 911
877 912 should "limit copy with :only option" do
878 913 assert @project.members.empty?
879 914 assert @project.issue_categories.empty?
880 915 assert @source_project.issues.any?
881 916
882 917 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
883 918
884 919 assert @project.members.any?
885 920 assert @project.issue_categories.any?
886 921 assert @project.issues.empty?
887 922 end
888 923
889 924 end
890 925
891 926 context "#start_date" do
892 927 setup do
893 928 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
894 929 @project = Project.generate!(:identifier => 'test0')
895 930 @project.trackers << Tracker.generate!
896 931 end
897 932
898 933 should "be nil if there are no issues on the project" do
899 934 assert_nil @project.start_date
900 935 end
901 936
902 937 should "be tested when issues have no start date"
903 938
904 939 should "be the earliest start date of it's issues" do
905 940 early = 7.days.ago.to_date
906 941 Issue.generate_for_project!(@project, :start_date => Date.today)
907 942 Issue.generate_for_project!(@project, :start_date => early)
908 943
909 944 assert_equal early, @project.start_date
910 945 end
911 946
912 947 end
913 948
914 949 context "#due_date" do
915 950 setup do
916 951 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
917 952 @project = Project.generate!(:identifier => 'test0')
918 953 @project.trackers << Tracker.generate!
919 954 end
920 955
921 956 should "be nil if there are no issues on the project" do
922 957 assert_nil @project.due_date
923 958 end
924 959
925 960 should "be tested when issues have no due date"
926 961
927 962 should "be the latest due date of it's issues" do
928 963 future = 7.days.from_now.to_date
929 964 Issue.generate_for_project!(@project, :due_date => future)
930 965 Issue.generate_for_project!(@project, :due_date => Date.today)
931 966
932 967 assert_equal future, @project.due_date
933 968 end
934 969
935 970 should "be the latest due date of it's versions" do
936 971 future = 7.days.from_now.to_date
937 972 @project.versions << Version.generate!(:effective_date => future)
938 973 @project.versions << Version.generate!(:effective_date => Date.today)
939 974
940 975
941 976 assert_equal future, @project.due_date
942 977
943 978 end
944 979
945 980 should "pick the latest date from it's issues and versions" do
946 981 future = 7.days.from_now.to_date
947 982 far_future = 14.days.from_now.to_date
948 983 Issue.generate_for_project!(@project, :due_date => far_future)
949 984 @project.versions << Version.generate!(:effective_date => future)
950 985
951 986 assert_equal far_future, @project.due_date
952 987 end
953 988
954 989 end
955 990
956 991 context "Project#completed_percent" do
957 992 setup do
958 993 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
959 994 @project = Project.generate!(:identifier => 'test0')
960 995 @project.trackers << Tracker.generate!
961 996 end
962 997
963 998 context "no versions" do
964 999 should "be 100" do
965 1000 assert_equal 100, @project.completed_percent
966 1001 end
967 1002 end
968 1003
969 1004 context "with versions" do
970 1005 should "return 0 if the versions have no issues" do
971 1006 Version.generate!(:project => @project)
972 1007 Version.generate!(:project => @project)
973 1008
974 1009 assert_equal 0, @project.completed_percent
975 1010 end
976 1011
977 1012 should "return 100 if the version has only closed issues" do
978 1013 v1 = Version.generate!(:project => @project)
979 1014 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
980 1015 v2 = Version.generate!(:project => @project)
981 1016 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
982 1017
983 1018 assert_equal 100, @project.completed_percent
984 1019 end
985 1020
986 1021 should "return the averaged completed percent of the versions (not weighted)" do
987 1022 v1 = Version.generate!(:project => @project)
988 1023 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
989 1024 v2 = Version.generate!(:project => @project)
990 1025 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
991 1026
992 1027 assert_equal 50, @project.completed_percent
993 1028 end
994 1029
995 1030 end
996 1031 end
997 1032
998 1033 context "#notified_users" do
999 1034 setup do
1000 1035 @project = Project.generate!
1001 1036 @role = Role.generate!
1002 1037
1003 1038 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1004 1039 Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1005 1040
1006 1041 @all_events_user = User.generate!(:mail_notification => 'all')
1007 1042 Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user)
1008 1043
1009 1044 @no_events_user = User.generate!(:mail_notification => 'none')
1010 1045 Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user)
1011 1046
1012 1047 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1013 1048 Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1014 1049
1015 1050 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1016 1051 Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1017 1052
1018 1053 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1019 1054 Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1020 1055 end
1021 1056
1022 1057 should "include members with a mail notification" do
1023 1058 assert @project.notified_users.include?(@user_with_membership_notification)
1024 1059 end
1025 1060
1026 1061 should "include users with the 'all' notification option" do
1027 1062 assert @project.notified_users.include?(@all_events_user)
1028 1063 end
1029 1064
1030 1065 should "not include users with the 'none' notification option" do
1031 1066 assert !@project.notified_users.include?(@no_events_user)
1032 1067 end
1033 1068
1034 1069 should "not include users with the 'only_my_events' notification option" do
1035 1070 assert !@project.notified_users.include?(@only_my_events_user)
1036 1071 end
1037 1072
1038 1073 should "not include users with the 'only_assigned' notification option" do
1039 1074 assert !@project.notified_users.include?(@only_assigned_user)
1040 1075 end
1041 1076
1042 1077 should "not include users with the 'only_owner' notification option" do
1043 1078 assert !@project.notified_users.include?(@only_owned_user)
1044 1079 end
1045 1080 end
1046 1081
1047 1082 end
General Comments 0
You need to be logged in to leave comments. Login now