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