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