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