comparison app/models/project.rb @ 1338:25603efa57b5

Merge from live branch
author Chris Cannam
date Thu, 20 Jun 2013 13:14:14 +0100
parents 0a574315af3e
children 4f746d8966dd 51364c0cd58f 467282ce64a4
comparison
equal deleted inserted replaced
1209:1b1138f6f55e 1338:25603efa57b5
1 # Redmine - project management software 1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 # 3 #
4 # This program is free software; you can redistribute it and/or 4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License 5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2 6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version. 7 # of the License, or (at your option) any later version.
18 class Project < ActiveRecord::Base 18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes 19 include Redmine::SafeAttributes
20 20
21 # Project statuses 21 # Project statuses
22 STATUS_ACTIVE = 1 22 STATUS_ACTIVE = 1
23 STATUS_CLOSED = 5
23 STATUS_ARCHIVED = 9 24 STATUS_ARCHIVED = 9
24 25
25 # Maximum length for project identifiers 26 # Maximum length for project identifiers
26 IDENTIFIER_MAX_LENGTH = 100 27 IDENTIFIER_MAX_LENGTH = 100
27 28
28 # Specific overidden Activities 29 # Specific overidden Activities
29 has_many :time_entry_activities 30 has_many :time_entry_activities
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}" 31 has_many :members, :include => [:principal, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 has_many :memberships, :class_name => 'Member' 32 has_many :memberships, :class_name => 'Member'
32 has_many :member_principals, :class_name => 'Member', 33 has_many :member_principals, :class_name => 'Member',
33 :include => :principal, 34 :include => :principal,
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})" 35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 has_many :users, :through => :members 36 has_many :users, :through => :members
36 has_many :principals, :through => :member_principals, :source => :principal 37 has_many :principals, :through => :member_principals, :source => :principal
37 38
38 has_many :enabled_modules, :dependent => :delete_all 39 has_many :enabled_modules, :dependent => :delete_all
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" 40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker] 41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
41 has_many :issue_changes, :through => :issues, :source => :journals 42 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" 43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :time_entries, :dependent => :delete_all 44 has_many :time_entries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all 45 has_many :queries, :dependent => :delete_all
45 has_many :documents, :dependent => :destroy 46 has_many :documents, :dependent => :destroy
46 has_many :news, :dependent => :destroy, :include => :author 47 has_many :news, :dependent => :destroy, :include => :author
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" 48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :boards, :dependent => :destroy, :order => "position ASC" 49 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_one :repository, :dependent => :destroy 50 has_one :repository, :conditions => ["is_default = ?", true]
51 has_many :repositories, :dependent => :destroy
50 has_many :changesets, :through => :repository 52 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy 53 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues 54 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields, 55 has_and_belongs_to_many :issue_custom_fields,
54 :class_name => 'IssueCustomField', 56 :class_name => 'IssueCustomField',
73 validates_associated :repository, :wiki 75 validates_associated :repository, :wiki
74 validates_length_of :name, :maximum => 255 76 validates_length_of :name, :maximum => 255
75 validates_length_of :homepage, :maximum => 255 77 validates_length_of :homepage, :maximum => 255
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH 78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 # donwcase letters, digits, dashes but not digits only 79 # donwcase letters, digits, dashes but not digits only
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? } 80 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 # reserved words 81 # reserved words
80 validates_exclusion_of :identifier, :in => %w( new ) 82 validates_exclusion_of :identifier, :in => %w( new )
81 83
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
82 before_destroy :delete_all_members 85 before_destroy :delete_all_members
83 86
84 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] } } 87 scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} 88 scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 named_scope :all_public, { :conditions => { :is_public => true } } 89 scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
87 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }} 90 scope :all_public, { :conditions => { :is_public => true } }
88 named_scope :visible_roots, lambda { { :conditions => Project.root_visible_by(User.current) } } 91 scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
89 92 scope :visible_roots, lambda { { :conditions => Project.root_visible_by(User.current) } }
90 def initialize(attributes = nil) 93 scope :allowed_to, lambda {|*args|
94 user = User.current
95 permission = nil
96 if args.first.is_a?(Symbol)
97 permission = args.shift
98 else
99 user = args.shift
100 permission = args.shift
101 end
102 { :conditions => Project.allowed_to_condition(user, permission, *args) }
103 }
104 scope :like, lambda {|arg|
105 if arg.blank?
106 {}
107 else
108 pattern = "%#{arg.to_s.strip.downcase}%"
109 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
110 end
111 }
112
113 def initialize(attributes=nil, *args)
91 super 114 super
92 115
93 initialized = (attributes || {}).stringify_keys 116 initialized = (attributes || {}).stringify_keys
94 if !initialized.key?('identifier') && Setting.sequential_project_identifiers? 117 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
95 self.identifier = Project.next_identifier 118 self.identifier = Project.next_identifier
99 end 122 end
100 if !initialized.key?('enabled_module_names') 123 if !initialized.key?('enabled_module_names')
101 self.enabled_module_names = Setting.default_projects_modules 124 self.enabled_module_names = Setting.default_projects_modules
102 end 125 end
103 if !initialized.key?('trackers') && !initialized.key?('tracker_ids') 126 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
104 self.trackers = Tracker.all 127 self.trackers = Tracker.sorted.all
105 end 128 end
106 end 129 end
107 130
108 def identifier=(identifier) 131 def identifier=(identifier)
109 super unless identifier_frozen? 132 super unless identifier_frozen?
110 end 133 end
111 134
112 def identifier_frozen? 135 def identifier_frozen?
113 errors[:identifier].nil? && !(new_record? || identifier.blank?) 136 errors[:identifier].blank? && !(new_record? || identifier.blank?)
114 end 137 end
115 138
116 # returns latest created projects 139 # returns latest created projects
117 # non public projects will be returned only if user is a member of those 140 # non public projects will be returned only if user is a member of those
118 def self.latest(user=nil, count=5) 141 def self.latest(user=nil, count=5)
143 # Valid options: 166 # Valid options:
144 # * :project => limit the condition to project 167 # * :project => limit the condition to project
145 # * :with_subprojects => limit the condition to project and its subprojects 168 # * :with_subprojects => limit the condition to project and its subprojects
146 # * :member => limit the condition to the user projects 169 # * :member => limit the condition to the user projects
147 def self.allowed_to_condition(user, permission, options={}) 170 def self.allowed_to_condition(user, permission, options={})
148 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" 171 perm = Redmine::AccessControl.permission(permission)
149 if perm = Redmine::AccessControl.permission(permission) 172 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
150 unless perm.project_module.nil? 173 if perm && perm.project_module
151 # If the permission belongs to a project module, make sure the module is enabled 174 # If the permission belongs to a project module, make sure the module is enabled
152 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" 175 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
153 end
154 end 176 end
155 if options[:project] 177 if options[:project]
156 project_statement = "#{Project.table_name}.id = #{options[:project].id}" 178 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
157 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects] 179 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
158 base_statement = "(#{project_statement}) AND (#{base_statement})" 180 base_statement = "(#{project_statement}) AND (#{base_statement})"
168 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}" 190 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
169 end 191 end
170 end 192 end
171 if user.logged? 193 if user.logged?
172 user.projects_by_role.each do |role, projects| 194 user.projects_by_role.each do |role, projects|
173 if role.allowed_to?(permission) 195 if role.allowed_to?(permission) && projects.any?
174 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})" 196 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
175 end 197 end
176 end 198 end
177 end 199 end
178 if statement_by_role.empty? 200 if statement_by_role.empty?
254 else 276 else
255 super 277 super
256 end 278 end
257 end 279 end
258 280
281 def self.find_by_param(*args)
282 self.find(*args)
283 end
284
285 def reload(*args)
286 @shared_versions = nil
287 @rolled_up_versions = nil
288 @rolled_up_trackers = nil
289 @all_issue_custom_fields = nil
290 @all_time_entry_custom_fields = nil
291 @to_param = nil
292 @allowed_parents = nil
293 @allowed_permissions = nil
294 @actions_allowed = nil
295 super
296 end
297
259 def to_param 298 def to_param
260 # id is used for projects with a numeric identifier (compatibility) 299 # id is used for projects with a numeric identifier (compatibility)
261 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier) 300 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
262 end 301 end
263 302
290 def unarchive 329 def unarchive
291 return false if ancestors.detect {|a| !a.active?} 330 return false if ancestors.detect {|a| !a.active?}
292 update_attribute :status, STATUS_ACTIVE 331 update_attribute :status, STATUS_ACTIVE
293 end 332 end
294 333
334 def close
335 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
336 end
337
338 def reopen
339 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
340 end
341
295 # Returns an array of projects the project can be moved to 342 # Returns an array of projects the project can be moved to
296 # by the current user 343 # by the current user
297 def allowed_parents 344 def allowed_parents
298 return @allowed_parents if @allowed_parents 345 return @allowed_parents if @allowed_parents
299 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects)) 346 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
340 end 387 end
341 if p == parent && !p.nil? 388 if p == parent && !p.nil?
342 # Nothing to do 389 # Nothing to do
343 true 390 true
344 elsif p.nil? || (p.active? && move_possible?(p)) 391 elsif p.nil? || (p.active? && move_possible?(p))
345 # Insert the project so that target's children or root projects stay alphabetically sorted 392 set_or_update_position_under(p)
346 sibs = (p.nil? ? self.class.roots : p.children)
347 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
348 if to_be_inserted_before
349 move_to_left_of(to_be_inserted_before)
350 elsif p.nil?
351 if sibs.empty?
352 # move_to_root adds the project in first (ie. left) position
353 move_to_root
354 else
355 move_to_right_of(sibs.last) unless self == sibs.last
356 end
357 else
358 # move_to_child_of adds the project in last (ie.right) position
359 move_to_child_of(p)
360 end
361 Issue.update_versions_from_hierarchy_change(self) 393 Issue.update_versions_from_hierarchy_change(self)
362 true 394 true
363 else 395 else
364 # Can not move to the given target 396 # Can not move to the given target
365 false 397 false
398 end
399 end
400
401 # Recalculates all lft and rgt values based on project names
402 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
403 # Used in BuildProjectsTree migration
404 def self.rebuild_tree!
405 transaction do
406 update_all "lft = NULL, rgt = NULL"
407 rebuild!(false)
366 end 408 end
367 end 409 end
368 410
369 # Returns an array of the trackers used by the project and its active sub projects 411 # Returns an array of the trackers used by the project and its active sub projects
370 def rolled_up_trackers 412 def rolled_up_trackers
371 @rolled_up_trackers ||= 413 @rolled_up_trackers ||=
372 Tracker.find(:all, :joins => :projects, 414 Tracker.find(:all, :joins => :projects,
373 :select => "DISTINCT #{Tracker.table_name}.*", 415 :select => "DISTINCT #{Tracker.table_name}.*",
374 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt], 416 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt],
375 :order => "#{Tracker.table_name}.position") 417 :order => "#{Tracker.table_name}.position")
376 end 418 end
377 419
378 # Closes open and locked project versions that are completed 420 # Closes open and locked project versions that are completed
379 def close_completed_versions 421 def close_completed_versions
388 430
389 # Returns a scope of the Versions on subprojects 431 # Returns a scope of the Versions on subprojects
390 def rolled_up_versions 432 def rolled_up_versions
391 @rolled_up_versions ||= 433 @rolled_up_versions ||=
392 Version.scoped(:include => :project, 434 Version.scoped(:include => :project,
393 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt]) 435 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
394 end 436 end
395 437
396 # Returns a scope of the Versions used by the project 438 # Returns a scope of the Versions used by the project
397 def shared_versions 439 def shared_versions
398 if new_record? 440 if new_record?
399 Version.scoped(:include => :project, 441 Version.scoped(:include => :project,
400 :conditions => "#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND #{Version.table_name}.sharing = 'system'") 442 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
401 else 443 else
402 @shared_versions ||= begin 444 @shared_versions ||= begin
403 r = root? ? self : root 445 r = root? ? self : root
404 Version.scoped(:include => :project, 446 Version.scoped(:include => :project,
405 :conditions => "#{Project.table_name}.id = #{id}" + 447 :conditions => "#{Project.table_name}.id = #{id}" +
406 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" + 448 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
407 " #{Version.table_name}.sharing = 'system'" + 449 " #{Version.table_name}.sharing = 'system'" +
408 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" + 450 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
409 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" + 451 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
410 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" + 452 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
411 "))") 453 "))")
443 end 485 end
444 486
445 # Returns the users that should be notified on project events 487 # Returns the users that should be notified on project events
446 def notified_users 488 def notified_users
447 # TODO: User part should be extracted to User#notify_about? 489 # TODO: User part should be extracted to User#notify_about?
448 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user} 490 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
449 end 491 end
450 492
451 # Returns an array of all custom fields enabled for project issues 493 # Returns an array of all custom fields enabled for project issues
452 # (explictly associated custom fields and custom fields enabled for all projects) 494 # (explictly associated custom fields and custom fields enabled for all projects)
453 def all_issue_custom_fields 495 def all_issue_custom_fields
471 def to_s 513 def to_s
472 name 514 name
473 end 515 end
474 516
475 # Returns a short description of the projects (first lines) 517 # Returns a short description of the projects (first lines)
476 def short_description(length = 255) 518 def short_description(length = 200)
477 519
478 ## The short description is used in lists, e.g. Latest projects, 520 ## The short description is used in lists, e.g. Latest projects,
479 ## My projects etc. It should be no more than a line or two with 521 ## My projects etc. It should be no more than a line or two with
480 ## no text formatting. 522 ## no text formatting.
481 523
486 ## That can leave too much text for us, and also we want to omit 528 ## That can leave too much text for us, and also we want to omit
487 ## images and the like. Truncate instead to the first CR that 529 ## images and the like. Truncate instead to the first CR that
488 ## follows _any_ non-blank text, and to the next word break beyond 530 ## follows _any_ non-blank text, and to the next word break beyond
489 ## "length" characters if the result is still longer than that. 531 ## "length" characters if the result is still longer than that.
490 ## 532 ##
491 description.gsub(/![^\s]+!/, '').gsub(/^(\s*[^\n\r]*).*$/m, '\1').gsub(/^(.{#{length}}\b).*$/m, '\1 ...').strip if description 533 description.gsub(/![^\s]+!/, '').gsub(/^(\s*[^\n\r]*).*$/m, '\1').gsub(/^(.{#{length}}[^\.;:,-]*).*$/m, '\1 ...').strip if description
492 end 534 end
493 535
494 def css_classes 536 def css_classes
495 s = 'project' 537 s = 'project'
496 s << ' root' if root? 538 s << ' root' if root?
497 s << ' child' if child? 539 s << ' child' if child?
498 s << (leaf? ? ' leaf' : ' parent') 540 s << (leaf? ? ' leaf' : ' parent')
541 unless active?
542 if archived?
543 s << ' archived'
544 else
545 s << ' closed'
546 end
547 end
499 s 548 s
500 end 549 end
501 550
502 # The earliest start date of a project, based on it's issues and versions 551 # The earliest start date of a project, based on it's issues and versions
503 def start_date 552 def start_date
537 100 586 100
538 end 587 end
539 end 588 end
540 end 589 end
541 590
542 # Return true if this project is allowed to do the specified action. 591 # Return true if this project allows to do the specified action.
543 # action can be: 592 # action can be:
544 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') 593 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
545 # * a permission Symbol (eg. :edit_project) 594 # * a permission Symbol (eg. :edit_project)
546 def allows_to?(action) 595 def allows_to?(action)
596 if archived?
597 # No action allowed on archived projects
598 return false
599 end
600 unless active? || Redmine::AccessControl.read_action?(action)
601 # No write action allowed on closed projects
602 return false
603 end
604 # No action allowed on disabled modules
547 if action.is_a? Hash 605 if action.is_a? Hash
548 allowed_actions.include? "#{action[:controller]}/#{action[:action]}" 606 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
549 else 607 else
550 allowed_permissions.include? action 608 allowed_permissions.include? action
551 end 609 end
691 749
692 # Copies wiki from +project+ 750 # Copies wiki from +project+
693 def copy_wiki(project) 751 def copy_wiki(project)
694 # Check that the source project has a wiki first 752 # Check that the source project has a wiki first
695 unless project.wiki.nil? 753 unless project.wiki.nil?
696 self.wiki ||= Wiki.new 754 wiki = self.wiki || Wiki.new
697 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id") 755 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
698 wiki_pages_map = {} 756 wiki_pages_map = {}
699 project.wiki.pages.each do |page| 757 project.wiki.pages.each do |page|
700 # Skip pages without content 758 # Skip pages without content
701 next if page.content.nil? 759 next if page.content.nil?
703 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id")) 761 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
704 new_wiki_page.content = new_wiki_content 762 new_wiki_page.content = new_wiki_content
705 wiki.pages << new_wiki_page 763 wiki.pages << new_wiki_page
706 wiki_pages_map[page.id] = new_wiki_page 764 wiki_pages_map[page.id] = new_wiki_page
707 end 765 end
766
767 self.wiki = wiki
708 wiki.save 768 wiki.save
709 # Reproduce page hierarchy 769 # Reproduce page hierarchy
710 project.wiki.pages.each do |page| 770 project.wiki.pages.each do |page|
711 if page.parent_id && wiki_pages_map[page.id] 771 if page.parent_id && wiki_pages_map[page.id]
712 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id] 772 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
733 self.issue_categories << new_issue_category 793 self.issue_categories << new_issue_category
734 end 794 end
735 end 795 end
736 796
737 # Copies issues from +project+ 797 # Copies issues from +project+
738 # Note: issues assigned to a closed version won't be copied due to validation rules
739 def copy_issues(project) 798 def copy_issues(project)
740 # Stores the source issue id as a key and the copied issues as the 799 # Stores the source issue id as a key and the copied issues as the
741 # value. Used to map the two togeather for issue relations. 800 # value. Used to map the two togeather for issue relations.
742 issues_map = {} 801 issues_map = {}
743 802
803 # Store status and reopen locked/closed versions
804 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
805 version_statuses.each do |version, status|
806 version.update_attribute :status, 'open'
807 end
808
744 # Get issues sorted by root_id, lft so that parent issues 809 # Get issues sorted by root_id, lft so that parent issues
745 # get copied before their children 810 # get copied before their children
746 project.issues.find(:all, :order => 'root_id, lft').each do |issue| 811 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
747 new_issue = Issue.new 812 new_issue = Issue.new
748 new_issue.copy_from(issue) 813 new_issue.copy_from(issue, :subtasks => false, :link => false)
749 new_issue.project = self 814 new_issue.project = self
750 # Reassign fixed_versions by name, since names are unique per 815 # Reassign fixed_versions by name, since names are unique per project
751 # project and the versions for self are not yet saved 816 if issue.fixed_version && issue.fixed_version.project == project
752 if issue.fixed_version 817 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
753 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first 818 end
754 end 819 # Reassign the category by name, since names are unique per project
755 # Reassign the category by name, since names are unique per
756 # project and the categories for self are not yet saved
757 if issue.category 820 if issue.category
758 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first 821 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
759 end 822 end
760 # Parent issue 823 # Parent issue
761 if issue.parent_id 824 if issue.parent_id
762 if copied_parent = issues_map[issue.parent_id] 825 if copied_parent = issues_map[issue.parent_id]
763 new_issue.parent_issue_id = copied_parent.id 826 new_issue.parent_issue_id = copied_parent.id
768 if new_issue.new_record? 831 if new_issue.new_record?
769 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info 832 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
770 else 833 else
771 issues_map[issue.id] = new_issue unless new_issue.new_record? 834 issues_map[issue.id] = new_issue unless new_issue.new_record?
772 end 835 end
836 end
837
838 # Restore locked/closed version statuses
839 version_statuses.each do |version, status|
840 version.update_attribute :status, status
773 end 841 end
774 842
775 # Relations after in case issues related each other 843 # Relations after in case issues related each other
776 project.issues.each do |issue| 844 project.issues.each do |issue|
777 new_issue = issues_map[issue.id] 845 new_issue = issues_map[issue.id]
824 end 892 end
825 893
826 # Copies queries from +project+ 894 # Copies queries from +project+
827 def copy_queries(project) 895 def copy_queries(project)
828 project.queries.each do |query| 896 project.queries.each do |query|
829 new_query = Query.new 897 new_query = ::Query.new
830 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria") 898 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
831 new_query.sort_criteria = query.sort_criteria if query.sort_criteria 899 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
832 new_query.project = self 900 new_query.project = self
833 new_query.user_id = query.user_id 901 new_query.user_id = query.user_id
834 self.queries << new_query 902 self.queries << new_query
899 children.each do |subproject| 967 children.each do |subproject|
900 subproject.send :archive! 968 subproject.send :archive!
901 end 969 end
902 update_attribute :status, STATUS_ARCHIVED 970 update_attribute :status, STATUS_ARCHIVED
903 end 971 end
972
973 def update_position_under_parent
974 set_or_update_position_under(parent)
975 end
976
977 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
978 def set_or_update_position_under(target_parent)
979 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
980 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
981
982 if to_be_inserted_before
983 move_to_left_of(to_be_inserted_before)
984 elsif target_parent.nil?
985 if sibs.empty?
986 # move_to_root adds the project in first (ie. left) position
987 move_to_root
988 else
989 move_to_right_of(sibs.last) unless self == sibs.last
990 end
991 else
992 # move_to_child_of adds the project in last (ie.right) position
993 move_to_child_of(target_parent)
994 end
995 end
904 end 996 end