##// END OF EJS Templates
Fixed: parent project field doesn't include blank value when a member with 'add subproject' permission edits a child project (#4790)....
Jean-Philippe Lang -
r3291:d02ddefff4f1
parent child
Show More
@@ -1,684 +1,684
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 # Project statuses
20 20 STATUS_ACTIVE = 1
21 21 STATUS_ARCHIVED = 9
22 22
23 23 # Specific overidden Activities
24 24 has_many :time_entry_activities
25 25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
26 26 has_many :memberships, :class_name => 'Member'
27 27 has_many :member_principals, :class_name => 'Member',
28 28 :include => :principal,
29 29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
30 30 has_many :users, :through => :members
31 31 has_many :principals, :through => :member_principals, :source => :principal
32 32
33 33 has_many :enabled_modules, :dependent => :delete_all
34 34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
35 35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
36 36 has_many :issue_changes, :through => :issues, :source => :journals
37 37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
38 38 has_many :time_entries, :dependent => :delete_all
39 39 has_many :queries, :dependent => :delete_all
40 40 has_many :documents, :dependent => :destroy
41 41 has_many :news, :dependent => :delete_all, :include => :author
42 42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
43 43 has_many :boards, :dependent => :destroy, :order => "position ASC"
44 44 has_one :repository, :dependent => :destroy
45 45 has_many :changesets, :through => :repository
46 46 has_one :wiki, :dependent => :destroy
47 47 # Custom field for the project issues
48 48 has_and_belongs_to_many :issue_custom_fields,
49 49 :class_name => 'IssueCustomField',
50 50 :order => "#{CustomField.table_name}.position",
51 51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
52 52 :association_foreign_key => 'custom_field_id'
53 53
54 54 acts_as_nested_set :order => 'name'
55 55 acts_as_attachable :view_permission => :view_files,
56 56 :delete_permission => :manage_files
57 57
58 58 acts_as_customizable
59 59 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
60 60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
61 61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
62 62 :author => nil
63 63
64 64 attr_protected :status, :enabled_module_names
65 65
66 66 validates_presence_of :name, :identifier
67 67 validates_uniqueness_of :name, :identifier
68 68 validates_associated :repository, :wiki
69 69 validates_length_of :name, :maximum => 30
70 70 validates_length_of :homepage, :maximum => 255
71 71 validates_length_of :identifier, :in => 1..20
72 72 # donwcase letters, digits, dashes but not digits only
73 73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
74 74 # reserved words
75 75 validates_exclusion_of :identifier, :in => %w( new )
76 76
77 77 before_destroy :delete_all_members, :destroy_children
78 78
79 79 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
80 80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
81 81 named_scope :all_public, { :conditions => { :is_public => true } }
82 82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
83 83
84 84 def identifier=(identifier)
85 85 super unless identifier_frozen?
86 86 end
87 87
88 88 def identifier_frozen?
89 89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
90 90 end
91 91
92 92 # returns latest created projects
93 93 # non public projects will be returned only if user is a member of those
94 94 def self.latest(user=nil, count=5)
95 95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
96 96 end
97 97
98 98 # Returns a SQL :conditions string used to find all active projects for the specified user.
99 99 #
100 100 # Examples:
101 101 # Projects.visible_by(admin) => "projects.status = 1"
102 102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
103 103 def self.visible_by(user=nil)
104 104 user ||= User.current
105 105 if user && user.admin?
106 106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
107 107 elsif user && user.memberships.any?
108 108 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
109 109 else
110 110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
111 111 end
112 112 end
113 113
114 114 def self.allowed_to_condition(user, permission, options={})
115 115 statements = []
116 116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
117 117 if perm = Redmine::AccessControl.permission(permission)
118 118 unless perm.project_module.nil?
119 119 # If the permission belongs to a project module, make sure the module is enabled
120 120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
121 121 end
122 122 end
123 123 if options[:project]
124 124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
125 125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
126 126 base_statement = "(#{project_statement}) AND (#{base_statement})"
127 127 end
128 128 if user.admin?
129 129 # no restriction
130 130 else
131 131 statements << "1=0"
132 132 if user.logged?
133 133 if Role.non_member.allowed_to?(permission) && !options[:member]
134 134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
135 135 end
136 136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
137 137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
138 138 else
139 139 if Role.anonymous.allowed_to?(permission) && !options[:member]
140 140 # anonymous user allowed on public project
141 141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
142 142 end
143 143 end
144 144 end
145 145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
146 146 end
147 147
148 148 # Returns the Systemwide and project specific activities
149 149 def activities(include_inactive=false)
150 150 if include_inactive
151 151 return all_activities
152 152 else
153 153 return active_activities
154 154 end
155 155 end
156 156
157 157 # Will create a new Project specific Activity or update an existing one
158 158 #
159 159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
160 160 # does not successfully save.
161 161 def update_or_create_time_entry_activity(id, activity_hash)
162 162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
163 163 self.create_time_entry_activity_if_needed(activity_hash)
164 164 else
165 165 activity = project.time_entry_activities.find_by_id(id.to_i)
166 166 activity.update_attributes(activity_hash) if activity
167 167 end
168 168 end
169 169
170 170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
171 171 #
172 172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
173 173 # does not successfully save.
174 174 def create_time_entry_activity_if_needed(activity)
175 175 if activity['parent_id']
176 176
177 177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
178 178 activity['name'] = parent_activity.name
179 179 activity['position'] = parent_activity.position
180 180
181 181 if Enumeration.overridding_change?(activity, parent_activity)
182 182 project_activity = self.time_entry_activities.create(activity)
183 183
184 184 if project_activity.new_record?
185 185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
186 186 else
187 187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
188 188 end
189 189 end
190 190 end
191 191 end
192 192
193 193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
194 194 #
195 195 # Examples:
196 196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
197 197 # project.project_condition(false) => "projects.id = 1"
198 198 def project_condition(with_subprojects)
199 199 cond = "#{Project.table_name}.id = #{id}"
200 200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
201 201 cond
202 202 end
203 203
204 204 def self.find(*args)
205 205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
206 206 project = find_by_identifier(*args)
207 207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
208 208 project
209 209 else
210 210 super
211 211 end
212 212 end
213 213
214 214 def to_param
215 215 # id is used for projects with a numeric identifier (compatibility)
216 216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
217 217 end
218 218
219 219 def active?
220 220 self.status == STATUS_ACTIVE
221 221 end
222 222
223 223 # Archives the project and its descendants
224 224 def archive
225 225 # Check that there is no issue of a non descendant project that is assigned
226 226 # to one of the project or descendant versions
227 227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
228 228 if v_ids.any? && Issue.find(:first, :include => :project,
229 229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
230 230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
231 231 return false
232 232 end
233 233 Project.transaction do
234 234 archive!
235 235 end
236 236 true
237 237 end
238 238
239 239 # Unarchives the project
240 240 # All its ancestors must be active
241 241 def unarchive
242 242 return false if ancestors.detect {|a| !a.active?}
243 243 update_attribute :status, STATUS_ACTIVE
244 244 end
245 245
246 246 # Returns an array of projects the project can be moved to
247 247 # by the current user
248 248 def allowed_parents
249 249 return @allowed_parents if @allowed_parents
250 250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
251 251 @allowed_parents = @allowed_parents - self_and_descendants
252 if User.current.allowed_to?(:add_project, nil, :global => true)
252 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
253 253 @allowed_parents << nil
254 254 end
255 255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
256 256 @allowed_parents << parent
257 257 end
258 258 @allowed_parents
259 259 end
260 260
261 261 # Sets the parent of the project with authorization check
262 262 def set_allowed_parent!(p)
263 263 unless p.nil? || p.is_a?(Project)
264 264 if p.to_s.blank?
265 265 p = nil
266 266 else
267 267 p = Project.find_by_id(p)
268 268 return false unless p
269 269 end
270 270 end
271 271 if p.nil?
272 272 if !new_record? && allowed_parents.empty?
273 273 return false
274 274 end
275 275 elsif !allowed_parents.include?(p)
276 276 return false
277 277 end
278 278 set_parent!(p)
279 279 end
280 280
281 281 # Sets the parent of the project
282 282 # Argument can be either a Project, a String, a Fixnum or nil
283 283 def set_parent!(p)
284 284 unless p.nil? || p.is_a?(Project)
285 285 if p.to_s.blank?
286 286 p = nil
287 287 else
288 288 p = Project.find_by_id(p)
289 289 return false unless p
290 290 end
291 291 end
292 292 if p == parent && !p.nil?
293 293 # Nothing to do
294 294 true
295 295 elsif p.nil? || (p.active? && move_possible?(p))
296 296 # Insert the project so that target's children or root projects stay alphabetically sorted
297 297 sibs = (p.nil? ? self.class.roots : p.children)
298 298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
299 299 if to_be_inserted_before
300 300 move_to_left_of(to_be_inserted_before)
301 301 elsif p.nil?
302 302 if sibs.empty?
303 303 # move_to_root adds the project in first (ie. left) position
304 304 move_to_root
305 305 else
306 306 move_to_right_of(sibs.last) unless self == sibs.last
307 307 end
308 308 else
309 309 # move_to_child_of adds the project in last (ie.right) position
310 310 move_to_child_of(p)
311 311 end
312 312 Issue.update_versions_from_hierarchy_change(self)
313 313 true
314 314 else
315 315 # Can not move to the given target
316 316 false
317 317 end
318 318 end
319 319
320 320 # Returns an array of the trackers used by the project and its active sub projects
321 321 def rolled_up_trackers
322 322 @rolled_up_trackers ||=
323 323 Tracker.find(:all, :include => :projects,
324 324 :select => "DISTINCT #{Tracker.table_name}.*",
325 325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
326 326 :order => "#{Tracker.table_name}.position")
327 327 end
328 328
329 329 # Closes open and locked project versions that are completed
330 330 def close_completed_versions
331 331 Version.transaction do
332 332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
333 333 if version.completed?
334 334 version.update_attribute(:status, 'closed')
335 335 end
336 336 end
337 337 end
338 338 end
339 339
340 340 # Returns a scope of the Versions used by the project
341 341 def shared_versions
342 342 @shared_versions ||=
343 343 Version.scoped(:include => :project,
344 344 :conditions => "#{Project.table_name}.id = #{id}" +
345 345 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
346 346 " #{Version.table_name}.sharing = 'system'" +
347 347 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
348 348 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
349 349 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
350 350 "))")
351 351 end
352 352
353 353 # Returns a hash of project users grouped by role
354 354 def users_by_role
355 355 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
356 356 m.roles.each do |r|
357 357 h[r] ||= []
358 358 h[r] << m.user
359 359 end
360 360 h
361 361 end
362 362 end
363 363
364 364 # Deletes all project's members
365 365 def delete_all_members
366 366 me, mr = Member.table_name, MemberRole.table_name
367 367 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
368 368 Member.delete_all(['project_id = ?', id])
369 369 end
370 370
371 371 # Users issues can be assigned to
372 372 def assignable_users
373 373 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
374 374 end
375 375
376 376 # Returns the mail adresses of users that should be always notified on project events
377 377 def recipients
378 378 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
379 379 end
380 380
381 381 # Returns the users that should be notified on project events
382 382 def notified_users
383 383 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
384 384 end
385 385
386 386 # Returns an array of all custom fields enabled for project issues
387 387 # (explictly associated custom fields and custom fields enabled for all projects)
388 388 def all_issue_custom_fields
389 389 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
390 390 end
391 391
392 392 def project
393 393 self
394 394 end
395 395
396 396 def <=>(project)
397 397 name.downcase <=> project.name.downcase
398 398 end
399 399
400 400 def to_s
401 401 name
402 402 end
403 403
404 404 # Returns a short description of the projects (first lines)
405 405 def short_description(length = 255)
406 406 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
407 407 end
408 408
409 409 # Return true if this project is allowed to do the specified action.
410 410 # action can be:
411 411 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
412 412 # * a permission Symbol (eg. :edit_project)
413 413 def allows_to?(action)
414 414 if action.is_a? Hash
415 415 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
416 416 else
417 417 allowed_permissions.include? action
418 418 end
419 419 end
420 420
421 421 def module_enabled?(module_name)
422 422 module_name = module_name.to_s
423 423 enabled_modules.detect {|m| m.name == module_name}
424 424 end
425 425
426 426 def enabled_module_names=(module_names)
427 427 if module_names && module_names.is_a?(Array)
428 428 module_names = module_names.collect(&:to_s)
429 429 # remove disabled modules
430 430 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
431 431 # add new modules
432 432 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
433 433 else
434 434 enabled_modules.clear
435 435 end
436 436 end
437 437
438 438 # Returns an auto-generated project identifier based on the last identifier used
439 439 def self.next_identifier
440 440 p = Project.find(:first, :order => 'created_on DESC')
441 441 p.nil? ? nil : p.identifier.to_s.succ
442 442 end
443 443
444 444 # Copies and saves the Project instance based on the +project+.
445 445 # Duplicates the source project's:
446 446 # * Wiki
447 447 # * Versions
448 448 # * Categories
449 449 # * Issues
450 450 # * Members
451 451 # * Queries
452 452 #
453 453 # Accepts an +options+ argument to specify what to copy
454 454 #
455 455 # Examples:
456 456 # project.copy(1) # => copies everything
457 457 # project.copy(1, :only => 'members') # => copies members only
458 458 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
459 459 def copy(project, options={})
460 460 project = project.is_a?(Project) ? project : Project.find(project)
461 461
462 462 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
463 463 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
464 464
465 465 Project.transaction do
466 466 if save
467 467 reload
468 468 to_be_copied.each do |name|
469 469 send "copy_#{name}", project
470 470 end
471 471 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
472 472 save
473 473 end
474 474 end
475 475 end
476 476
477 477
478 478 # Copies +project+ and returns the new instance. This will not save
479 479 # the copy
480 480 def self.copy_from(project)
481 481 begin
482 482 project = project.is_a?(Project) ? project : Project.find(project)
483 483 if project
484 484 # clear unique attributes
485 485 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
486 486 copy = Project.new(attributes)
487 487 copy.enabled_modules = project.enabled_modules
488 488 copy.trackers = project.trackers
489 489 copy.custom_values = project.custom_values.collect {|v| v.clone}
490 490 copy.issue_custom_fields = project.issue_custom_fields
491 491 return copy
492 492 else
493 493 return nil
494 494 end
495 495 rescue ActiveRecord::RecordNotFound
496 496 return nil
497 497 end
498 498 end
499 499
500 500 private
501 501
502 502 # Destroys children before destroying self
503 503 def destroy_children
504 504 children.each do |child|
505 505 child.destroy
506 506 end
507 507 end
508 508
509 509 # Copies wiki from +project+
510 510 def copy_wiki(project)
511 511 # Check that the source project has a wiki first
512 512 unless project.wiki.nil?
513 513 self.wiki ||= Wiki.new
514 514 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
515 515 project.wiki.pages.each do |page|
516 516 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
517 517 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
518 518 new_wiki_page.content = new_wiki_content
519 519 wiki.pages << new_wiki_page
520 520 end
521 521 end
522 522 end
523 523
524 524 # Copies versions from +project+
525 525 def copy_versions(project)
526 526 project.versions.each do |version|
527 527 new_version = Version.new
528 528 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
529 529 self.versions << new_version
530 530 end
531 531 end
532 532
533 533 # Copies issue categories from +project+
534 534 def copy_issue_categories(project)
535 535 project.issue_categories.each do |issue_category|
536 536 new_issue_category = IssueCategory.new
537 537 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
538 538 self.issue_categories << new_issue_category
539 539 end
540 540 end
541 541
542 542 # Copies issues from +project+
543 543 def copy_issues(project)
544 544 # Stores the source issue id as a key and the copied issues as the
545 545 # value. Used to map the two togeather for issue relations.
546 546 issues_map = {}
547 547
548 548 project.issues.each do |issue|
549 549 new_issue = Issue.new
550 550 new_issue.copy_from(issue)
551 551 # Reassign fixed_versions by name, since names are unique per
552 552 # project and the versions for self are not yet saved
553 553 if issue.fixed_version
554 554 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
555 555 end
556 556 # Reassign the category by name, since names are unique per
557 557 # project and the categories for self are not yet saved
558 558 if issue.category
559 559 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
560 560 end
561 561 self.issues << new_issue
562 562 issues_map[issue.id] = new_issue
563 563 end
564 564
565 565 # Relations after in case issues related each other
566 566 project.issues.each do |issue|
567 567 new_issue = issues_map[issue.id]
568 568
569 569 # Relations
570 570 issue.relations_from.each do |source_relation|
571 571 new_issue_relation = IssueRelation.new
572 572 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
573 573 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
574 574 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
575 575 new_issue_relation.issue_to = source_relation.issue_to
576 576 end
577 577 new_issue.relations_from << new_issue_relation
578 578 end
579 579
580 580 issue.relations_to.each do |source_relation|
581 581 new_issue_relation = IssueRelation.new
582 582 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
583 583 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
584 584 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
585 585 new_issue_relation.issue_from = source_relation.issue_from
586 586 end
587 587 new_issue.relations_to << new_issue_relation
588 588 end
589 589 end
590 590 end
591 591
592 592 # Copies members from +project+
593 593 def copy_members(project)
594 594 project.memberships.each do |member|
595 595 new_member = Member.new
596 596 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
597 597 # only copy non inherited roles
598 598 # inherited roles will be added when copying the group membership
599 599 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
600 600 next if role_ids.empty?
601 601 new_member.role_ids = role_ids
602 602 new_member.project = self
603 603 self.members << new_member
604 604 end
605 605 end
606 606
607 607 # Copies queries from +project+
608 608 def copy_queries(project)
609 609 project.queries.each do |query|
610 610 new_query = Query.new
611 611 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
612 612 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
613 613 new_query.project = self
614 614 self.queries << new_query
615 615 end
616 616 end
617 617
618 618 # Copies boards from +project+
619 619 def copy_boards(project)
620 620 project.boards.each do |board|
621 621 new_board = Board.new
622 622 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
623 623 new_board.project = self
624 624 self.boards << new_board
625 625 end
626 626 end
627 627
628 628 def allowed_permissions
629 629 @allowed_permissions ||= begin
630 630 module_names = enabled_modules.collect {|m| m.name}
631 631 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
632 632 end
633 633 end
634 634
635 635 def allowed_actions
636 636 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
637 637 end
638 638
639 639 # Returns all the active Systemwide and project specific activities
640 640 def active_activities
641 641 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
642 642
643 643 if overridden_activity_ids.empty?
644 644 return TimeEntryActivity.shared.active
645 645 else
646 646 return system_activities_and_project_overrides
647 647 end
648 648 end
649 649
650 650 # Returns all the Systemwide and project specific activities
651 651 # (inactive and active)
652 652 def all_activities
653 653 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
654 654
655 655 if overridden_activity_ids.empty?
656 656 return TimeEntryActivity.shared
657 657 else
658 658 return system_activities_and_project_overrides(true)
659 659 end
660 660 end
661 661
662 662 # Returns the systemwide active activities merged with the project specific overrides
663 663 def system_activities_and_project_overrides(include_inactive=false)
664 664 if include_inactive
665 665 return TimeEntryActivity.shared.
666 666 find(:all,
667 667 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
668 668 self.time_entry_activities
669 669 else
670 670 return TimeEntryActivity.shared.active.
671 671 find(:all,
672 672 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
673 673 self.time_entry_activities.active
674 674 end
675 675 end
676 676
677 677 # Archives subprojects recursively
678 678 def archive!
679 679 children.each do |subproject|
680 680 subproject.send :archive!
681 681 end
682 682 update_attribute :status, STATUS_ARCHIVED
683 683 end
684 684 end
@@ -1,741 +1,783
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class ProjectTest < ActiveSupport::TestCase
21 21 fixtures :all
22 22
23 23 def setup
24 24 @ecookbook = Project.find(1)
25 25 @ecookbook_sub1 = Project.find(3)
26 26 User.current = nil
27 27 end
28 28
29 29 should_validate_presence_of :name
30 30 should_validate_presence_of :identifier
31 31
32 32 should_validate_uniqueness_of :name
33 33 should_validate_uniqueness_of :identifier
34 34
35 35 context "associations" do
36 36 should_have_many :members
37 37 should_have_many :users, :through => :members
38 38 should_have_many :member_principals
39 39 should_have_many :principals, :through => :member_principals
40 40 should_have_many :enabled_modules
41 41 should_have_many :issues
42 42 should_have_many :issue_changes, :through => :issues
43 43 should_have_many :versions
44 44 should_have_many :time_entries
45 45 should_have_many :queries
46 46 should_have_many :documents
47 47 should_have_many :news
48 48 should_have_many :issue_categories
49 49 should_have_many :boards
50 50 should_have_many :changesets, :through => :repository
51 51
52 52 should_have_one :repository
53 53 should_have_one :wiki
54 54
55 55 should_have_and_belong_to_many :trackers
56 56 should_have_and_belong_to_many :issue_custom_fields
57 57 end
58 58
59 59 def test_truth
60 60 assert_kind_of Project, @ecookbook
61 61 assert_equal "eCookbook", @ecookbook.name
62 62 end
63 63
64 64 def test_update
65 65 assert_equal "eCookbook", @ecookbook.name
66 66 @ecookbook.name = "eCook"
67 67 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
68 68 @ecookbook.reload
69 69 assert_equal "eCook", @ecookbook.name
70 70 end
71 71
72 72 def test_validate_identifier
73 73 to_test = {"abc" => true,
74 74 "ab12" => true,
75 75 "ab-12" => true,
76 76 "12" => false,
77 77 "new" => false}
78 78
79 79 to_test.each do |identifier, valid|
80 80 p = Project.new
81 81 p.identifier = identifier
82 82 p.valid?
83 83 assert_equal valid, p.errors.on('identifier').nil?
84 84 end
85 85 end
86 86
87 87 def test_members_should_be_active_users
88 88 Project.all.each do |project|
89 89 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
90 90 end
91 91 end
92 92
93 93 def test_users_should_be_active_users
94 94 Project.all.each do |project|
95 95 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
96 96 end
97 97 end
98 98
99 99 def test_archive
100 100 user = @ecookbook.members.first.user
101 101 @ecookbook.archive
102 102 @ecookbook.reload
103 103
104 104 assert !@ecookbook.active?
105 105 assert !user.projects.include?(@ecookbook)
106 106 # Subproject are also archived
107 107 assert !@ecookbook.children.empty?
108 108 assert @ecookbook.descendants.active.empty?
109 109 end
110 110
111 111 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
112 112 # Assign an issue of a project to a version of a child project
113 113 Issue.find(4).update_attribute :fixed_version_id, 4
114 114
115 115 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
116 116 assert_equal false, @ecookbook.archive
117 117 end
118 118 @ecookbook.reload
119 119 assert @ecookbook.active?
120 120 end
121 121
122 122 def test_unarchive
123 123 user = @ecookbook.members.first.user
124 124 @ecookbook.archive
125 125 # A subproject of an archived project can not be unarchived
126 126 assert !@ecookbook_sub1.unarchive
127 127
128 128 # Unarchive project
129 129 assert @ecookbook.unarchive
130 130 @ecookbook.reload
131 131 assert @ecookbook.active?
132 132 assert user.projects.include?(@ecookbook)
133 133 # Subproject can now be unarchived
134 134 @ecookbook_sub1.reload
135 135 assert @ecookbook_sub1.unarchive
136 136 end
137 137
138 138 def test_destroy
139 139 # 2 active members
140 140 assert_equal 2, @ecookbook.members.size
141 141 # and 1 is locked
142 142 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
143 143 # some boards
144 144 assert @ecookbook.boards.any?
145 145
146 146 @ecookbook.destroy
147 147 # make sure that the project non longer exists
148 148 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
149 149 # make sure related data was removed
150 150 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
151 151 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
152 152 end
153 153
154 154 def test_move_an_orphan_project_to_a_root_project
155 155 sub = Project.find(2)
156 156 sub.set_parent! @ecookbook
157 157 assert_equal @ecookbook.id, sub.parent.id
158 158 @ecookbook.reload
159 159 assert_equal 4, @ecookbook.children.size
160 160 end
161 161
162 162 def test_move_an_orphan_project_to_a_subproject
163 163 sub = Project.find(2)
164 164 assert sub.set_parent!(@ecookbook_sub1)
165 165 end
166 166
167 167 def test_move_a_root_project_to_a_project
168 168 sub = @ecookbook
169 169 assert sub.set_parent!(Project.find(2))
170 170 end
171 171
172 172 def test_should_not_move_a_project_to_its_children
173 173 sub = @ecookbook
174 174 assert !(sub.set_parent!(Project.find(3)))
175 175 end
176 176
177 177 def test_set_parent_should_add_roots_in_alphabetical_order
178 178 ProjectCustomField.delete_all
179 179 Project.delete_all
180 180 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
181 181 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
182 182 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
183 183 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
184 184
185 185 assert_equal 4, Project.count
186 186 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
187 187 end
188 188
189 189 def test_set_parent_should_add_children_in_alphabetical_order
190 190 ProjectCustomField.delete_all
191 191 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
192 192 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
193 193 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
194 194 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
195 195 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
196 196
197 197 parent.reload
198 198 assert_equal 4, parent.children.size
199 199 assert_equal parent.children.sort_by(&:name), parent.children
200 200 end
201 201
202 202 def test_rebuild_should_sort_children_alphabetically
203 203 ProjectCustomField.delete_all
204 204 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
205 205 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
206 206 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
207 207 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
208 208 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
209 209
210 210 Project.update_all("lft = NULL, rgt = NULL")
211 211 Project.rebuild!
212 212
213 213 parent.reload
214 214 assert_equal 4, parent.children.size
215 215 assert_equal parent.children.sort_by(&:name), parent.children
216 216 end
217 217
218 218
219 219 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
220 220 # Parent issue with a hierarchy project's fixed version
221 221 parent_issue = Issue.find(1)
222 222 parent_issue.update_attribute(:fixed_version_id, 4)
223 223 parent_issue.reload
224 224 assert_equal 4, parent_issue.fixed_version_id
225 225
226 226 # Should keep fixed versions for the issues
227 227 issue_with_local_fixed_version = Issue.find(5)
228 228 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
229 229 issue_with_local_fixed_version.reload
230 230 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
231 231
232 232 # Local issue with hierarchy fixed_version
233 233 issue_with_hierarchy_fixed_version = Issue.find(13)
234 234 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
235 235 issue_with_hierarchy_fixed_version.reload
236 236 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
237 237
238 238 # Move project out of the issue's hierarchy
239 239 moved_project = Project.find(3)
240 240 moved_project.set_parent!(Project.find(2))
241 241 parent_issue.reload
242 242 issue_with_local_fixed_version.reload
243 243 issue_with_hierarchy_fixed_version.reload
244 244
245 245 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
246 246 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"
247 247 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
248 248 end
249 249
250 250 def test_parent
251 251 p = Project.find(6).parent
252 252 assert p.is_a?(Project)
253 253 assert_equal 5, p.id
254 254 end
255 255
256 256 def test_ancestors
257 257 a = Project.find(6).ancestors
258 258 assert a.first.is_a?(Project)
259 259 assert_equal [1, 5], a.collect(&:id)
260 260 end
261 261
262 262 def test_root
263 263 r = Project.find(6).root
264 264 assert r.is_a?(Project)
265 265 assert_equal 1, r.id
266 266 end
267 267
268 268 def test_children
269 269 c = Project.find(1).children
270 270 assert c.first.is_a?(Project)
271 271 assert_equal [5, 3, 4], c.collect(&:id)
272 272 end
273 273
274 274 def test_descendants
275 275 d = Project.find(1).descendants
276 276 assert d.first.is_a?(Project)
277 277 assert_equal [5, 6, 3, 4], d.collect(&:id)
278 278 end
279 279
280 280 def test_allowed_parents_should_be_empty_for_non_member_user
281 281 Role.non_member.add_permission!(:add_project)
282 282 user = User.find(9)
283 283 assert user.memberships.empty?
284 284 User.current = user
285 285 assert Project.new.allowed_parents.compact.empty?
286 286 end
287 287
288 def test_allowed_parents_with_add_subprojects_permission
289 Role.find(1).remove_permission!(:add_project)
290 Role.find(1).add_permission!(:add_subprojects)
291 User.current = User.find(2)
292 # new project
293 assert !Project.new.allowed_parents.include?(nil)
294 assert Project.new.allowed_parents.include?(Project.find(1))
295 # existing root project
296 assert Project.find(1).allowed_parents.include?(nil)
297 # existing child
298 assert Project.find(3).allowed_parents.include?(Project.find(1))
299 assert !Project.find(3).allowed_parents.include?(nil)
300 end
301
302 def test_allowed_parents_with_add_project_permission
303 Role.find(1).add_permission!(:add_project)
304 Role.find(1).remove_permission!(:add_subprojects)
305 User.current = User.find(2)
306 # new project
307 assert Project.new.allowed_parents.include?(nil)
308 assert !Project.new.allowed_parents.include?(Project.find(1))
309 # existing root project
310 assert Project.find(1).allowed_parents.include?(nil)
311 # existing child
312 assert Project.find(3).allowed_parents.include?(Project.find(1))
313 assert Project.find(3).allowed_parents.include?(nil)
314 end
315
316 def test_allowed_parents_with_add_project_and_subprojects_permission
317 Role.find(1).add_permission!(:add_project)
318 Role.find(1).add_permission!(:add_subprojects)
319 User.current = User.find(2)
320 # new project
321 assert Project.new.allowed_parents.include?(nil)
322 assert Project.new.allowed_parents.include?(Project.find(1))
323 # existing root project
324 assert Project.find(1).allowed_parents.include?(nil)
325 # existing child
326 assert Project.find(3).allowed_parents.include?(Project.find(1))
327 assert Project.find(3).allowed_parents.include?(nil)
328 end
329
288 330 def test_users_by_role
289 331 users_by_role = Project.find(1).users_by_role
290 332 assert_kind_of Hash, users_by_role
291 333 role = Role.find(1)
292 334 assert_kind_of Array, users_by_role[role]
293 335 assert users_by_role[role].include?(User.find(2))
294 336 end
295 337
296 338 def test_rolled_up_trackers
297 339 parent = Project.find(1)
298 340 parent.trackers = Tracker.find([1,2])
299 341 child = parent.children.find(3)
300 342
301 343 assert_equal [1, 2], parent.tracker_ids
302 344 assert_equal [2, 3], child.trackers.collect(&:id)
303 345
304 346 assert_kind_of Tracker, parent.rolled_up_trackers.first
305 347 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
306 348
307 349 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
308 350 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
309 351 end
310 352
311 353 def test_rolled_up_trackers_should_ignore_archived_subprojects
312 354 parent = Project.find(1)
313 355 parent.trackers = Tracker.find([1,2])
314 356 child = parent.children.find(3)
315 357 child.trackers = Tracker.find([1,3])
316 358 parent.children.each(&:archive)
317 359
318 360 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
319 361 end
320 362
321 363 def test_shared_versions_none_sharing
322 364 p = Project.find(5)
323 365 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
324 366 assert p.shared_versions.include?(v)
325 367 assert !p.children.first.shared_versions.include?(v)
326 368 assert !p.root.shared_versions.include?(v)
327 369 assert !p.siblings.first.shared_versions.include?(v)
328 370 assert !p.root.siblings.first.shared_versions.include?(v)
329 371 end
330 372
331 373 def test_shared_versions_descendants_sharing
332 374 p = Project.find(5)
333 375 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
334 376 assert p.shared_versions.include?(v)
335 377 assert p.children.first.shared_versions.include?(v)
336 378 assert !p.root.shared_versions.include?(v)
337 379 assert !p.siblings.first.shared_versions.include?(v)
338 380 assert !p.root.siblings.first.shared_versions.include?(v)
339 381 end
340 382
341 383 def test_shared_versions_hierarchy_sharing
342 384 p = Project.find(5)
343 385 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
344 386 assert p.shared_versions.include?(v)
345 387 assert p.children.first.shared_versions.include?(v)
346 388 assert p.root.shared_versions.include?(v)
347 389 assert !p.siblings.first.shared_versions.include?(v)
348 390 assert !p.root.siblings.first.shared_versions.include?(v)
349 391 end
350 392
351 393 def test_shared_versions_tree_sharing
352 394 p = Project.find(5)
353 395 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
354 396 assert p.shared_versions.include?(v)
355 397 assert p.children.first.shared_versions.include?(v)
356 398 assert p.root.shared_versions.include?(v)
357 399 assert p.siblings.first.shared_versions.include?(v)
358 400 assert !p.root.siblings.first.shared_versions.include?(v)
359 401 end
360 402
361 403 def test_shared_versions_system_sharing
362 404 p = Project.find(5)
363 405 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
364 406 assert p.shared_versions.include?(v)
365 407 assert p.children.first.shared_versions.include?(v)
366 408 assert p.root.shared_versions.include?(v)
367 409 assert p.siblings.first.shared_versions.include?(v)
368 410 assert p.root.siblings.first.shared_versions.include?(v)
369 411 end
370 412
371 413 def test_shared_versions
372 414 parent = Project.find(1)
373 415 child = parent.children.find(3)
374 416 private_child = parent.children.find(5)
375 417
376 418 assert_equal [1,2,3], parent.version_ids.sort
377 419 assert_equal [4], child.version_ids
378 420 assert_equal [6], private_child.version_ids
379 421 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
380 422
381 423 assert_equal 6, parent.shared_versions.size
382 424 parent.shared_versions.each do |version|
383 425 assert_kind_of Version, version
384 426 end
385 427
386 428 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
387 429 end
388 430
389 431 def test_shared_versions_should_ignore_archived_subprojects
390 432 parent = Project.find(1)
391 433 child = parent.children.find(3)
392 434 child.archive
393 435 parent.reload
394 436
395 437 assert_equal [1,2,3], parent.version_ids.sort
396 438 assert_equal [4], child.version_ids
397 439 assert !parent.shared_versions.collect(&:id).include?(4)
398 440 end
399 441
400 442 def test_shared_versions_visible_to_user
401 443 user = User.find(3)
402 444 parent = Project.find(1)
403 445 child = parent.children.find(5)
404 446
405 447 assert_equal [1,2,3], parent.version_ids.sort
406 448 assert_equal [6], child.version_ids
407 449
408 450 versions = parent.shared_versions.visible(user)
409 451
410 452 assert_equal 4, versions.size
411 453 versions.each do |version|
412 454 assert_kind_of Version, version
413 455 end
414 456
415 457 assert !versions.collect(&:id).include?(6)
416 458 end
417 459
418 460
419 461 def test_next_identifier
420 462 ProjectCustomField.delete_all
421 463 Project.create!(:name => 'last', :identifier => 'p2008040')
422 464 assert_equal 'p2008041', Project.next_identifier
423 465 end
424 466
425 467 def test_next_identifier_first_project
426 468 Project.delete_all
427 469 assert_nil Project.next_identifier
428 470 end
429 471
430 472
431 473 def test_enabled_module_names_should_not_recreate_enabled_modules
432 474 project = Project.find(1)
433 475 # Remove one module
434 476 modules = project.enabled_modules.slice(0..-2)
435 477 assert modules.any?
436 478 assert_difference 'EnabledModule.count', -1 do
437 479 project.enabled_module_names = modules.collect(&:name)
438 480 end
439 481 project.reload
440 482 # Ids should be preserved
441 483 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
442 484 end
443 485
444 486 def test_copy_from_existing_project
445 487 source_project = Project.find(1)
446 488 copied_project = Project.copy_from(1)
447 489
448 490 assert copied_project
449 491 # Cleared attributes
450 492 assert copied_project.id.blank?
451 493 assert copied_project.name.blank?
452 494 assert copied_project.identifier.blank?
453 495
454 496 # Duplicated attributes
455 497 assert_equal source_project.description, copied_project.description
456 498 assert_equal source_project.enabled_modules, copied_project.enabled_modules
457 499 assert_equal source_project.trackers, copied_project.trackers
458 500
459 501 # Default attributes
460 502 assert_equal 1, copied_project.status
461 503 end
462 504
463 505 def test_activities_should_use_the_system_activities
464 506 project = Project.find(1)
465 507 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
466 508 end
467 509
468 510
469 511 def test_activities_should_use_the_project_specific_activities
470 512 project = Project.find(1)
471 513 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
472 514 assert overridden_activity.save!
473 515
474 516 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
475 517 end
476 518
477 519 def test_activities_should_not_include_the_inactive_project_specific_activities
478 520 project = Project.find(1)
479 521 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
480 522 assert overridden_activity.save!
481 523
482 524 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
483 525 end
484 526
485 527 def test_activities_should_not_include_project_specific_activities_from_other_projects
486 528 project = Project.find(1)
487 529 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
488 530 assert overridden_activity.save!
489 531
490 532 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
491 533 end
492 534
493 535 def test_activities_should_handle_nils
494 536 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
495 537 TimeEntryActivity.delete_all
496 538
497 539 # No activities
498 540 project = Project.find(1)
499 541 assert project.activities.empty?
500 542
501 543 # No system, one overridden
502 544 assert overridden_activity.save!
503 545 project.reload
504 546 assert_equal [overridden_activity], project.activities
505 547 end
506 548
507 549 def test_activities_should_override_system_activities_with_project_activities
508 550 project = Project.find(1)
509 551 parent_activity = TimeEntryActivity.find(:first)
510 552 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
511 553 assert overridden_activity.save!
512 554
513 555 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
514 556 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
515 557 end
516 558
517 559 def test_activities_should_include_inactive_activities_if_specified
518 560 project = Project.find(1)
519 561 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
520 562 assert overridden_activity.save!
521 563
522 564 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
523 565 end
524 566
525 567 test 'activities should not include active System activities if the project has an override that is inactive' do
526 568 project = Project.find(1)
527 569 system_activity = TimeEntryActivity.find_by_name('Design')
528 570 assert system_activity.active?
529 571 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
530 572 assert overridden_activity.save!
531 573
532 574 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
533 575 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
534 576 end
535 577
536 578 def test_close_completed_versions
537 579 Version.update_all("status = 'open'")
538 580 project = Project.find(1)
539 581 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
540 582 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
541 583 project.close_completed_versions
542 584 project.reload
543 585 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
544 586 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
545 587 end
546 588
547 589 context "Project#copy" do
548 590 setup do
549 591 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
550 592 Project.destroy_all :identifier => "copy-test"
551 593 @source_project = Project.find(2)
552 594 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
553 595 @project.trackers = @source_project.trackers
554 596 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
555 597 end
556 598
557 599 should "copy issues" do
558 600 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
559 601 :subject => "copy issue status",
560 602 :tracker_id => 1,
561 603 :assigned_to_id => 2,
562 604 :project_id => @source_project.id)
563 605 assert @project.valid?
564 606 assert @project.issues.empty?
565 607 assert @project.copy(@source_project)
566 608
567 609 assert_equal @source_project.issues.size, @project.issues.size
568 610 @project.issues.each do |issue|
569 611 assert issue.valid?
570 612 assert ! issue.assigned_to.blank?
571 613 assert_equal @project, issue.project
572 614 end
573 615
574 616 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
575 617 assert copied_issue
576 618 assert copied_issue.status
577 619 assert_equal "Closed", copied_issue.status.name
578 620 end
579 621
580 622 should "change the new issues to use the copied version" do
581 623 User.current = User.find(1)
582 624 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
583 625 @source_project.versions << assigned_version
584 626 assert_equal 3, @source_project.versions.size
585 627 Issue.generate_for_project!(@source_project,
586 628 :fixed_version_id => assigned_version.id,
587 629 :subject => "change the new issues to use the copied version",
588 630 :tracker_id => 1,
589 631 :project_id => @source_project.id)
590 632
591 633 assert @project.copy(@source_project)
592 634 @project.reload
593 635 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
594 636
595 637 assert copied_issue
596 638 assert copied_issue.fixed_version
597 639 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
598 640 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
599 641 end
600 642
601 643 should "copy issue relations" do
602 644 Setting.cross_project_issue_relations = '1'
603 645
604 646 second_issue = Issue.generate!(:status_id => 5,
605 647 :subject => "copy issue relation",
606 648 :tracker_id => 1,
607 649 :assigned_to_id => 2,
608 650 :project_id => @source_project.id)
609 651 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
610 652 :issue_to => second_issue,
611 653 :relation_type => "relates")
612 654 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
613 655 :issue_to => second_issue,
614 656 :relation_type => "duplicates")
615 657
616 658 assert @project.copy(@source_project)
617 659 assert_equal @source_project.issues.count, @project.issues.count
618 660 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
619 661 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
620 662
621 663 # First issue with a relation on project
622 664 assert_equal 1, copied_issue.relations.size, "Relation not copied"
623 665 copied_relation = copied_issue.relations.first
624 666 assert_equal "relates", copied_relation.relation_type
625 667 assert_equal copied_second_issue.id, copied_relation.issue_to_id
626 668 assert_not_equal source_relation.id, copied_relation.id
627 669
628 670 # Second issue with a cross project relation
629 671 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
630 672 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
631 673 assert_equal "duplicates", copied_relation.relation_type
632 674 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
633 675 assert_not_equal source_relation_cross_project.id, copied_relation.id
634 676 end
635 677
636 678 should "copy memberships" do
637 679 assert @project.valid?
638 680 assert @project.members.empty?
639 681 assert @project.copy(@source_project)
640 682
641 683 assert_equal @source_project.memberships.size, @project.memberships.size
642 684 @project.memberships.each do |membership|
643 685 assert membership
644 686 assert_equal @project, membership.project
645 687 end
646 688 end
647 689
648 690 should "copy project specific queries" do
649 691 assert @project.valid?
650 692 assert @project.queries.empty?
651 693 assert @project.copy(@source_project)
652 694
653 695 assert_equal @source_project.queries.size, @project.queries.size
654 696 @project.queries.each do |query|
655 697 assert query
656 698 assert_equal @project, query.project
657 699 end
658 700 end
659 701
660 702 should "copy versions" do
661 703 @source_project.versions << Version.generate!
662 704 @source_project.versions << Version.generate!
663 705
664 706 assert @project.versions.empty?
665 707 assert @project.copy(@source_project)
666 708
667 709 assert_equal @source_project.versions.size, @project.versions.size
668 710 @project.versions.each do |version|
669 711 assert version
670 712 assert_equal @project, version.project
671 713 end
672 714 end
673 715
674 716 should "copy wiki" do
675 717 assert_difference 'Wiki.count' do
676 718 assert @project.copy(@source_project)
677 719 end
678 720
679 721 assert @project.wiki
680 722 assert_not_equal @source_project.wiki, @project.wiki
681 723 assert_equal "Start page", @project.wiki.start_page
682 724 end
683 725
684 726 should "copy wiki pages and content" do
685 727 assert @project.copy(@source_project)
686 728
687 729 assert @project.wiki
688 730 assert_equal 1, @project.wiki.pages.length
689 731
690 732 @project.wiki.pages.each do |wiki_page|
691 733 assert wiki_page.content
692 734 assert !@source_project.wiki.pages.include?(wiki_page)
693 735 end
694 736 end
695 737
696 738 should "copy issue categories" do
697 739 assert @project.copy(@source_project)
698 740
699 741 assert_equal 2, @project.issue_categories.size
700 742 @project.issue_categories.each do |issue_category|
701 743 assert !@source_project.issue_categories.include?(issue_category)
702 744 end
703 745 end
704 746
705 747 should "copy boards" do
706 748 assert @project.copy(@source_project)
707 749
708 750 assert_equal 1, @project.boards.size
709 751 @project.boards.each do |board|
710 752 assert !@source_project.boards.include?(board)
711 753 end
712 754 end
713 755
714 756 should "change the new issues to use the copied issue categories" do
715 757 issue = Issue.find(4)
716 758 issue.update_attribute(:category_id, 3)
717 759
718 760 assert @project.copy(@source_project)
719 761
720 762 @project.issues.each do |issue|
721 763 assert issue.category
722 764 assert_equal "Stock management", issue.category.name # Same name
723 765 assert_not_equal IssueCategory.find(3), issue.category # Different record
724 766 end
725 767 end
726 768
727 769 should "limit copy with :only option" do
728 770 assert @project.members.empty?
729 771 assert @project.issue_categories.empty?
730 772 assert @source_project.issues.any?
731 773
732 774 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
733 775
734 776 assert @project.members.any?
735 777 assert @project.issue_categories.any?
736 778 assert @project.issues.empty?
737 779 end
738 780
739 781 end
740 782
741 783 end
General Comments 0
You need to be logged in to leave comments. Login now