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