##// END OF EJS Templates
Fixed: Project copy loses wiki pages hierarchy (#4797)....
Jean-Philippe Lang -
r3298:541d830d2a96
parent child
Show More
@@ -1,684 +1,696
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 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 wiki_pages_map = {}
515 516 project.wiki.pages.each do |page|
517 # Skip pages without content
518 next if page.content.nil?
516 519 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
517 520 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
518 521 new_wiki_page.content = new_wiki_content
519 522 wiki.pages << new_wiki_page
523 wiki_pages_map[page.id] = new_wiki_page
524 end
525 wiki.save
526 # Reproduce page hierarchy
527 project.wiki.pages.each do |page|
528 if page.parent_id && wiki_pages_map[page.id]
529 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
530 wiki_pages_map[page.id].save
531 end
520 532 end
521 533 end
522 534 end
523 535
524 536 # Copies versions from +project+
525 537 def copy_versions(project)
526 538 project.versions.each do |version|
527 539 new_version = Version.new
528 540 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
529 541 self.versions << new_version
530 542 end
531 543 end
532 544
533 545 # Copies issue categories from +project+
534 546 def copy_issue_categories(project)
535 547 project.issue_categories.each do |issue_category|
536 548 new_issue_category = IssueCategory.new
537 549 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
538 550 self.issue_categories << new_issue_category
539 551 end
540 552 end
541 553
542 554 # Copies issues from +project+
543 555 def copy_issues(project)
544 556 # Stores the source issue id as a key and the copied issues as the
545 557 # value. Used to map the two togeather for issue relations.
546 558 issues_map = {}
547 559
548 560 project.issues.each do |issue|
549 561 new_issue = Issue.new
550 562 new_issue.copy_from(issue)
551 563 # Reassign fixed_versions by name, since names are unique per
552 564 # project and the versions for self are not yet saved
553 565 if issue.fixed_version
554 566 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
555 567 end
556 568 # Reassign the category by name, since names are unique per
557 569 # project and the categories for self are not yet saved
558 570 if issue.category
559 571 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
560 572 end
561 573 self.issues << new_issue
562 574 issues_map[issue.id] = new_issue
563 575 end
564 576
565 577 # Relations after in case issues related each other
566 578 project.issues.each do |issue|
567 579 new_issue = issues_map[issue.id]
568 580
569 581 # Relations
570 582 issue.relations_from.each do |source_relation|
571 583 new_issue_relation = IssueRelation.new
572 584 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
573 585 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
574 586 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
575 587 new_issue_relation.issue_to = source_relation.issue_to
576 588 end
577 589 new_issue.relations_from << new_issue_relation
578 590 end
579 591
580 592 issue.relations_to.each do |source_relation|
581 593 new_issue_relation = IssueRelation.new
582 594 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
583 595 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
584 596 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
585 597 new_issue_relation.issue_from = source_relation.issue_from
586 598 end
587 599 new_issue.relations_to << new_issue_relation
588 600 end
589 601 end
590 602 end
591 603
592 604 # Copies members from +project+
593 605 def copy_members(project)
594 606 project.memberships.each do |member|
595 607 new_member = Member.new
596 608 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
597 609 # only copy non inherited roles
598 610 # inherited roles will be added when copying the group membership
599 611 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
600 612 next if role_ids.empty?
601 613 new_member.role_ids = role_ids
602 614 new_member.project = self
603 615 self.members << new_member
604 616 end
605 617 end
606 618
607 619 # Copies queries from +project+
608 620 def copy_queries(project)
609 621 project.queries.each do |query|
610 622 new_query = Query.new
611 623 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
612 624 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
613 625 new_query.project = self
614 626 self.queries << new_query
615 627 end
616 628 end
617 629
618 630 # Copies boards from +project+
619 631 def copy_boards(project)
620 632 project.boards.each do |board|
621 633 new_board = Board.new
622 634 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
623 635 new_board.project = self
624 636 self.boards << new_board
625 637 end
626 638 end
627 639
628 640 def allowed_permissions
629 641 @allowed_permissions ||= begin
630 642 module_names = enabled_modules.collect {|m| m.name}
631 643 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
632 644 end
633 645 end
634 646
635 647 def allowed_actions
636 648 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
637 649 end
638 650
639 651 # Returns all the active Systemwide and project specific activities
640 652 def active_activities
641 653 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
642 654
643 655 if overridden_activity_ids.empty?
644 656 return TimeEntryActivity.shared.active
645 657 else
646 658 return system_activities_and_project_overrides
647 659 end
648 660 end
649 661
650 662 # Returns all the Systemwide and project specific activities
651 663 # (inactive and active)
652 664 def all_activities
653 665 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
654 666
655 667 if overridden_activity_ids.empty?
656 668 return TimeEntryActivity.shared
657 669 else
658 670 return system_activities_and_project_overrides(true)
659 671 end
660 672 end
661 673
662 674 # Returns the systemwide active activities merged with the project specific overrides
663 675 def system_activities_and_project_overrides(include_inactive=false)
664 676 if include_inactive
665 677 return TimeEntryActivity.shared.
666 678 find(:all,
667 679 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
668 680 self.time_entry_activities
669 681 else
670 682 return TimeEntryActivity.shared.active.
671 683 find(:all,
672 684 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
673 685 self.time_entry_activities.active
674 686 end
675 687 end
676 688
677 689 # Archives subprojects recursively
678 690 def archive!
679 691 children.each do |subproject|
680 692 subproject.send :archive!
681 693 end
682 694 update_attribute :status, STATUS_ARCHIVED
683 695 end
684 696 end
@@ -1,74 +1,98
1 1 ---
2 2 wiki_contents_001:
3 3 text: |-
4 4 h1. CookBook documentation
5 5
6 6 {{child_pages}}
7 7
8 8 Some updated [[documentation]] here with gzipped history
9 9 updated_on: 2007-03-07 00:10:51 +01:00
10 10 page_id: 1
11 11 id: 1
12 12 version: 3
13 13 author_id: 1
14 14 comments: Gzip compression activated
15 15 wiki_contents_002:
16 16 text: |-
17 17 h1. Another page
18 18
19 19 This is a link to a ticket: #2
20 20 And this is an included page:
21 21 {{include(Page with an inline image)}}
22 22 updated_on: 2007-03-08 00:18:07 +01:00
23 23 page_id: 2
24 24 id: 2
25 25 version: 1
26 26 author_id: 1
27 27 comments:
28 28 wiki_contents_003:
29 29 text: |-
30 30 h1. Start page
31 31
32 32 E-commerce web site start page
33 33 updated_on: 2007-03-08 00:18:07 +01:00
34 34 page_id: 3
35 35 id: 3
36 36 version: 1
37 37 author_id: 1
38 38 comments:
39 39 wiki_contents_004:
40 40 text: |-
41 41 h1. Page with an inline image
42 42
43 43 This is an inline image:
44 44
45 45 !logo.gif!
46 46 updated_on: 2007-03-08 00:18:07 +01:00
47 47 page_id: 4
48 48 id: 4
49 49 version: 1
50 50 author_id: 1
51 51 comments:
52 52 wiki_contents_005:
53 53 text: |-
54 54 h1. Child page 1
55 55
56 56 This is a child page
57 57 updated_on: 2007-03-08 00:18:07 +01:00
58 58 page_id: 5
59 59 id: 5
60 60 version: 1
61 61 author_id: 1
62 62 comments:
63 63 wiki_contents_006:
64 64 text: |-
65 65 h1. Child page 2
66 66
67 67 This is a child page
68 68 updated_on: 2007-03-08 00:18:07 +01:00
69 69 page_id: 6
70 70 id: 6
71 71 version: 1
72 72 author_id: 1
73 73 comments:
74 wiki_contents_007:
75 text: This is a child page
76 updated_on: 2007-03-08 00:18:07 +01:00
77 page_id: 7
78 id: 7
79 version: 1
80 author_id: 1
81 comments:
82 wiki_contents_008:
83 text: This is a parent page
84 updated_on: 2007-03-08 00:18:07 +01:00
85 page_id: 8
86 id: 8
87 version: 1
88 author_id: 1
89 comments:
90 wiki_contents_009:
91 text: This is a child page
92 updated_on: 2007-03-08 00:18:07 +01:00
93 page_id: 9
94 id: 9
95 version: 1
96 author_id: 1
97 comments:
74 98 No newline at end of file
@@ -1,44 +1,65
1 1 ---
2 2 wiki_pages_001:
3 3 created_on: 2007-03-07 00:08:07 +01:00
4 4 title: CookBook_documentation
5 5 id: 1
6 6 wiki_id: 1
7 7 protected: true
8 8 parent_id:
9 9 wiki_pages_002:
10 10 created_on: 2007-03-08 00:18:07 +01:00
11 11 title: Another_page
12 12 id: 2
13 13 wiki_id: 1
14 14 protected: false
15 15 parent_id:
16 16 wiki_pages_003:
17 17 created_on: 2007-03-08 00:18:07 +01:00
18 18 title: Start_page
19 19 id: 3
20 20 wiki_id: 2
21 21 protected: false
22 22 parent_id:
23 23 wiki_pages_004:
24 24 created_on: 2007-03-08 00:18:07 +01:00
25 25 title: Page_with_an_inline_image
26 26 id: 4
27 27 wiki_id: 1
28 28 protected: false
29 29 parent_id: 1
30 30 wiki_pages_005:
31 31 created_on: 2007-03-08 00:18:07 +01:00
32 32 title: Child_1
33 33 id: 5
34 34 wiki_id: 1
35 35 protected: false
36 36 parent_id: 2
37 37 wiki_pages_006:
38 38 created_on: 2007-03-08 00:18:07 +01:00
39 39 title: Child_2
40 40 id: 6
41 41 wiki_id: 1
42 42 protected: false
43 43 parent_id: 2
44 wiki_pages_007:
45 created_on: 2007-03-08 00:18:07 +01:00
46 title: Child_page_1
47 id: 7
48 wiki_id: 2
49 protected: false
50 parent_id: 8
51 wiki_pages_008:
52 created_on: 2007-03-08 00:18:07 +01:00
53 title: Parent_page
54 id: 8
55 wiki_id: 2
56 protected: false
57 parent_id:
58 wiki_pages_009:
59 created_on: 2007-03-08 00:18:07 +01:00
60 title: Child_page_2
61 id: 9
62 wiki_id: 2
63 protected: false
64 parent_id: 8
44 65 No newline at end of file
@@ -1,783 +1,791
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 288 def test_allowed_parents_with_add_subprojects_permission
289 289 Role.find(1).remove_permission!(:add_project)
290 290 Role.find(1).add_permission!(:add_subprojects)
291 291 User.current = User.find(2)
292 292 # new project
293 293 assert !Project.new.allowed_parents.include?(nil)
294 294 assert Project.new.allowed_parents.include?(Project.find(1))
295 295 # existing root project
296 296 assert Project.find(1).allowed_parents.include?(nil)
297 297 # existing child
298 298 assert Project.find(3).allowed_parents.include?(Project.find(1))
299 299 assert !Project.find(3).allowed_parents.include?(nil)
300 300 end
301 301
302 302 def test_allowed_parents_with_add_project_permission
303 303 Role.find(1).add_permission!(:add_project)
304 304 Role.find(1).remove_permission!(:add_subprojects)
305 305 User.current = User.find(2)
306 306 # new project
307 307 assert Project.new.allowed_parents.include?(nil)
308 308 assert !Project.new.allowed_parents.include?(Project.find(1))
309 309 # existing root project
310 310 assert Project.find(1).allowed_parents.include?(nil)
311 311 # existing child
312 312 assert Project.find(3).allowed_parents.include?(Project.find(1))
313 313 assert Project.find(3).allowed_parents.include?(nil)
314 314 end
315 315
316 316 def test_allowed_parents_with_add_project_and_subprojects_permission
317 317 Role.find(1).add_permission!(:add_project)
318 318 Role.find(1).add_permission!(:add_subprojects)
319 319 User.current = User.find(2)
320 320 # new project
321 321 assert Project.new.allowed_parents.include?(nil)
322 322 assert Project.new.allowed_parents.include?(Project.find(1))
323 323 # existing root project
324 324 assert Project.find(1).allowed_parents.include?(nil)
325 325 # existing child
326 326 assert Project.find(3).allowed_parents.include?(Project.find(1))
327 327 assert Project.find(3).allowed_parents.include?(nil)
328 328 end
329 329
330 330 def test_users_by_role
331 331 users_by_role = Project.find(1).users_by_role
332 332 assert_kind_of Hash, users_by_role
333 333 role = Role.find(1)
334 334 assert_kind_of Array, users_by_role[role]
335 335 assert users_by_role[role].include?(User.find(2))
336 336 end
337 337
338 338 def test_rolled_up_trackers
339 339 parent = Project.find(1)
340 340 parent.trackers = Tracker.find([1,2])
341 341 child = parent.children.find(3)
342 342
343 343 assert_equal [1, 2], parent.tracker_ids
344 344 assert_equal [2, 3], child.trackers.collect(&:id)
345 345
346 346 assert_kind_of Tracker, parent.rolled_up_trackers.first
347 347 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
348 348
349 349 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
350 350 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
351 351 end
352 352
353 353 def test_rolled_up_trackers_should_ignore_archived_subprojects
354 354 parent = Project.find(1)
355 355 parent.trackers = Tracker.find([1,2])
356 356 child = parent.children.find(3)
357 357 child.trackers = Tracker.find([1,3])
358 358 parent.children.each(&:archive)
359 359
360 360 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
361 361 end
362 362
363 363 def test_shared_versions_none_sharing
364 364 p = Project.find(5)
365 365 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
366 366 assert p.shared_versions.include?(v)
367 367 assert !p.children.first.shared_versions.include?(v)
368 368 assert !p.root.shared_versions.include?(v)
369 369 assert !p.siblings.first.shared_versions.include?(v)
370 370 assert !p.root.siblings.first.shared_versions.include?(v)
371 371 end
372 372
373 373 def test_shared_versions_descendants_sharing
374 374 p = Project.find(5)
375 375 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
376 376 assert p.shared_versions.include?(v)
377 377 assert p.children.first.shared_versions.include?(v)
378 378 assert !p.root.shared_versions.include?(v)
379 379 assert !p.siblings.first.shared_versions.include?(v)
380 380 assert !p.root.siblings.first.shared_versions.include?(v)
381 381 end
382 382
383 383 def test_shared_versions_hierarchy_sharing
384 384 p = Project.find(5)
385 385 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
386 386 assert p.shared_versions.include?(v)
387 387 assert p.children.first.shared_versions.include?(v)
388 388 assert p.root.shared_versions.include?(v)
389 389 assert !p.siblings.first.shared_versions.include?(v)
390 390 assert !p.root.siblings.first.shared_versions.include?(v)
391 391 end
392 392
393 393 def test_shared_versions_tree_sharing
394 394 p = Project.find(5)
395 395 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
396 396 assert p.shared_versions.include?(v)
397 397 assert p.children.first.shared_versions.include?(v)
398 398 assert p.root.shared_versions.include?(v)
399 399 assert p.siblings.first.shared_versions.include?(v)
400 400 assert !p.root.siblings.first.shared_versions.include?(v)
401 401 end
402 402
403 403 def test_shared_versions_system_sharing
404 404 p = Project.find(5)
405 405 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
406 406 assert p.shared_versions.include?(v)
407 407 assert p.children.first.shared_versions.include?(v)
408 408 assert p.root.shared_versions.include?(v)
409 409 assert p.siblings.first.shared_versions.include?(v)
410 410 assert p.root.siblings.first.shared_versions.include?(v)
411 411 end
412 412
413 413 def test_shared_versions
414 414 parent = Project.find(1)
415 415 child = parent.children.find(3)
416 416 private_child = parent.children.find(5)
417 417
418 418 assert_equal [1,2,3], parent.version_ids.sort
419 419 assert_equal [4], child.version_ids
420 420 assert_equal [6], private_child.version_ids
421 421 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
422 422
423 423 assert_equal 6, parent.shared_versions.size
424 424 parent.shared_versions.each do |version|
425 425 assert_kind_of Version, version
426 426 end
427 427
428 428 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
429 429 end
430 430
431 431 def test_shared_versions_should_ignore_archived_subprojects
432 432 parent = Project.find(1)
433 433 child = parent.children.find(3)
434 434 child.archive
435 435 parent.reload
436 436
437 437 assert_equal [1,2,3], parent.version_ids.sort
438 438 assert_equal [4], child.version_ids
439 439 assert !parent.shared_versions.collect(&:id).include?(4)
440 440 end
441 441
442 442 def test_shared_versions_visible_to_user
443 443 user = User.find(3)
444 444 parent = Project.find(1)
445 445 child = parent.children.find(5)
446 446
447 447 assert_equal [1,2,3], parent.version_ids.sort
448 448 assert_equal [6], child.version_ids
449 449
450 450 versions = parent.shared_versions.visible(user)
451 451
452 452 assert_equal 4, versions.size
453 453 versions.each do |version|
454 454 assert_kind_of Version, version
455 455 end
456 456
457 457 assert !versions.collect(&:id).include?(6)
458 458 end
459 459
460 460
461 461 def test_next_identifier
462 462 ProjectCustomField.delete_all
463 463 Project.create!(:name => 'last', :identifier => 'p2008040')
464 464 assert_equal 'p2008041', Project.next_identifier
465 465 end
466 466
467 467 def test_next_identifier_first_project
468 468 Project.delete_all
469 469 assert_nil Project.next_identifier
470 470 end
471 471
472 472
473 473 def test_enabled_module_names_should_not_recreate_enabled_modules
474 474 project = Project.find(1)
475 475 # Remove one module
476 476 modules = project.enabled_modules.slice(0..-2)
477 477 assert modules.any?
478 478 assert_difference 'EnabledModule.count', -1 do
479 479 project.enabled_module_names = modules.collect(&:name)
480 480 end
481 481 project.reload
482 482 # Ids should be preserved
483 483 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
484 484 end
485 485
486 486 def test_copy_from_existing_project
487 487 source_project = Project.find(1)
488 488 copied_project = Project.copy_from(1)
489 489
490 490 assert copied_project
491 491 # Cleared attributes
492 492 assert copied_project.id.blank?
493 493 assert copied_project.name.blank?
494 494 assert copied_project.identifier.blank?
495 495
496 496 # Duplicated attributes
497 497 assert_equal source_project.description, copied_project.description
498 498 assert_equal source_project.enabled_modules, copied_project.enabled_modules
499 499 assert_equal source_project.trackers, copied_project.trackers
500 500
501 501 # Default attributes
502 502 assert_equal 1, copied_project.status
503 503 end
504 504
505 505 def test_activities_should_use_the_system_activities
506 506 project = Project.find(1)
507 507 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
508 508 end
509 509
510 510
511 511 def test_activities_should_use_the_project_specific_activities
512 512 project = Project.find(1)
513 513 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
514 514 assert overridden_activity.save!
515 515
516 516 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
517 517 end
518 518
519 519 def test_activities_should_not_include_the_inactive_project_specific_activities
520 520 project = Project.find(1)
521 521 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
522 522 assert overridden_activity.save!
523 523
524 524 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
525 525 end
526 526
527 527 def test_activities_should_not_include_project_specific_activities_from_other_projects
528 528 project = Project.find(1)
529 529 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
530 530 assert overridden_activity.save!
531 531
532 532 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
533 533 end
534 534
535 535 def test_activities_should_handle_nils
536 536 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
537 537 TimeEntryActivity.delete_all
538 538
539 539 # No activities
540 540 project = Project.find(1)
541 541 assert project.activities.empty?
542 542
543 543 # No system, one overridden
544 544 assert overridden_activity.save!
545 545 project.reload
546 546 assert_equal [overridden_activity], project.activities
547 547 end
548 548
549 549 def test_activities_should_override_system_activities_with_project_activities
550 550 project = Project.find(1)
551 551 parent_activity = TimeEntryActivity.find(:first)
552 552 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
553 553 assert overridden_activity.save!
554 554
555 555 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
556 556 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
557 557 end
558 558
559 559 def test_activities_should_include_inactive_activities_if_specified
560 560 project = Project.find(1)
561 561 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
562 562 assert overridden_activity.save!
563 563
564 564 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
565 565 end
566 566
567 567 test 'activities should not include active System activities if the project has an override that is inactive' do
568 568 project = Project.find(1)
569 569 system_activity = TimeEntryActivity.find_by_name('Design')
570 570 assert system_activity.active?
571 571 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
572 572 assert overridden_activity.save!
573 573
574 574 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
575 575 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
576 576 end
577 577
578 578 def test_close_completed_versions
579 579 Version.update_all("status = 'open'")
580 580 project = Project.find(1)
581 581 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
582 582 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
583 583 project.close_completed_versions
584 584 project.reload
585 585 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
586 586 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
587 587 end
588 588
589 589 context "Project#copy" do
590 590 setup do
591 591 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
592 592 Project.destroy_all :identifier => "copy-test"
593 593 @source_project = Project.find(2)
594 594 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
595 595 @project.trackers = @source_project.trackers
596 596 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
597 597 end
598 598
599 599 should "copy issues" do
600 600 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
601 601 :subject => "copy issue status",
602 602 :tracker_id => 1,
603 603 :assigned_to_id => 2,
604 604 :project_id => @source_project.id)
605 605 assert @project.valid?
606 606 assert @project.issues.empty?
607 607 assert @project.copy(@source_project)
608 608
609 609 assert_equal @source_project.issues.size, @project.issues.size
610 610 @project.issues.each do |issue|
611 611 assert issue.valid?
612 612 assert ! issue.assigned_to.blank?
613 613 assert_equal @project, issue.project
614 614 end
615 615
616 616 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
617 617 assert copied_issue
618 618 assert copied_issue.status
619 619 assert_equal "Closed", copied_issue.status.name
620 620 end
621 621
622 622 should "change the new issues to use the copied version" do
623 623 User.current = User.find(1)
624 624 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
625 625 @source_project.versions << assigned_version
626 626 assert_equal 3, @source_project.versions.size
627 627 Issue.generate_for_project!(@source_project,
628 628 :fixed_version_id => assigned_version.id,
629 629 :subject => "change the new issues to use the copied version",
630 630 :tracker_id => 1,
631 631 :project_id => @source_project.id)
632 632
633 633 assert @project.copy(@source_project)
634 634 @project.reload
635 635 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
636 636
637 637 assert copied_issue
638 638 assert copied_issue.fixed_version
639 639 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
640 640 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
641 641 end
642 642
643 643 should "copy issue relations" do
644 644 Setting.cross_project_issue_relations = '1'
645 645
646 646 second_issue = Issue.generate!(:status_id => 5,
647 647 :subject => "copy issue relation",
648 648 :tracker_id => 1,
649 649 :assigned_to_id => 2,
650 650 :project_id => @source_project.id)
651 651 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
652 652 :issue_to => second_issue,
653 653 :relation_type => "relates")
654 654 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
655 655 :issue_to => second_issue,
656 656 :relation_type => "duplicates")
657 657
658 658 assert @project.copy(@source_project)
659 659 assert_equal @source_project.issues.count, @project.issues.count
660 660 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
661 661 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
662 662
663 663 # First issue with a relation on project
664 664 assert_equal 1, copied_issue.relations.size, "Relation not copied"
665 665 copied_relation = copied_issue.relations.first
666 666 assert_equal "relates", copied_relation.relation_type
667 667 assert_equal copied_second_issue.id, copied_relation.issue_to_id
668 668 assert_not_equal source_relation.id, copied_relation.id
669 669
670 670 # Second issue with a cross project relation
671 671 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
672 672 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
673 673 assert_equal "duplicates", copied_relation.relation_type
674 674 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
675 675 assert_not_equal source_relation_cross_project.id, copied_relation.id
676 676 end
677 677
678 678 should "copy memberships" do
679 679 assert @project.valid?
680 680 assert @project.members.empty?
681 681 assert @project.copy(@source_project)
682 682
683 683 assert_equal @source_project.memberships.size, @project.memberships.size
684 684 @project.memberships.each do |membership|
685 685 assert membership
686 686 assert_equal @project, membership.project
687 687 end
688 688 end
689 689
690 690 should "copy project specific queries" do
691 691 assert @project.valid?
692 692 assert @project.queries.empty?
693 693 assert @project.copy(@source_project)
694 694
695 695 assert_equal @source_project.queries.size, @project.queries.size
696 696 @project.queries.each do |query|
697 697 assert query
698 698 assert_equal @project, query.project
699 699 end
700 700 end
701 701
702 702 should "copy versions" do
703 703 @source_project.versions << Version.generate!
704 704 @source_project.versions << Version.generate!
705 705
706 706 assert @project.versions.empty?
707 707 assert @project.copy(@source_project)
708 708
709 709 assert_equal @source_project.versions.size, @project.versions.size
710 710 @project.versions.each do |version|
711 711 assert version
712 712 assert_equal @project, version.project
713 713 end
714 714 end
715 715
716 716 should "copy wiki" do
717 717 assert_difference 'Wiki.count' do
718 718 assert @project.copy(@source_project)
719 719 end
720 720
721 721 assert @project.wiki
722 722 assert_not_equal @source_project.wiki, @project.wiki
723 723 assert_equal "Start page", @project.wiki.start_page
724 724 end
725 725
726 should "copy wiki pages and content" do
726 should "copy wiki pages and content with hierarchy" do
727 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
727 728 assert @project.copy(@source_project)
729 end
728 730
729 731 assert @project.wiki
730 assert_equal 1, @project.wiki.pages.length
732 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
731 733
732 734 @project.wiki.pages.each do |wiki_page|
733 735 assert wiki_page.content
734 736 assert !@source_project.wiki.pages.include?(wiki_page)
735 737 end
738
739 parent = @project.wiki.find_page('Parent_page')
740 child1 = @project.wiki.find_page('Child_page_1')
741 child2 = @project.wiki.find_page('Child_page_2')
742 assert_equal parent, child1.parent
743 assert_equal parent, child2.parent
736 744 end
737 745
738 746 should "copy issue categories" do
739 747 assert @project.copy(@source_project)
740 748
741 749 assert_equal 2, @project.issue_categories.size
742 750 @project.issue_categories.each do |issue_category|
743 751 assert !@source_project.issue_categories.include?(issue_category)
744 752 end
745 753 end
746 754
747 755 should "copy boards" do
748 756 assert @project.copy(@source_project)
749 757
750 758 assert_equal 1, @project.boards.size
751 759 @project.boards.each do |board|
752 760 assert !@source_project.boards.include?(board)
753 761 end
754 762 end
755 763
756 764 should "change the new issues to use the copied issue categories" do
757 765 issue = Issue.find(4)
758 766 issue.update_attribute(:category_id, 3)
759 767
760 768 assert @project.copy(@source_project)
761 769
762 770 @project.issues.each do |issue|
763 771 assert issue.category
764 772 assert_equal "Stock management", issue.category.name # Same name
765 773 assert_not_equal IssueCategory.find(3), issue.category # Different record
766 774 end
767 775 end
768 776
769 777 should "limit copy with :only option" do
770 778 assert @project.members.empty?
771 779 assert @project.issue_categories.empty?
772 780 assert @source_project.issues.any?
773 781
774 782 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
775 783
776 784 assert @project.members.any?
777 785 assert @project.issue_categories.any?
778 786 assert @project.issues.empty?
779 787 end
780 788
781 789 end
782 790
783 791 end
General Comments 0
You need to be logged in to leave comments. Login now