##// END OF EJS Templates
Fixed: Issues associated with a locked version are not copied when copying a project (#11207)....
Jean-Philippe Lang -
r10151:ff86c37ed330
parent child
Show More
@@ -1,954 +1,964
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 # Note: issues assigned to a closed version won't be copied due to validation rules
767 766 def copy_issues(project)
768 767 # Stores the source issue id as a key and the copied issues as the
769 768 # value. Used to map the two togeather for issue relations.
770 769 issues_map = {}
771 770
771 # Store status and reopen locked/closed versions
772 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
773 version_statuses.each do |version, status|
774 version.update_attribute :status, 'open'
775 end
776
772 777 # Get issues sorted by root_id, lft so that parent issues
773 778 # get copied before their children
774 779 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
775 780 new_issue = Issue.new
776 781 new_issue.copy_from(issue, :subtasks => false)
777 782 new_issue.project = self
778 783 # Reassign fixed_versions by name, since names are unique per project
779 784 if issue.fixed_version && issue.fixed_version.project == project
780 785 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
781 786 end
782 787 # Reassign the category by name, since names are unique per project
783 788 if issue.category
784 789 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
785 790 end
786 791 # Parent issue
787 792 if issue.parent_id
788 793 if copied_parent = issues_map[issue.parent_id]
789 794 new_issue.parent_issue_id = copied_parent.id
790 795 end
791 796 end
792 797
793 798 self.issues << new_issue
794 799 if new_issue.new_record?
795 800 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
796 801 else
797 802 issues_map[issue.id] = new_issue unless new_issue.new_record?
798 803 end
799 804 end
800 805
806 # Restore locked/closed version statuses
807 version_statuses.each do |version, status|
808 version.update_attribute :status, status
809 end
810
801 811 # Relations after in case issues related each other
802 812 project.issues.each do |issue|
803 813 new_issue = issues_map[issue.id]
804 814 unless new_issue
805 815 # Issue was not copied
806 816 next
807 817 end
808 818
809 819 # Relations
810 820 issue.relations_from.each do |source_relation|
811 821 new_issue_relation = IssueRelation.new
812 822 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
813 823 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
814 824 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
815 825 new_issue_relation.issue_to = source_relation.issue_to
816 826 end
817 827 new_issue.relations_from << new_issue_relation
818 828 end
819 829
820 830 issue.relations_to.each do |source_relation|
821 831 new_issue_relation = IssueRelation.new
822 832 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
823 833 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
824 834 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
825 835 new_issue_relation.issue_from = source_relation.issue_from
826 836 end
827 837 new_issue.relations_to << new_issue_relation
828 838 end
829 839 end
830 840 end
831 841
832 842 # Copies members from +project+
833 843 def copy_members(project)
834 844 # Copy users first, then groups to handle members with inherited and given roles
835 845 members_to_copy = []
836 846 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
837 847 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
838 848
839 849 members_to_copy.each do |member|
840 850 new_member = Member.new
841 851 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
842 852 # only copy non inherited roles
843 853 # inherited roles will be added when copying the group membership
844 854 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
845 855 next if role_ids.empty?
846 856 new_member.role_ids = role_ids
847 857 new_member.project = self
848 858 self.members << new_member
849 859 end
850 860 end
851 861
852 862 # Copies queries from +project+
853 863 def copy_queries(project)
854 864 project.queries.each do |query|
855 865 new_query = ::Query.new
856 866 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
857 867 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
858 868 new_query.project = self
859 869 new_query.user_id = query.user_id
860 870 self.queries << new_query
861 871 end
862 872 end
863 873
864 874 # Copies boards from +project+
865 875 def copy_boards(project)
866 876 project.boards.each do |board|
867 877 new_board = Board.new
868 878 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
869 879 new_board.project = self
870 880 self.boards << new_board
871 881 end
872 882 end
873 883
874 884 def allowed_permissions
875 885 @allowed_permissions ||= begin
876 886 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
877 887 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
878 888 end
879 889 end
880 890
881 891 def allowed_actions
882 892 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
883 893 end
884 894
885 895 # Returns all the active Systemwide and project specific activities
886 896 def active_activities
887 897 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
888 898
889 899 if overridden_activity_ids.empty?
890 900 return TimeEntryActivity.shared.active
891 901 else
892 902 return system_activities_and_project_overrides
893 903 end
894 904 end
895 905
896 906 # Returns all the Systemwide and project specific activities
897 907 # (inactive and active)
898 908 def all_activities
899 909 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
900 910
901 911 if overridden_activity_ids.empty?
902 912 return TimeEntryActivity.shared
903 913 else
904 914 return system_activities_and_project_overrides(true)
905 915 end
906 916 end
907 917
908 918 # Returns the systemwide active activities merged with the project specific overrides
909 919 def system_activities_and_project_overrides(include_inactive=false)
910 920 if include_inactive
911 921 return TimeEntryActivity.shared.
912 922 find(:all,
913 923 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
914 924 self.time_entry_activities
915 925 else
916 926 return TimeEntryActivity.shared.active.
917 927 find(:all,
918 928 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
919 929 self.time_entry_activities.active
920 930 end
921 931 end
922 932
923 933 # Archives subprojects recursively
924 934 def archive!
925 935 children.each do |subproject|
926 936 subproject.send :archive!
927 937 end
928 938 update_attribute :status, STATUS_ARCHIVED
929 939 end
930 940
931 941 def update_position_under_parent
932 942 set_or_update_position_under(parent)
933 943 end
934 944
935 945 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
936 946 def set_or_update_position_under(target_parent)
937 947 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
938 948 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 949
940 950 if to_be_inserted_before
941 951 move_to_left_of(to_be_inserted_before)
942 952 elsif target_parent.nil?
943 953 if sibs.empty?
944 954 # move_to_root adds the project in first (ie. left) position
945 955 move_to_root
946 956 else
947 957 move_to_right_of(sibs.last) unless self == sibs.last
948 958 end
949 959 else
950 960 # move_to_child_of adds the project in last (ie.right) position
951 961 move_to_child_of(target_parent)
952 962 end
953 963 end
954 964 end
@@ -1,1212 +1,1233
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 should "copy issues assigned to a locked version" do
827 User.current = User.find(1)
828 assigned_version = Version.generate!(:name => "Assigned Issues")
829 @source_project.versions << assigned_version
830 Issue.generate_for_project!(@source_project,
831 :fixed_version_id => assigned_version.id,
832 :subject => "copy issues assigned to a locked version",
833 :tracker_id => 1,
834 :project_id => @source_project.id)
835 assigned_version.update_attribute :status, 'locked'
836
837 assert @project.copy(@source_project)
838 @project.reload
839 copied_issue = @project.issues.first(:conditions => {:subject => "copy issues assigned to a locked version"})
840
841 assert copied_issue
842 assert copied_issue.fixed_version
843 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
844 assert_equal 'locked', copied_issue.fixed_version.status
845 end
846
826 847 should "change the new issues to use the copied version" do
827 848 User.current = User.find(1)
828 849 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
829 850 @source_project.versions << assigned_version
830 851 assert_equal 3, @source_project.versions.size
831 852 Issue.generate_for_project!(@source_project,
832 853 :fixed_version_id => assigned_version.id,
833 854 :subject => "change the new issues to use the copied version",
834 855 :tracker_id => 1,
835 856 :project_id => @source_project.id)
836 857
837 858 assert @project.copy(@source_project)
838 859 @project.reload
839 860 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
840 861
841 862 assert copied_issue
842 863 assert copied_issue.fixed_version
843 864 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
844 865 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
845 866 end
846 867
847 868 should "keep target shared versions from other project" do
848 869 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open', :project_id => 1, :sharing => 'system')
849 870 issue = Issue.generate_for_project!(@source_project,
850 871 :fixed_version => assigned_version,
851 872 :subject => "keep target shared versions",
852 873 :tracker_id => 1,
853 874 :project_id => @source_project.id)
854 875
855 876 assert @project.copy(@source_project)
856 877 @project.reload
857 878 copied_issue = @project.issues.first(:conditions => {:subject => "keep target shared versions"})
858 879
859 880 assert copied_issue
860 881 assert_equal assigned_version, copied_issue.fixed_version
861 882 end
862 883
863 884 should "copy issue relations" do
864 885 Setting.cross_project_issue_relations = '1'
865 886
866 887 second_issue = Issue.generate!(:status_id => 5,
867 888 :subject => "copy issue relation",
868 889 :tracker_id => 1,
869 890 :assigned_to_id => 2,
870 891 :project_id => @source_project.id)
871 892 source_relation = IssueRelation.create!(:issue_from => Issue.find(4),
872 893 :issue_to => second_issue,
873 894 :relation_type => "relates")
874 895 source_relation_cross_project = IssueRelation.create!(:issue_from => Issue.find(1),
875 896 :issue_to => second_issue,
876 897 :relation_type => "duplicates")
877 898
878 899 assert @project.copy(@source_project)
879 900 assert_equal @source_project.issues.count, @project.issues.count
880 901 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
881 902 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
882 903
883 904 # First issue with a relation on project
884 905 assert_equal 1, copied_issue.relations.size, "Relation not copied"
885 906 copied_relation = copied_issue.relations.first
886 907 assert_equal "relates", copied_relation.relation_type
887 908 assert_equal copied_second_issue.id, copied_relation.issue_to_id
888 909 assert_not_equal source_relation.id, copied_relation.id
889 910
890 911 # Second issue with a cross project relation
891 912 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
892 913 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
893 914 assert_equal "duplicates", copied_relation.relation_type
894 915 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
895 916 assert_not_equal source_relation_cross_project.id, copied_relation.id
896 917 end
897 918
898 919 should "copy issue attachments" do
899 920 issue = Issue.generate!(:subject => "copy with attachment", :tracker_id => 1, :project_id => @source_project.id)
900 921 Attachment.create!(:container => issue, :file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 1)
901 922 @source_project.issues << issue
902 923 assert @project.copy(@source_project)
903 924
904 925 copied_issue = @project.issues.first(:conditions => {:subject => "copy with attachment"})
905 926 assert_not_nil copied_issue
906 927 assert_equal 1, copied_issue.attachments.count, "Attachment not copied"
907 928 assert_equal "testfile.txt", copied_issue.attachments.first.filename
908 929 end
909 930
910 931 should "copy memberships" do
911 932 assert @project.valid?
912 933 assert @project.members.empty?
913 934 assert @project.copy(@source_project)
914 935
915 936 assert_equal @source_project.memberships.size, @project.memberships.size
916 937 @project.memberships.each do |membership|
917 938 assert membership
918 939 assert_equal @project, membership.project
919 940 end
920 941 end
921 942
922 943 should "copy memberships with groups and additional roles" do
923 944 group = Group.create!(:lastname => "Copy group")
924 945 user = User.find(7)
925 946 group.users << user
926 947 # group role
927 948 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
928 949 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
929 950 # additional role
930 951 member.role_ids = [1]
931 952
932 953 assert @project.copy(@source_project)
933 954 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
934 955 assert_not_nil member
935 956 assert_equal [1, 2], member.role_ids.sort
936 957 end
937 958
938 959 should "copy project specific queries" do
939 960 assert @project.valid?
940 961 assert @project.queries.empty?
941 962 assert @project.copy(@source_project)
942 963
943 964 assert_equal @source_project.queries.size, @project.queries.size
944 965 @project.queries.each do |query|
945 966 assert query
946 967 assert_equal @project, query.project
947 968 end
948 969 assert_equal @source_project.queries.map(&:user_id).sort, @project.queries.map(&:user_id).sort
949 970 end
950 971
951 972 should "copy versions" do
952 973 @source_project.versions << Version.generate!
953 974 @source_project.versions << Version.generate!
954 975
955 976 assert @project.versions.empty?
956 977 assert @project.copy(@source_project)
957 978
958 979 assert_equal @source_project.versions.size, @project.versions.size
959 980 @project.versions.each do |version|
960 981 assert version
961 982 assert_equal @project, version.project
962 983 end
963 984 end
964 985
965 986 should "copy wiki" do
966 987 assert_difference 'Wiki.count' do
967 988 assert @project.copy(@source_project)
968 989 end
969 990
970 991 assert @project.wiki
971 992 assert_not_equal @source_project.wiki, @project.wiki
972 993 assert_equal "Start page", @project.wiki.start_page
973 994 end
974 995
975 996 should "copy wiki pages and content with hierarchy" do
976 997 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
977 998 assert @project.copy(@source_project)
978 999 end
979 1000
980 1001 assert @project.wiki
981 1002 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
982 1003
983 1004 @project.wiki.pages.each do |wiki_page|
984 1005 assert wiki_page.content
985 1006 assert !@source_project.wiki.pages.include?(wiki_page)
986 1007 end
987 1008
988 1009 parent = @project.wiki.find_page('Parent_page')
989 1010 child1 = @project.wiki.find_page('Child_page_1')
990 1011 child2 = @project.wiki.find_page('Child_page_2')
991 1012 assert_equal parent, child1.parent
992 1013 assert_equal parent, child2.parent
993 1014 end
994 1015
995 1016 should "copy issue categories" do
996 1017 assert @project.copy(@source_project)
997 1018
998 1019 assert_equal 2, @project.issue_categories.size
999 1020 @project.issue_categories.each do |issue_category|
1000 1021 assert !@source_project.issue_categories.include?(issue_category)
1001 1022 end
1002 1023 end
1003 1024
1004 1025 should "copy boards" do
1005 1026 assert @project.copy(@source_project)
1006 1027
1007 1028 assert_equal 1, @project.boards.size
1008 1029 @project.boards.each do |board|
1009 1030 assert !@source_project.boards.include?(board)
1010 1031 end
1011 1032 end
1012 1033
1013 1034 should "change the new issues to use the copied issue categories" do
1014 1035 issue = Issue.find(4)
1015 1036 issue.update_attribute(:category_id, 3)
1016 1037
1017 1038 assert @project.copy(@source_project)
1018 1039
1019 1040 @project.issues.each do |issue|
1020 1041 assert issue.category
1021 1042 assert_equal "Stock management", issue.category.name # Same name
1022 1043 assert_not_equal IssueCategory.find(3), issue.category # Different record
1023 1044 end
1024 1045 end
1025 1046
1026 1047 should "limit copy with :only option" do
1027 1048 assert @project.members.empty?
1028 1049 assert @project.issue_categories.empty?
1029 1050 assert @source_project.issues.any?
1030 1051
1031 1052 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
1032 1053
1033 1054 assert @project.members.any?
1034 1055 assert @project.issue_categories.any?
1035 1056 assert @project.issues.empty?
1036 1057 end
1037 1058 end
1038 1059
1039 1060 def test_copy_should_copy_subtasks
1040 1061 source = Project.generate!(:tracker_ids => [1])
1041 1062 issue = Issue.generate_with_descendants!(source, :subject => 'Parent')
1042 1063 project = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1])
1043 1064
1044 1065 assert_difference 'Project.count' do
1045 1066 assert_difference 'Issue.count', 1+issue.descendants.count do
1046 1067 assert project.copy(source.reload)
1047 1068 end
1048 1069 end
1049 1070 copy = Issue.where(:parent_id => nil).order("id DESC").first
1050 1071 assert_equal project, copy.project
1051 1072 assert_equal issue.descendants.count, copy.descendants.count
1052 1073 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
1053 1074 assert child_copy.descendants.any?
1054 1075 end
1055 1076
1056 1077 context "#start_date" do
1057 1078 setup do
1058 1079 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1059 1080 @project = Project.generate!(:identifier => 'test0')
1060 1081 @project.trackers << Tracker.generate!
1061 1082 end
1062 1083
1063 1084 should "be nil if there are no issues on the project" do
1064 1085 assert_nil @project.start_date
1065 1086 end
1066 1087
1067 1088 should "be tested when issues have no start date"
1068 1089
1069 1090 should "be the earliest start date of it's issues" do
1070 1091 early = 7.days.ago.to_date
1071 1092 Issue.generate_for_project!(@project, :start_date => Date.today)
1072 1093 Issue.generate_for_project!(@project, :start_date => early)
1073 1094
1074 1095 assert_equal early, @project.start_date
1075 1096 end
1076 1097
1077 1098 end
1078 1099
1079 1100 context "#due_date" do
1080 1101 setup do
1081 1102 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1082 1103 @project = Project.generate!(:identifier => 'test0')
1083 1104 @project.trackers << Tracker.generate!
1084 1105 end
1085 1106
1086 1107 should "be nil if there are no issues on the project" do
1087 1108 assert_nil @project.due_date
1088 1109 end
1089 1110
1090 1111 should "be tested when issues have no due date"
1091 1112
1092 1113 should "be the latest due date of it's issues" do
1093 1114 future = 7.days.from_now.to_date
1094 1115 Issue.generate_for_project!(@project, :due_date => future)
1095 1116 Issue.generate_for_project!(@project, :due_date => Date.today)
1096 1117
1097 1118 assert_equal future, @project.due_date
1098 1119 end
1099 1120
1100 1121 should "be the latest due date of it's versions" do
1101 1122 future = 7.days.from_now.to_date
1102 1123 @project.versions << Version.generate!(:effective_date => future)
1103 1124 @project.versions << Version.generate!(:effective_date => Date.today)
1104 1125
1105 1126
1106 1127 assert_equal future, @project.due_date
1107 1128
1108 1129 end
1109 1130
1110 1131 should "pick the latest date from it's issues and versions" do
1111 1132 future = 7.days.from_now.to_date
1112 1133 far_future = 14.days.from_now.to_date
1113 1134 Issue.generate_for_project!(@project, :due_date => far_future)
1114 1135 @project.versions << Version.generate!(:effective_date => future)
1115 1136
1116 1137 assert_equal far_future, @project.due_date
1117 1138 end
1118 1139
1119 1140 end
1120 1141
1121 1142 context "Project#completed_percent" do
1122 1143 setup do
1123 1144 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1124 1145 @project = Project.generate!(:identifier => 'test0')
1125 1146 @project.trackers << Tracker.generate!
1126 1147 end
1127 1148
1128 1149 context "no versions" do
1129 1150 should "be 100" do
1130 1151 assert_equal 100, @project.completed_percent
1131 1152 end
1132 1153 end
1133 1154
1134 1155 context "with versions" do
1135 1156 should "return 0 if the versions have no issues" do
1136 1157 Version.generate!(:project => @project)
1137 1158 Version.generate!(:project => @project)
1138 1159
1139 1160 assert_equal 0, @project.completed_percent
1140 1161 end
1141 1162
1142 1163 should "return 100 if the version has only closed issues" do
1143 1164 v1 = Version.generate!(:project => @project)
1144 1165 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
1145 1166 v2 = Version.generate!(:project => @project)
1146 1167 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
1147 1168
1148 1169 assert_equal 100, @project.completed_percent
1149 1170 end
1150 1171
1151 1172 should "return the averaged completed percent of the versions (not weighted)" do
1152 1173 v1 = Version.generate!(:project => @project)
1153 1174 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
1154 1175 v2 = Version.generate!(:project => @project)
1155 1176 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
1156 1177
1157 1178 assert_equal 50, @project.completed_percent
1158 1179 end
1159 1180
1160 1181 end
1161 1182 end
1162 1183
1163 1184 context "#notified_users" do
1164 1185 setup do
1165 1186 @project = Project.generate!
1166 1187 @role = Role.generate!
1167 1188
1168 1189 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1169 1190 Member.create!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1170 1191
1171 1192 @all_events_user = User.generate!(:mail_notification => 'all')
1172 1193 Member.create!(:project => @project, :roles => [@role], :principal => @all_events_user)
1173 1194
1174 1195 @no_events_user = User.generate!(:mail_notification => 'none')
1175 1196 Member.create!(:project => @project, :roles => [@role], :principal => @no_events_user)
1176 1197
1177 1198 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1178 1199 Member.create!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1179 1200
1180 1201 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1181 1202 Member.create!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1182 1203
1183 1204 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1184 1205 Member.create!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1185 1206 end
1186 1207
1187 1208 should "include members with a mail notification" do
1188 1209 assert @project.notified_users.include?(@user_with_membership_notification)
1189 1210 end
1190 1211
1191 1212 should "include users with the 'all' notification option" do
1192 1213 assert @project.notified_users.include?(@all_events_user)
1193 1214 end
1194 1215
1195 1216 should "not include users with the 'none' notification option" do
1196 1217 assert !@project.notified_users.include?(@no_events_user)
1197 1218 end
1198 1219
1199 1220 should "not include users with the 'only_my_events' notification option" do
1200 1221 assert !@project.notified_users.include?(@only_my_events_user)
1201 1222 end
1202 1223
1203 1224 should "not include users with the 'only_assigned' notification option" do
1204 1225 assert !@project.notified_users.include?(@only_assigned_user)
1205 1226 end
1206 1227
1207 1228 should "not include users with the 'only_owner' notification option" do
1208 1229 assert !@project.notified_users.include?(@only_owned_user)
1209 1230 end
1210 1231 end
1211 1232
1212 1233 end
General Comments 0
You need to be logged in to leave comments. Login now