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