##// END OF EJS Templates
Fixed that values of custom fields are not kept in issues when copying a project (#13910)....
Jean-Philippe Lang -
r11613:ece5232110d0
parent child
Show More
@@ -1,1034 +1,1037
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
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_CLOSED = 5
24 STATUS_ARCHIVED = 9
24 STATUS_ARCHIVED = 9
25
25
26 # Maximum length for project identifiers
26 # Maximum length for project identifiers
27 IDENTIFIER_MAX_LENGTH = 100
27 IDENTIFIER_MAX_LENGTH = 100
28
28
29 # Specific overidden Activities
29 # Specific overidden Activities
30 has_many :time_entry_activities
30 has_many :time_entry_activities
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 has_many :memberships, :class_name => 'Member'
32 has_many :memberships, :class_name => 'Member'
33 has_many :member_principals, :class_name => 'Member',
33 has_many :member_principals, :class_name => 'Member',
34 :include => :principal,
34 :include => :principal,
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
36
36
37 has_many :enabled_modules, :dependent => :delete_all
37 has_many :enabled_modules, :dependent => :delete_all
38 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
38 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
39 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
40 has_many :issue_changes, :through => :issues, :source => :journals
40 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
41 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :time_entries, :dependent => :delete_all
42 has_many :time_entries, :dependent => :delete_all
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
44 has_many :documents, :dependent => :destroy
44 has_many :documents, :dependent => :destroy
45 has_many :news, :dependent => :destroy, :include => :author
45 has_many :news, :dependent => :destroy, :include => :author
46 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
46 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :boards, :dependent => :destroy, :order => "position ASC"
47 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_one :repository, :conditions => ["is_default = ?", true]
48 has_one :repository, :conditions => ["is_default = ?", true]
49 has_many :repositories, :dependent => :destroy
49 has_many :repositories, :dependent => :destroy
50 has_many :changesets, :through => :repository
50 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy
51 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues
52 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields,
53 has_and_belongs_to_many :issue_custom_fields,
54 :class_name => 'IssueCustomField',
54 :class_name => 'IssueCustomField',
55 :order => "#{CustomField.table_name}.position",
55 :order => "#{CustomField.table_name}.position",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :association_foreign_key => 'custom_field_id'
57 :association_foreign_key => 'custom_field_id'
58
58
59 acts_as_nested_set :order => 'name', :dependent => :destroy
59 acts_as_nested_set :order => 'name', :dependent => :destroy
60 acts_as_attachable :view_permission => :view_files,
60 acts_as_attachable :view_permission => :view_files,
61 :delete_permission => :manage_files
61 :delete_permission => :manage_files
62
62
63 acts_as_customizable
63 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
67 :author => nil
68
68
69 attr_protected :status
69 attr_protected :status
70
70
71 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier
72 validates_uniqueness_of :identifier
73 validates_associated :repository, :wiki
73 validates_associated :repository, :wiki
74 validates_length_of :name, :maximum => 255
74 validates_length_of :name, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 # donwcase letters, digits, dashes but not digits only
77 # donwcase letters, digits, dashes but not digits only
78 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
78 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
79 # reserved words
79 # reserved words
80 validates_exclusion_of :identifier, :in => %w( new )
80 validates_exclusion_of :identifier, :in => %w( new )
81
81
82 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
82 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
83 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
83 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
84 before_destroy :delete_all_members
84 before_destroy :delete_all_members
85
85
86 scope :has_module, lambda {|mod|
86 scope :has_module, lambda {|mod|
87 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
87 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
88 }
88 }
89 scope :active, lambda { where(:status => STATUS_ACTIVE) }
89 scope :active, lambda { where(:status => STATUS_ACTIVE) }
90 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
90 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
91 scope :all_public, lambda { where(:is_public => true) }
91 scope :all_public, lambda { where(:is_public => true) }
92 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
92 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
93 scope :allowed_to, lambda {|*args|
93 scope :allowed_to, lambda {|*args|
94 user = User.current
94 user = User.current
95 permission = nil
95 permission = nil
96 if args.first.is_a?(Symbol)
96 if args.first.is_a?(Symbol)
97 permission = args.shift
97 permission = args.shift
98 else
98 else
99 user = args.shift
99 user = args.shift
100 permission = args.shift
100 permission = args.shift
101 end
101 end
102 where(Project.allowed_to_condition(user, permission, *args))
102 where(Project.allowed_to_condition(user, permission, *args))
103 }
103 }
104 scope :like, lambda {|arg|
104 scope :like, lambda {|arg|
105 if arg.blank?
105 if arg.blank?
106 where(nil)
106 where(nil)
107 else
107 else
108 pattern = "%#{arg.to_s.strip.downcase}%"
108 pattern = "%#{arg.to_s.strip.downcase}%"
109 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
109 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
110 end
110 end
111 }
111 }
112
112
113 def initialize(attributes=nil, *args)
113 def initialize(attributes=nil, *args)
114 super
114 super
115
115
116 initialized = (attributes || {}).stringify_keys
116 initialized = (attributes || {}).stringify_keys
117 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
117 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
118 self.identifier = Project.next_identifier
118 self.identifier = Project.next_identifier
119 end
119 end
120 if !initialized.key?('is_public')
120 if !initialized.key?('is_public')
121 self.is_public = Setting.default_projects_public?
121 self.is_public = Setting.default_projects_public?
122 end
122 end
123 if !initialized.key?('enabled_module_names')
123 if !initialized.key?('enabled_module_names')
124 self.enabled_module_names = Setting.default_projects_modules
124 self.enabled_module_names = Setting.default_projects_modules
125 end
125 end
126 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
126 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
127 default = Setting.default_projects_tracker_ids
127 default = Setting.default_projects_tracker_ids
128 if default.is_a?(Array)
128 if default.is_a?(Array)
129 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
129 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
130 else
130 else
131 self.trackers = Tracker.sorted.all
131 self.trackers = Tracker.sorted.all
132 end
132 end
133 end
133 end
134 end
134 end
135
135
136 def identifier=(identifier)
136 def identifier=(identifier)
137 super unless identifier_frozen?
137 super unless identifier_frozen?
138 end
138 end
139
139
140 def identifier_frozen?
140 def identifier_frozen?
141 errors[:identifier].blank? && !(new_record? || identifier.blank?)
141 errors[:identifier].blank? && !(new_record? || identifier.blank?)
142 end
142 end
143
143
144 # returns latest created projects
144 # returns latest created projects
145 # non public projects will be returned only if user is a member of those
145 # non public projects will be returned only if user is a member of those
146 def self.latest(user=nil, count=5)
146 def self.latest(user=nil, count=5)
147 visible(user).limit(count).order("created_on DESC").all
147 visible(user).limit(count).order("created_on DESC").all
148 end
148 end
149
149
150 # Returns true if the project is visible to +user+ or to the current user.
150 # Returns true if the project is visible to +user+ or to the current user.
151 def visible?(user=User.current)
151 def visible?(user=User.current)
152 user.allowed_to?(:view_project, self)
152 user.allowed_to?(:view_project, self)
153 end
153 end
154
154
155 # Returns a SQL conditions string used to find all projects visible by the specified user.
155 # Returns a SQL conditions string used to find all projects visible by the specified user.
156 #
156 #
157 # Examples:
157 # Examples:
158 # Project.visible_condition(admin) => "projects.status = 1"
158 # Project.visible_condition(admin) => "projects.status = 1"
159 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
159 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
160 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
160 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
161 def self.visible_condition(user, options={})
161 def self.visible_condition(user, options={})
162 allowed_to_condition(user, :view_project, options)
162 allowed_to_condition(user, :view_project, options)
163 end
163 end
164
164
165 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
165 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
166 #
166 #
167 # Valid options:
167 # Valid options:
168 # * :project => limit the condition to project
168 # * :project => limit the condition to project
169 # * :with_subprojects => limit the condition to project and its subprojects
169 # * :with_subprojects => limit the condition to project and its subprojects
170 # * :member => limit the condition to the user projects
170 # * :member => limit the condition to the user projects
171 def self.allowed_to_condition(user, permission, options={})
171 def self.allowed_to_condition(user, permission, options={})
172 perm = Redmine::AccessControl.permission(permission)
172 perm = Redmine::AccessControl.permission(permission)
173 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
173 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
174 if perm && perm.project_module
174 if perm && perm.project_module
175 # If the permission belongs to a project module, make sure the module is enabled
175 # If the permission belongs to a project module, make sure the module is enabled
176 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
176 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
177 end
177 end
178 if options[:project]
178 if options[:project]
179 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
179 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
180 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
180 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
181 base_statement = "(#{project_statement}) AND (#{base_statement})"
181 base_statement = "(#{project_statement}) AND (#{base_statement})"
182 end
182 end
183
183
184 if user.admin?
184 if user.admin?
185 base_statement
185 base_statement
186 else
186 else
187 statement_by_role = {}
187 statement_by_role = {}
188 unless options[:member]
188 unless options[:member]
189 role = user.logged? ? Role.non_member : Role.anonymous
189 role = user.logged? ? Role.non_member : Role.anonymous
190 if role.allowed_to?(permission)
190 if role.allowed_to?(permission)
191 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
191 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
192 end
192 end
193 end
193 end
194 if user.logged?
194 if user.logged?
195 user.projects_by_role.each do |role, projects|
195 user.projects_by_role.each do |role, projects|
196 if role.allowed_to?(permission) && projects.any?
196 if role.allowed_to?(permission) && projects.any?
197 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
197 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
198 end
198 end
199 end
199 end
200 end
200 end
201 if statement_by_role.empty?
201 if statement_by_role.empty?
202 "1=0"
202 "1=0"
203 else
203 else
204 if block_given?
204 if block_given?
205 statement_by_role.each do |role, statement|
205 statement_by_role.each do |role, statement|
206 if s = yield(role, user)
206 if s = yield(role, user)
207 statement_by_role[role] = "(#{statement} AND (#{s}))"
207 statement_by_role[role] = "(#{statement} AND (#{s}))"
208 end
208 end
209 end
209 end
210 end
210 end
211 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
211 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
212 end
212 end
213 end
213 end
214 end
214 end
215
215
216 def principals
216 def principals
217 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
217 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
218 end
218 end
219
219
220 def users
220 def users
221 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
221 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
222 end
222 end
223
223
224 # Returns the Systemwide and project specific activities
224 # Returns the Systemwide and project specific activities
225 def activities(include_inactive=false)
225 def activities(include_inactive=false)
226 if include_inactive
226 if include_inactive
227 return all_activities
227 return all_activities
228 else
228 else
229 return active_activities
229 return active_activities
230 end
230 end
231 end
231 end
232
232
233 # Will create a new Project specific Activity or update an existing one
233 # Will create a new Project specific Activity or update an existing one
234 #
234 #
235 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
235 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
236 # does not successfully save.
236 # does not successfully save.
237 def update_or_create_time_entry_activity(id, activity_hash)
237 def update_or_create_time_entry_activity(id, activity_hash)
238 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
238 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
239 self.create_time_entry_activity_if_needed(activity_hash)
239 self.create_time_entry_activity_if_needed(activity_hash)
240 else
240 else
241 activity = project.time_entry_activities.find_by_id(id.to_i)
241 activity = project.time_entry_activities.find_by_id(id.to_i)
242 activity.update_attributes(activity_hash) if activity
242 activity.update_attributes(activity_hash) if activity
243 end
243 end
244 end
244 end
245
245
246 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
246 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
247 #
247 #
248 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
248 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
249 # does not successfully save.
249 # does not successfully save.
250 def create_time_entry_activity_if_needed(activity)
250 def create_time_entry_activity_if_needed(activity)
251 if activity['parent_id']
251 if activity['parent_id']
252
252
253 parent_activity = TimeEntryActivity.find(activity['parent_id'])
253 parent_activity = TimeEntryActivity.find(activity['parent_id'])
254 activity['name'] = parent_activity.name
254 activity['name'] = parent_activity.name
255 activity['position'] = parent_activity.position
255 activity['position'] = parent_activity.position
256
256
257 if Enumeration.overridding_change?(activity, parent_activity)
257 if Enumeration.overridding_change?(activity, parent_activity)
258 project_activity = self.time_entry_activities.create(activity)
258 project_activity = self.time_entry_activities.create(activity)
259
259
260 if project_activity.new_record?
260 if project_activity.new_record?
261 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
261 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
262 else
262 else
263 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
263 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
264 end
264 end
265 end
265 end
266 end
266 end
267 end
267 end
268
268
269 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
269 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
270 #
270 #
271 # Examples:
271 # Examples:
272 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
272 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
273 # project.project_condition(false) => "projects.id = 1"
273 # project.project_condition(false) => "projects.id = 1"
274 def project_condition(with_subprojects)
274 def project_condition(with_subprojects)
275 cond = "#{Project.table_name}.id = #{id}"
275 cond = "#{Project.table_name}.id = #{id}"
276 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
276 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
277 cond
277 cond
278 end
278 end
279
279
280 def self.find(*args)
280 def self.find(*args)
281 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
281 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
282 project = find_by_identifier(*args)
282 project = find_by_identifier(*args)
283 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
283 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
284 project
284 project
285 else
285 else
286 super
286 super
287 end
287 end
288 end
288 end
289
289
290 def self.find_by_param(*args)
290 def self.find_by_param(*args)
291 self.find(*args)
291 self.find(*args)
292 end
292 end
293
293
294 alias :base_reload :reload
294 alias :base_reload :reload
295 def reload(*args)
295 def reload(*args)
296 @principals = nil
296 @principals = nil
297 @users = nil
297 @users = nil
298 @shared_versions = nil
298 @shared_versions = nil
299 @rolled_up_versions = nil
299 @rolled_up_versions = nil
300 @rolled_up_trackers = nil
300 @rolled_up_trackers = nil
301 @all_issue_custom_fields = nil
301 @all_issue_custom_fields = nil
302 @all_time_entry_custom_fields = nil
302 @all_time_entry_custom_fields = nil
303 @to_param = nil
303 @to_param = nil
304 @allowed_parents = nil
304 @allowed_parents = nil
305 @allowed_permissions = nil
305 @allowed_permissions = nil
306 @actions_allowed = nil
306 @actions_allowed = nil
307 @start_date = nil
307 @start_date = nil
308 @due_date = nil
308 @due_date = nil
309 base_reload(*args)
309 base_reload(*args)
310 end
310 end
311
311
312 def to_param
312 def to_param
313 # id is used for projects with a numeric identifier (compatibility)
313 # id is used for projects with a numeric identifier (compatibility)
314 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
314 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
315 end
315 end
316
316
317 def active?
317 def active?
318 self.status == STATUS_ACTIVE
318 self.status == STATUS_ACTIVE
319 end
319 end
320
320
321 def archived?
321 def archived?
322 self.status == STATUS_ARCHIVED
322 self.status == STATUS_ARCHIVED
323 end
323 end
324
324
325 # Archives the project and its descendants
325 # Archives the project and its descendants
326 def archive
326 def archive
327 # Check that there is no issue of a non descendant project that is assigned
327 # Check that there is no issue of a non descendant project that is assigned
328 # to one of the project or descendant versions
328 # to one of the project or descendant versions
329 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
329 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
330 if v_ids.any? &&
330 if v_ids.any? &&
331 Issue.
331 Issue.
332 includes(:project).
332 includes(:project).
333 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
333 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
334 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
334 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
335 exists?
335 exists?
336 return false
336 return false
337 end
337 end
338 Project.transaction do
338 Project.transaction do
339 archive!
339 archive!
340 end
340 end
341 true
341 true
342 end
342 end
343
343
344 # Unarchives the project
344 # Unarchives the project
345 # All its ancestors must be active
345 # All its ancestors must be active
346 def unarchive
346 def unarchive
347 return false if ancestors.detect {|a| !a.active?}
347 return false if ancestors.detect {|a| !a.active?}
348 update_attribute :status, STATUS_ACTIVE
348 update_attribute :status, STATUS_ACTIVE
349 end
349 end
350
350
351 def close
351 def close
352 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
352 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
353 end
353 end
354
354
355 def reopen
355 def reopen
356 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
356 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
357 end
357 end
358
358
359 # Returns an array of projects the project can be moved to
359 # Returns an array of projects the project can be moved to
360 # by the current user
360 # by the current user
361 def allowed_parents
361 def allowed_parents
362 return @allowed_parents if @allowed_parents
362 return @allowed_parents if @allowed_parents
363 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
363 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
364 @allowed_parents = @allowed_parents - self_and_descendants
364 @allowed_parents = @allowed_parents - self_and_descendants
365 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
365 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
366 @allowed_parents << nil
366 @allowed_parents << nil
367 end
367 end
368 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
368 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
369 @allowed_parents << parent
369 @allowed_parents << parent
370 end
370 end
371 @allowed_parents
371 @allowed_parents
372 end
372 end
373
373
374 # Sets the parent of the project with authorization check
374 # Sets the parent of the project with authorization check
375 def set_allowed_parent!(p)
375 def set_allowed_parent!(p)
376 unless p.nil? || p.is_a?(Project)
376 unless p.nil? || p.is_a?(Project)
377 if p.to_s.blank?
377 if p.to_s.blank?
378 p = nil
378 p = nil
379 else
379 else
380 p = Project.find_by_id(p)
380 p = Project.find_by_id(p)
381 return false unless p
381 return false unless p
382 end
382 end
383 end
383 end
384 if p.nil?
384 if p.nil?
385 if !new_record? && allowed_parents.empty?
385 if !new_record? && allowed_parents.empty?
386 return false
386 return false
387 end
387 end
388 elsif !allowed_parents.include?(p)
388 elsif !allowed_parents.include?(p)
389 return false
389 return false
390 end
390 end
391 set_parent!(p)
391 set_parent!(p)
392 end
392 end
393
393
394 # Sets the parent of the project
394 # Sets the parent of the project
395 # Argument can be either a Project, a String, a Fixnum or nil
395 # Argument can be either a Project, a String, a Fixnum or nil
396 def set_parent!(p)
396 def set_parent!(p)
397 unless p.nil? || p.is_a?(Project)
397 unless p.nil? || p.is_a?(Project)
398 if p.to_s.blank?
398 if p.to_s.blank?
399 p = nil
399 p = nil
400 else
400 else
401 p = Project.find_by_id(p)
401 p = Project.find_by_id(p)
402 return false unless p
402 return false unless p
403 end
403 end
404 end
404 end
405 if p == parent && !p.nil?
405 if p == parent && !p.nil?
406 # Nothing to do
406 # Nothing to do
407 true
407 true
408 elsif p.nil? || (p.active? && move_possible?(p))
408 elsif p.nil? || (p.active? && move_possible?(p))
409 set_or_update_position_under(p)
409 set_or_update_position_under(p)
410 Issue.update_versions_from_hierarchy_change(self)
410 Issue.update_versions_from_hierarchy_change(self)
411 true
411 true
412 else
412 else
413 # Can not move to the given target
413 # Can not move to the given target
414 false
414 false
415 end
415 end
416 end
416 end
417
417
418 # Recalculates all lft and rgt values based on project names
418 # Recalculates all lft and rgt values based on project names
419 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
419 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
420 # Used in BuildProjectsTree migration
420 # Used in BuildProjectsTree migration
421 def self.rebuild_tree!
421 def self.rebuild_tree!
422 transaction do
422 transaction do
423 update_all "lft = NULL, rgt = NULL"
423 update_all "lft = NULL, rgt = NULL"
424 rebuild!(false)
424 rebuild!(false)
425 end
425 end
426 end
426 end
427
427
428 # Returns an array of the trackers used by the project and its active sub projects
428 # Returns an array of the trackers used by the project and its active sub projects
429 def rolled_up_trackers
429 def rolled_up_trackers
430 @rolled_up_trackers ||=
430 @rolled_up_trackers ||=
431 Tracker.
431 Tracker.
432 joins(:projects).
432 joins(:projects).
433 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
433 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
434 select("DISTINCT #{Tracker.table_name}.*").
434 select("DISTINCT #{Tracker.table_name}.*").
435 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
435 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
436 sorted.
436 sorted.
437 all
437 all
438 end
438 end
439
439
440 # Closes open and locked project versions that are completed
440 # Closes open and locked project versions that are completed
441 def close_completed_versions
441 def close_completed_versions
442 Version.transaction do
442 Version.transaction do
443 versions.where(:status => %w(open locked)).all.each do |version|
443 versions.where(:status => %w(open locked)).all.each do |version|
444 if version.completed?
444 if version.completed?
445 version.update_attribute(:status, 'closed')
445 version.update_attribute(:status, 'closed')
446 end
446 end
447 end
447 end
448 end
448 end
449 end
449 end
450
450
451 # Returns a scope of the Versions on subprojects
451 # Returns a scope of the Versions on subprojects
452 def rolled_up_versions
452 def rolled_up_versions
453 @rolled_up_versions ||=
453 @rolled_up_versions ||=
454 Version.scoped(:include => :project,
454 Version.scoped(:include => :project,
455 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
455 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
456 end
456 end
457
457
458 # Returns a scope of the Versions used by the project
458 # Returns a scope of the Versions used by the project
459 def shared_versions
459 def shared_versions
460 if new_record?
460 if new_record?
461 Version.scoped(:include => :project,
461 Version.scoped(:include => :project,
462 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
462 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
463 else
463 else
464 @shared_versions ||= begin
464 @shared_versions ||= begin
465 r = root? ? self : root
465 r = root? ? self : root
466 Version.scoped(:include => :project,
466 Version.scoped(:include => :project,
467 :conditions => "#{Project.table_name}.id = #{id}" +
467 :conditions => "#{Project.table_name}.id = #{id}" +
468 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
468 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
469 " #{Version.table_name}.sharing = 'system'" +
469 " #{Version.table_name}.sharing = 'system'" +
470 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
470 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
471 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
471 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
472 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
472 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
473 "))")
473 "))")
474 end
474 end
475 end
475 end
476 end
476 end
477
477
478 # Returns a hash of project users grouped by role
478 # Returns a hash of project users grouped by role
479 def users_by_role
479 def users_by_role
480 members.includes(:user, :roles).all.inject({}) do |h, m|
480 members.includes(:user, :roles).all.inject({}) do |h, m|
481 m.roles.each do |r|
481 m.roles.each do |r|
482 h[r] ||= []
482 h[r] ||= []
483 h[r] << m.user
483 h[r] << m.user
484 end
484 end
485 h
485 h
486 end
486 end
487 end
487 end
488
488
489 # Deletes all project's members
489 # Deletes all project's members
490 def delete_all_members
490 def delete_all_members
491 me, mr = Member.table_name, MemberRole.table_name
491 me, mr = Member.table_name, MemberRole.table_name
492 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
492 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
493 Member.delete_all(['project_id = ?', id])
493 Member.delete_all(['project_id = ?', id])
494 end
494 end
495
495
496 # Users/groups issues can be assigned to
496 # Users/groups issues can be assigned to
497 def assignable_users
497 def assignable_users
498 assignable = Setting.issue_group_assignment? ? member_principals : members
498 assignable = Setting.issue_group_assignment? ? member_principals : members
499 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
499 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
500 end
500 end
501
501
502 # Returns the mail adresses of users that should be always notified on project events
502 # Returns the mail adresses of users that should be always notified on project events
503 def recipients
503 def recipients
504 notified_users.collect {|user| user.mail}
504 notified_users.collect {|user| user.mail}
505 end
505 end
506
506
507 # Returns the users that should be notified on project events
507 # Returns the users that should be notified on project events
508 def notified_users
508 def notified_users
509 # TODO: User part should be extracted to User#notify_about?
509 # TODO: User part should be extracted to User#notify_about?
510 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
510 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
511 end
511 end
512
512
513 # Returns an array of all custom fields enabled for project issues
513 # Returns an array of all custom fields enabled for project issues
514 # (explictly associated custom fields and custom fields enabled for all projects)
514 # (explictly associated custom fields and custom fields enabled for all projects)
515 def all_issue_custom_fields
515 def all_issue_custom_fields
516 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
516 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
517 end
517 end
518
518
519 # Returns an array of all custom fields enabled for project time entries
519 # Returns an array of all custom fields enabled for project time entries
520 # (explictly associated custom fields and custom fields enabled for all projects)
520 # (explictly associated custom fields and custom fields enabled for all projects)
521 def all_time_entry_custom_fields
521 def all_time_entry_custom_fields
522 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
522 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
523 end
523 end
524
524
525 def project
525 def project
526 self
526 self
527 end
527 end
528
528
529 def <=>(project)
529 def <=>(project)
530 name.downcase <=> project.name.downcase
530 name.downcase <=> project.name.downcase
531 end
531 end
532
532
533 def to_s
533 def to_s
534 name
534 name
535 end
535 end
536
536
537 # Returns a short description of the projects (first lines)
537 # Returns a short description of the projects (first lines)
538 def short_description(length = 255)
538 def short_description(length = 255)
539 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
539 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
540 end
540 end
541
541
542 def css_classes
542 def css_classes
543 s = 'project'
543 s = 'project'
544 s << ' root' if root?
544 s << ' root' if root?
545 s << ' child' if child?
545 s << ' child' if child?
546 s << (leaf? ? ' leaf' : ' parent')
546 s << (leaf? ? ' leaf' : ' parent')
547 unless active?
547 unless active?
548 if archived?
548 if archived?
549 s << ' archived'
549 s << ' archived'
550 else
550 else
551 s << ' closed'
551 s << ' closed'
552 end
552 end
553 end
553 end
554 s
554 s
555 end
555 end
556
556
557 # The earliest start date of a project, based on it's issues and versions
557 # The earliest start date of a project, based on it's issues and versions
558 def start_date
558 def start_date
559 @start_date ||= [
559 @start_date ||= [
560 issues.minimum('start_date'),
560 issues.minimum('start_date'),
561 shared_versions.minimum('effective_date'),
561 shared_versions.minimum('effective_date'),
562 Issue.fixed_version(shared_versions).minimum('start_date')
562 Issue.fixed_version(shared_versions).minimum('start_date')
563 ].compact.min
563 ].compact.min
564 end
564 end
565
565
566 # The latest due date of an issue or version
566 # The latest due date of an issue or version
567 def due_date
567 def due_date
568 @due_date ||= [
568 @due_date ||= [
569 issues.maximum('due_date'),
569 issues.maximum('due_date'),
570 shared_versions.maximum('effective_date'),
570 shared_versions.maximum('effective_date'),
571 Issue.fixed_version(shared_versions).maximum('due_date')
571 Issue.fixed_version(shared_versions).maximum('due_date')
572 ].compact.max
572 ].compact.max
573 end
573 end
574
574
575 def overdue?
575 def overdue?
576 active? && !due_date.nil? && (due_date < Date.today)
576 active? && !due_date.nil? && (due_date < Date.today)
577 end
577 end
578
578
579 # Returns the percent completed for this project, based on the
579 # Returns the percent completed for this project, based on the
580 # progress on it's versions.
580 # progress on it's versions.
581 def completed_percent(options={:include_subprojects => false})
581 def completed_percent(options={:include_subprojects => false})
582 if options.delete(:include_subprojects)
582 if options.delete(:include_subprojects)
583 total = self_and_descendants.collect(&:completed_percent).sum
583 total = self_and_descendants.collect(&:completed_percent).sum
584
584
585 total / self_and_descendants.count
585 total / self_and_descendants.count
586 else
586 else
587 if versions.count > 0
587 if versions.count > 0
588 total = versions.collect(&:completed_percent).sum
588 total = versions.collect(&:completed_percent).sum
589
589
590 total / versions.count
590 total / versions.count
591 else
591 else
592 100
592 100
593 end
593 end
594 end
594 end
595 end
595 end
596
596
597 # Return true if this project allows to do the specified action.
597 # Return true if this project allows to do the specified action.
598 # action can be:
598 # action can be:
599 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
599 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
600 # * a permission Symbol (eg. :edit_project)
600 # * a permission Symbol (eg. :edit_project)
601 def allows_to?(action)
601 def allows_to?(action)
602 if archived?
602 if archived?
603 # No action allowed on archived projects
603 # No action allowed on archived projects
604 return false
604 return false
605 end
605 end
606 unless active? || Redmine::AccessControl.read_action?(action)
606 unless active? || Redmine::AccessControl.read_action?(action)
607 # No write action allowed on closed projects
607 # No write action allowed on closed projects
608 return false
608 return false
609 end
609 end
610 # No action allowed on disabled modules
610 # No action allowed on disabled modules
611 if action.is_a? Hash
611 if action.is_a? Hash
612 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
612 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
613 else
613 else
614 allowed_permissions.include? action
614 allowed_permissions.include? action
615 end
615 end
616 end
616 end
617
617
618 def module_enabled?(module_name)
618 def module_enabled?(module_name)
619 module_name = module_name.to_s
619 module_name = module_name.to_s
620 enabled_modules.detect {|m| m.name == module_name}
620 enabled_modules.detect {|m| m.name == module_name}
621 end
621 end
622
622
623 def enabled_module_names=(module_names)
623 def enabled_module_names=(module_names)
624 if module_names && module_names.is_a?(Array)
624 if module_names && module_names.is_a?(Array)
625 module_names = module_names.collect(&:to_s).reject(&:blank?)
625 module_names = module_names.collect(&:to_s).reject(&:blank?)
626 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
626 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
627 else
627 else
628 enabled_modules.clear
628 enabled_modules.clear
629 end
629 end
630 end
630 end
631
631
632 # Returns an array of the enabled modules names
632 # Returns an array of the enabled modules names
633 def enabled_module_names
633 def enabled_module_names
634 enabled_modules.collect(&:name)
634 enabled_modules.collect(&:name)
635 end
635 end
636
636
637 # Enable a specific module
637 # Enable a specific module
638 #
638 #
639 # Examples:
639 # Examples:
640 # project.enable_module!(:issue_tracking)
640 # project.enable_module!(:issue_tracking)
641 # project.enable_module!("issue_tracking")
641 # project.enable_module!("issue_tracking")
642 def enable_module!(name)
642 def enable_module!(name)
643 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
643 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
644 end
644 end
645
645
646 # Disable a module if it exists
646 # Disable a module if it exists
647 #
647 #
648 # Examples:
648 # Examples:
649 # project.disable_module!(:issue_tracking)
649 # project.disable_module!(:issue_tracking)
650 # project.disable_module!("issue_tracking")
650 # project.disable_module!("issue_tracking")
651 # project.disable_module!(project.enabled_modules.first)
651 # project.disable_module!(project.enabled_modules.first)
652 def disable_module!(target)
652 def disable_module!(target)
653 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
653 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
654 target.destroy unless target.blank?
654 target.destroy unless target.blank?
655 end
655 end
656
656
657 safe_attributes 'name',
657 safe_attributes 'name',
658 'description',
658 'description',
659 'homepage',
659 'homepage',
660 'is_public',
660 'is_public',
661 'identifier',
661 'identifier',
662 'custom_field_values',
662 'custom_field_values',
663 'custom_fields',
663 'custom_fields',
664 'tracker_ids',
664 'tracker_ids',
665 'issue_custom_field_ids'
665 'issue_custom_field_ids'
666
666
667 safe_attributes 'enabled_module_names',
667 safe_attributes 'enabled_module_names',
668 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
668 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
669
669
670 safe_attributes 'inherit_members',
670 safe_attributes 'inherit_members',
671 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
671 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
672
672
673 # Returns an array of projects that are in this project's hierarchy
673 # Returns an array of projects that are in this project's hierarchy
674 #
674 #
675 # Example: parents, children, siblings
675 # Example: parents, children, siblings
676 def hierarchy
676 def hierarchy
677 parents = project.self_and_ancestors || []
677 parents = project.self_and_ancestors || []
678 descendants = project.descendants || []
678 descendants = project.descendants || []
679 project_hierarchy = parents | descendants # Set union
679 project_hierarchy = parents | descendants # Set union
680 end
680 end
681
681
682 # Returns an auto-generated project identifier based on the last identifier used
682 # Returns an auto-generated project identifier based on the last identifier used
683 def self.next_identifier
683 def self.next_identifier
684 p = Project.order('created_on DESC').first
684 p = Project.order('created_on DESC').first
685 p.nil? ? nil : p.identifier.to_s.succ
685 p.nil? ? nil : p.identifier.to_s.succ
686 end
686 end
687
687
688 # Copies and saves the Project instance based on the +project+.
688 # Copies and saves the Project instance based on the +project+.
689 # Duplicates the source project's:
689 # Duplicates the source project's:
690 # * Wiki
690 # * Wiki
691 # * Versions
691 # * Versions
692 # * Categories
692 # * Categories
693 # * Issues
693 # * Issues
694 # * Members
694 # * Members
695 # * Queries
695 # * Queries
696 #
696 #
697 # Accepts an +options+ argument to specify what to copy
697 # Accepts an +options+ argument to specify what to copy
698 #
698 #
699 # Examples:
699 # Examples:
700 # project.copy(1) # => copies everything
700 # project.copy(1) # => copies everything
701 # project.copy(1, :only => 'members') # => copies members only
701 # project.copy(1, :only => 'members') # => copies members only
702 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
702 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
703 def copy(project, options={})
703 def copy(project, options={})
704 project = project.is_a?(Project) ? project : Project.find(project)
704 project = project.is_a?(Project) ? project : Project.find(project)
705
705
706 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
706 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
707 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
707 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
708
708
709 Project.transaction do
709 Project.transaction do
710 if save
710 if save
711 reload
711 reload
712 to_be_copied.each do |name|
712 to_be_copied.each do |name|
713 send "copy_#{name}", project
713 send "copy_#{name}", project
714 end
714 end
715 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
715 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
716 save
716 save
717 end
717 end
718 end
718 end
719 end
719 end
720
720
721 # Returns a new unsaved Project instance with attributes copied from +project+
721 # Returns a new unsaved Project instance with attributes copied from +project+
722 def self.copy_from(project)
722 def self.copy_from(project)
723 project = project.is_a?(Project) ? project : Project.find(project)
723 project = project.is_a?(Project) ? project : Project.find(project)
724 # clear unique attributes
724 # clear unique attributes
725 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
725 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
726 copy = Project.new(attributes)
726 copy = Project.new(attributes)
727 copy.enabled_modules = project.enabled_modules
727 copy.enabled_modules = project.enabled_modules
728 copy.trackers = project.trackers
728 copy.trackers = project.trackers
729 copy.custom_values = project.custom_values.collect {|v| v.clone}
729 copy.custom_values = project.custom_values.collect {|v| v.clone}
730 copy.issue_custom_fields = project.issue_custom_fields
730 copy.issue_custom_fields = project.issue_custom_fields
731 copy
731 copy
732 end
732 end
733
733
734 # Yields the given block for each project with its level in the tree
734 # Yields the given block for each project with its level in the tree
735 def self.project_tree(projects, &block)
735 def self.project_tree(projects, &block)
736 ancestors = []
736 ancestors = []
737 projects.sort_by(&:lft).each do |project|
737 projects.sort_by(&:lft).each do |project|
738 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
738 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
739 ancestors.pop
739 ancestors.pop
740 end
740 end
741 yield project, ancestors.size
741 yield project, ancestors.size
742 ancestors << project
742 ancestors << project
743 end
743 end
744 end
744 end
745
745
746 private
746 private
747
747
748 def after_parent_changed(parent_was)
748 def after_parent_changed(parent_was)
749 remove_inherited_member_roles
749 remove_inherited_member_roles
750 add_inherited_member_roles
750 add_inherited_member_roles
751 end
751 end
752
752
753 def update_inherited_members
753 def update_inherited_members
754 if parent
754 if parent
755 if inherit_members? && !inherit_members_was
755 if inherit_members? && !inherit_members_was
756 remove_inherited_member_roles
756 remove_inherited_member_roles
757 add_inherited_member_roles
757 add_inherited_member_roles
758 elsif !inherit_members? && inherit_members_was
758 elsif !inherit_members? && inherit_members_was
759 remove_inherited_member_roles
759 remove_inherited_member_roles
760 end
760 end
761 end
761 end
762 end
762 end
763
763
764 def remove_inherited_member_roles
764 def remove_inherited_member_roles
765 member_roles = memberships.map(&:member_roles).flatten
765 member_roles = memberships.map(&:member_roles).flatten
766 member_role_ids = member_roles.map(&:id)
766 member_role_ids = member_roles.map(&:id)
767 member_roles.each do |member_role|
767 member_roles.each do |member_role|
768 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
768 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
769 member_role.destroy
769 member_role.destroy
770 end
770 end
771 end
771 end
772 end
772 end
773
773
774 def add_inherited_member_roles
774 def add_inherited_member_roles
775 if inherit_members? && parent
775 if inherit_members? && parent
776 parent.memberships.each do |parent_member|
776 parent.memberships.each do |parent_member|
777 member = Member.find_or_new(self.id, parent_member.user_id)
777 member = Member.find_or_new(self.id, parent_member.user_id)
778 parent_member.member_roles.each do |parent_member_role|
778 parent_member.member_roles.each do |parent_member_role|
779 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
779 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
780 end
780 end
781 member.save!
781 member.save!
782 end
782 end
783 end
783 end
784 end
784 end
785
785
786 # Copies wiki from +project+
786 # Copies wiki from +project+
787 def copy_wiki(project)
787 def copy_wiki(project)
788 # Check that the source project has a wiki first
788 # Check that the source project has a wiki first
789 unless project.wiki.nil?
789 unless project.wiki.nil?
790 wiki = self.wiki || Wiki.new
790 wiki = self.wiki || Wiki.new
791 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
791 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
792 wiki_pages_map = {}
792 wiki_pages_map = {}
793 project.wiki.pages.each do |page|
793 project.wiki.pages.each do |page|
794 # Skip pages without content
794 # Skip pages without content
795 next if page.content.nil?
795 next if page.content.nil?
796 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
796 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
797 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
797 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
798 new_wiki_page.content = new_wiki_content
798 new_wiki_page.content = new_wiki_content
799 wiki.pages << new_wiki_page
799 wiki.pages << new_wiki_page
800 wiki_pages_map[page.id] = new_wiki_page
800 wiki_pages_map[page.id] = new_wiki_page
801 end
801 end
802
802
803 self.wiki = wiki
803 self.wiki = wiki
804 wiki.save
804 wiki.save
805 # Reproduce page hierarchy
805 # Reproduce page hierarchy
806 project.wiki.pages.each do |page|
806 project.wiki.pages.each do |page|
807 if page.parent_id && wiki_pages_map[page.id]
807 if page.parent_id && wiki_pages_map[page.id]
808 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
808 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
809 wiki_pages_map[page.id].save
809 wiki_pages_map[page.id].save
810 end
810 end
811 end
811 end
812 end
812 end
813 end
813 end
814
814
815 # Copies versions from +project+
815 # Copies versions from +project+
816 def copy_versions(project)
816 def copy_versions(project)
817 project.versions.each do |version|
817 project.versions.each do |version|
818 new_version = Version.new
818 new_version = Version.new
819 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
819 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
820 self.versions << new_version
820 self.versions << new_version
821 end
821 end
822 end
822 end
823
823
824 # Copies issue categories from +project+
824 # Copies issue categories from +project+
825 def copy_issue_categories(project)
825 def copy_issue_categories(project)
826 project.issue_categories.each do |issue_category|
826 project.issue_categories.each do |issue_category|
827 new_issue_category = IssueCategory.new
827 new_issue_category = IssueCategory.new
828 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
828 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
829 self.issue_categories << new_issue_category
829 self.issue_categories << new_issue_category
830 end
830 end
831 end
831 end
832
832
833 # Copies issues from +project+
833 # Copies issues from +project+
834 def copy_issues(project)
834 def copy_issues(project)
835 # Stores the source issue id as a key and the copied issues as the
835 # Stores the source issue id as a key and the copied issues as the
836 # value. Used to map the two togeather for issue relations.
836 # value. Used to map the two togeather for issue relations.
837 issues_map = {}
837 issues_map = {}
838
838
839 # Store status and reopen locked/closed versions
839 # Store status and reopen locked/closed versions
840 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
840 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
841 version_statuses.each do |version, status|
841 version_statuses.each do |version, status|
842 version.update_attribute :status, 'open'
842 version.update_attribute :status, 'open'
843 end
843 end
844
844
845 # Get issues sorted by root_id, lft so that parent issues
845 # Get issues sorted by root_id, lft so that parent issues
846 # get copied before their children
846 # get copied before their children
847 project.issues.reorder('root_id, lft').all.each do |issue|
847 project.issues.reorder('root_id, lft').all.each do |issue|
848 new_issue = Issue.new
848 new_issue = Issue.new
849 new_issue.copy_from(issue, :subtasks => false, :link => false)
849 new_issue.copy_from(issue, :subtasks => false, :link => false)
850 new_issue.project = self
850 new_issue.project = self
851 # Changing project resets the custom field values
852 # TODO: handle this in Issue#project=
853 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
851 # Reassign fixed_versions by name, since names are unique per project
854 # Reassign fixed_versions by name, since names are unique per project
852 if issue.fixed_version && issue.fixed_version.project == project
855 if issue.fixed_version && issue.fixed_version.project == project
853 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
856 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
854 end
857 end
855 # Reassign the category by name, since names are unique per project
858 # Reassign the category by name, since names are unique per project
856 if issue.category
859 if issue.category
857 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
860 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
858 end
861 end
859 # Parent issue
862 # Parent issue
860 if issue.parent_id
863 if issue.parent_id
861 if copied_parent = issues_map[issue.parent_id]
864 if copied_parent = issues_map[issue.parent_id]
862 new_issue.parent_issue_id = copied_parent.id
865 new_issue.parent_issue_id = copied_parent.id
863 end
866 end
864 end
867 end
865
868
866 self.issues << new_issue
869 self.issues << new_issue
867 if new_issue.new_record?
870 if new_issue.new_record?
868 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
871 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
869 else
872 else
870 issues_map[issue.id] = new_issue unless new_issue.new_record?
873 issues_map[issue.id] = new_issue unless new_issue.new_record?
871 end
874 end
872 end
875 end
873
876
874 # Restore locked/closed version statuses
877 # Restore locked/closed version statuses
875 version_statuses.each do |version, status|
878 version_statuses.each do |version, status|
876 version.update_attribute :status, status
879 version.update_attribute :status, status
877 end
880 end
878
881
879 # Relations after in case issues related each other
882 # Relations after in case issues related each other
880 project.issues.each do |issue|
883 project.issues.each do |issue|
881 new_issue = issues_map[issue.id]
884 new_issue = issues_map[issue.id]
882 unless new_issue
885 unless new_issue
883 # Issue was not copied
886 # Issue was not copied
884 next
887 next
885 end
888 end
886
889
887 # Relations
890 # Relations
888 issue.relations_from.each do |source_relation|
891 issue.relations_from.each do |source_relation|
889 new_issue_relation = IssueRelation.new
892 new_issue_relation = IssueRelation.new
890 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
893 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
891 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
894 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
892 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
895 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
893 new_issue_relation.issue_to = source_relation.issue_to
896 new_issue_relation.issue_to = source_relation.issue_to
894 end
897 end
895 new_issue.relations_from << new_issue_relation
898 new_issue.relations_from << new_issue_relation
896 end
899 end
897
900
898 issue.relations_to.each do |source_relation|
901 issue.relations_to.each do |source_relation|
899 new_issue_relation = IssueRelation.new
902 new_issue_relation = IssueRelation.new
900 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
903 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
901 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
904 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
902 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
905 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
903 new_issue_relation.issue_from = source_relation.issue_from
906 new_issue_relation.issue_from = source_relation.issue_from
904 end
907 end
905 new_issue.relations_to << new_issue_relation
908 new_issue.relations_to << new_issue_relation
906 end
909 end
907 end
910 end
908 end
911 end
909
912
910 # Copies members from +project+
913 # Copies members from +project+
911 def copy_members(project)
914 def copy_members(project)
912 # Copy users first, then groups to handle members with inherited and given roles
915 # Copy users first, then groups to handle members with inherited and given roles
913 members_to_copy = []
916 members_to_copy = []
914 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
917 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
915 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
918 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
916
919
917 members_to_copy.each do |member|
920 members_to_copy.each do |member|
918 new_member = Member.new
921 new_member = Member.new
919 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
922 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
920 # only copy non inherited roles
923 # only copy non inherited roles
921 # inherited roles will be added when copying the group membership
924 # inherited roles will be added when copying the group membership
922 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
925 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
923 next if role_ids.empty?
926 next if role_ids.empty?
924 new_member.role_ids = role_ids
927 new_member.role_ids = role_ids
925 new_member.project = self
928 new_member.project = self
926 self.members << new_member
929 self.members << new_member
927 end
930 end
928 end
931 end
929
932
930 # Copies queries from +project+
933 # Copies queries from +project+
931 def copy_queries(project)
934 def copy_queries(project)
932 project.queries.each do |query|
935 project.queries.each do |query|
933 new_query = IssueQuery.new
936 new_query = IssueQuery.new
934 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
937 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
935 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
938 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
936 new_query.project = self
939 new_query.project = self
937 new_query.user_id = query.user_id
940 new_query.user_id = query.user_id
938 self.queries << new_query
941 self.queries << new_query
939 end
942 end
940 end
943 end
941
944
942 # Copies boards from +project+
945 # Copies boards from +project+
943 def copy_boards(project)
946 def copy_boards(project)
944 project.boards.each do |board|
947 project.boards.each do |board|
945 new_board = Board.new
948 new_board = Board.new
946 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
949 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
947 new_board.project = self
950 new_board.project = self
948 self.boards << new_board
951 self.boards << new_board
949 end
952 end
950 end
953 end
951
954
952 def allowed_permissions
955 def allowed_permissions
953 @allowed_permissions ||= begin
956 @allowed_permissions ||= begin
954 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
957 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
955 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
958 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
956 end
959 end
957 end
960 end
958
961
959 def allowed_actions
962 def allowed_actions
960 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
963 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
961 end
964 end
962
965
963 # Returns all the active Systemwide and project specific activities
966 # Returns all the active Systemwide and project specific activities
964 def active_activities
967 def active_activities
965 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
968 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
966
969
967 if overridden_activity_ids.empty?
970 if overridden_activity_ids.empty?
968 return TimeEntryActivity.shared.active
971 return TimeEntryActivity.shared.active
969 else
972 else
970 return system_activities_and_project_overrides
973 return system_activities_and_project_overrides
971 end
974 end
972 end
975 end
973
976
974 # Returns all the Systemwide and project specific activities
977 # Returns all the Systemwide and project specific activities
975 # (inactive and active)
978 # (inactive and active)
976 def all_activities
979 def all_activities
977 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
980 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
978
981
979 if overridden_activity_ids.empty?
982 if overridden_activity_ids.empty?
980 return TimeEntryActivity.shared
983 return TimeEntryActivity.shared
981 else
984 else
982 return system_activities_and_project_overrides(true)
985 return system_activities_and_project_overrides(true)
983 end
986 end
984 end
987 end
985
988
986 # Returns the systemwide active activities merged with the project specific overrides
989 # Returns the systemwide active activities merged with the project specific overrides
987 def system_activities_and_project_overrides(include_inactive=false)
990 def system_activities_and_project_overrides(include_inactive=false)
988 if include_inactive
991 if include_inactive
989 return TimeEntryActivity.shared.
992 return TimeEntryActivity.shared.
990 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
993 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
991 self.time_entry_activities
994 self.time_entry_activities
992 else
995 else
993 return TimeEntryActivity.shared.active.
996 return TimeEntryActivity.shared.active.
994 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
997 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
995 self.time_entry_activities.active
998 self.time_entry_activities.active
996 end
999 end
997 end
1000 end
998
1001
999 # Archives subprojects recursively
1002 # Archives subprojects recursively
1000 def archive!
1003 def archive!
1001 children.each do |subproject|
1004 children.each do |subproject|
1002 subproject.send :archive!
1005 subproject.send :archive!
1003 end
1006 end
1004 update_attribute :status, STATUS_ARCHIVED
1007 update_attribute :status, STATUS_ARCHIVED
1005 end
1008 end
1006
1009
1007 def update_position_under_parent
1010 def update_position_under_parent
1008 set_or_update_position_under(parent)
1011 set_or_update_position_under(parent)
1009 end
1012 end
1010
1013
1011 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1014 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1012 def set_or_update_position_under(target_parent)
1015 def set_or_update_position_under(target_parent)
1013 parent_was = parent
1016 parent_was = parent
1014 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1017 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1015 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1018 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1016
1019
1017 if to_be_inserted_before
1020 if to_be_inserted_before
1018 move_to_left_of(to_be_inserted_before)
1021 move_to_left_of(to_be_inserted_before)
1019 elsif target_parent.nil?
1022 elsif target_parent.nil?
1020 if sibs.empty?
1023 if sibs.empty?
1021 # move_to_root adds the project in first (ie. left) position
1024 # move_to_root adds the project in first (ie. left) position
1022 move_to_root
1025 move_to_root
1023 else
1026 else
1024 move_to_right_of(sibs.last) unless self == sibs.last
1027 move_to_right_of(sibs.last) unless self == sibs.last
1025 end
1028 end
1026 else
1029 else
1027 # move_to_child_of adds the project in last (ie.right) position
1030 # move_to_child_of adds the project in last (ie.right) position
1028 move_to_child_of(target_parent)
1031 move_to_child_of(target_parent)
1029 end
1032 end
1030 if parent_was != target_parent
1033 if parent_was != target_parent
1031 after_parent_changed(parent_was)
1034 after_parent_changed(parent_was)
1032 end
1035 end
1033 end
1036 end
1034 end
1037 end
@@ -1,324 +1,337
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class ProjectCopyTest < ActiveSupport::TestCase
20 class ProjectCopyTest < ActiveSupport::TestCase
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :journals, :journal_details,
22 :journals, :journal_details,
23 :enumerations, :users, :issue_categories,
23 :enumerations, :users, :issue_categories,
24 :projects_trackers,
24 :projects_trackers,
25 :custom_fields,
25 :custom_fields,
26 :custom_fields_projects,
26 :custom_fields_projects,
27 :custom_fields_trackers,
27 :custom_fields_trackers,
28 :custom_values,
28 :custom_values,
29 :roles,
29 :roles,
30 :member_roles,
30 :member_roles,
31 :members,
31 :members,
32 :enabled_modules,
32 :enabled_modules,
33 :versions,
33 :versions,
34 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
34 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
35 :groups_users,
35 :groups_users,
36 :boards, :messages,
36 :boards, :messages,
37 :repositories,
37 :repositories,
38 :news, :comments,
38 :news, :comments,
39 :documents
39 :documents
40
40
41 def setup
41 def setup
42 ProjectCustomField.destroy_all
42 ProjectCustomField.destroy_all
43 @source_project = Project.find(2)
43 @source_project = Project.find(2)
44 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
44 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
45 @project.trackers = @source_project.trackers
45 @project.trackers = @source_project.trackers
46 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
46 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
47 end
47 end
48
48
49 test "#copy should copy issues" do
49 test "#copy should copy issues" do
50 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
50 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
51 :subject => "copy issue status",
51 :subject => "copy issue status",
52 :tracker_id => 1,
52 :tracker_id => 1,
53 :assigned_to_id => 2,
53 :assigned_to_id => 2,
54 :project_id => @source_project.id)
54 :project_id => @source_project.id)
55 assert @project.valid?
55 assert @project.valid?
56 assert @project.issues.empty?
56 assert @project.issues.empty?
57 assert @project.copy(@source_project)
57 assert @project.copy(@source_project)
58
58
59 assert_equal @source_project.issues.size, @project.issues.size
59 assert_equal @source_project.issues.size, @project.issues.size
60 @project.issues.each do |issue|
60 @project.issues.each do |issue|
61 assert issue.valid?
61 assert issue.valid?
62 assert ! issue.assigned_to.blank?
62 assert ! issue.assigned_to.blank?
63 assert_equal @project, issue.project
63 assert_equal @project, issue.project
64 end
64 end
65
65
66 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
66 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
67 assert copied_issue
67 assert copied_issue
68 assert copied_issue.status
68 assert copied_issue.status
69 assert_equal "Closed", copied_issue.status.name
69 assert_equal "Closed", copied_issue.status.name
70 end
70 end
71
71
72 test "#copy should copy issues custom values" do
73 field = IssueCustomField.generate!(:is_for_all => true, :trackers => Tracker.all)
74 issue = Issue.generate!(:project => @source_project, :subject => 'Custom field copy')
75 issue.custom_field_values = {field.id => 'custom'}
76 issue.save!
77 assert_equal 'custom', issue.reload.custom_field_value(field)
78
79 assert @project.copy(@source_project)
80 copy = @project.issues.find_by_subject('Custom field copy')
81 assert copy
82 assert_equal 'custom', copy.reload.custom_field_value(field)
83 end
84
72 test "#copy should copy issues assigned to a locked version" do
85 test "#copy should copy issues assigned to a locked version" do
73 User.current = User.find(1)
86 User.current = User.find(1)
74 assigned_version = Version.generate!(:name => "Assigned Issues")
87 assigned_version = Version.generate!(:name => "Assigned Issues")
75 @source_project.versions << assigned_version
88 @source_project.versions << assigned_version
76 Issue.generate!(:project => @source_project,
89 Issue.generate!(:project => @source_project,
77 :fixed_version_id => assigned_version.id,
90 :fixed_version_id => assigned_version.id,
78 :subject => "copy issues assigned to a locked version")
91 :subject => "copy issues assigned to a locked version")
79 assigned_version.update_attribute :status, 'locked'
92 assigned_version.update_attribute :status, 'locked'
80
93
81 assert @project.copy(@source_project)
94 assert @project.copy(@source_project)
82 @project.reload
95 @project.reload
83 copied_issue = @project.issues.first(:conditions => {:subject => "copy issues assigned to a locked version"})
96 copied_issue = @project.issues.first(:conditions => {:subject => "copy issues assigned to a locked version"})
84
97
85 assert copied_issue
98 assert copied_issue
86 assert copied_issue.fixed_version
99 assert copied_issue.fixed_version
87 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
100 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
88 assert_equal 'locked', copied_issue.fixed_version.status
101 assert_equal 'locked', copied_issue.fixed_version.status
89 end
102 end
90
103
91 test "#copy should change the new issues to use the copied version" do
104 test "#copy should change the new issues to use the copied version" do
92 User.current = User.find(1)
105 User.current = User.find(1)
93 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
106 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
94 @source_project.versions << assigned_version
107 @source_project.versions << assigned_version
95 assert_equal 3, @source_project.versions.size
108 assert_equal 3, @source_project.versions.size
96 Issue.generate!(:project => @source_project,
109 Issue.generate!(:project => @source_project,
97 :fixed_version_id => assigned_version.id,
110 :fixed_version_id => assigned_version.id,
98 :subject => "change the new issues to use the copied version")
111 :subject => "change the new issues to use the copied version")
99
112
100 assert @project.copy(@source_project)
113 assert @project.copy(@source_project)
101 @project.reload
114 @project.reload
102 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
115 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
103
116
104 assert copied_issue
117 assert copied_issue
105 assert copied_issue.fixed_version
118 assert copied_issue.fixed_version
106 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
119 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
107 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
120 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
108 end
121 end
109
122
110 test "#copy should keep target shared versions from other project" do
123 test "#copy should keep target shared versions from other project" do
111 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open', :project_id => 1, :sharing => 'system')
124 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open', :project_id => 1, :sharing => 'system')
112 issue = Issue.generate!(:project => @source_project,
125 issue = Issue.generate!(:project => @source_project,
113 :fixed_version => assigned_version,
126 :fixed_version => assigned_version,
114 :subject => "keep target shared versions")
127 :subject => "keep target shared versions")
115
128
116 assert @project.copy(@source_project)
129 assert @project.copy(@source_project)
117 @project.reload
130 @project.reload
118 copied_issue = @project.issues.first(:conditions => {:subject => "keep target shared versions"})
131 copied_issue = @project.issues.first(:conditions => {:subject => "keep target shared versions"})
119
132
120 assert copied_issue
133 assert copied_issue
121 assert_equal assigned_version, copied_issue.fixed_version
134 assert_equal assigned_version, copied_issue.fixed_version
122 end
135 end
123
136
124 test "#copy should copy issue relations" do
137 test "#copy should copy issue relations" do
125 Setting.cross_project_issue_relations = '1'
138 Setting.cross_project_issue_relations = '1'
126
139
127 second_issue = Issue.generate!(:status_id => 5,
140 second_issue = Issue.generate!(:status_id => 5,
128 :subject => "copy issue relation",
141 :subject => "copy issue relation",
129 :tracker_id => 1,
142 :tracker_id => 1,
130 :assigned_to_id => 2,
143 :assigned_to_id => 2,
131 :project_id => @source_project.id)
144 :project_id => @source_project.id)
132 source_relation = IssueRelation.create!(:issue_from => Issue.find(4),
145 source_relation = IssueRelation.create!(:issue_from => Issue.find(4),
133 :issue_to => second_issue,
146 :issue_to => second_issue,
134 :relation_type => "relates")
147 :relation_type => "relates")
135 source_relation_cross_project = IssueRelation.create!(:issue_from => Issue.find(1),
148 source_relation_cross_project = IssueRelation.create!(:issue_from => Issue.find(1),
136 :issue_to => second_issue,
149 :issue_to => second_issue,
137 :relation_type => "duplicates")
150 :relation_type => "duplicates")
138
151
139 assert @project.copy(@source_project)
152 assert @project.copy(@source_project)
140 assert_equal @source_project.issues.count, @project.issues.count
153 assert_equal @source_project.issues.count, @project.issues.count
141 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
154 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
142 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
155 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
143
156
144 # First issue with a relation on project
157 # First issue with a relation on project
145 assert_equal 1, copied_issue.relations.size, "Relation not copied"
158 assert_equal 1, copied_issue.relations.size, "Relation not copied"
146 copied_relation = copied_issue.relations.first
159 copied_relation = copied_issue.relations.first
147 assert_equal "relates", copied_relation.relation_type
160 assert_equal "relates", copied_relation.relation_type
148 assert_equal copied_second_issue.id, copied_relation.issue_to_id
161 assert_equal copied_second_issue.id, copied_relation.issue_to_id
149 assert_not_equal source_relation.id, copied_relation.id
162 assert_not_equal source_relation.id, copied_relation.id
150
163
151 # Second issue with a cross project relation
164 # Second issue with a cross project relation
152 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
165 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
153 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
166 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
154 assert_equal "duplicates", copied_relation.relation_type
167 assert_equal "duplicates", copied_relation.relation_type
155 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
168 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
156 assert_not_equal source_relation_cross_project.id, copied_relation.id
169 assert_not_equal source_relation_cross_project.id, copied_relation.id
157 end
170 end
158
171
159 test "#copy should copy issue attachments" do
172 test "#copy should copy issue attachments" do
160 issue = Issue.generate!(:subject => "copy with attachment", :tracker_id => 1, :project_id => @source_project.id)
173 issue = Issue.generate!(:subject => "copy with attachment", :tracker_id => 1, :project_id => @source_project.id)
161 Attachment.create!(:container => issue, :file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 1)
174 Attachment.create!(:container => issue, :file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 1)
162 @source_project.issues << issue
175 @source_project.issues << issue
163 assert @project.copy(@source_project)
176 assert @project.copy(@source_project)
164
177
165 copied_issue = @project.issues.first(:conditions => {:subject => "copy with attachment"})
178 copied_issue = @project.issues.first(:conditions => {:subject => "copy with attachment"})
166 assert_not_nil copied_issue
179 assert_not_nil copied_issue
167 assert_equal 1, copied_issue.attachments.count, "Attachment not copied"
180 assert_equal 1, copied_issue.attachments.count, "Attachment not copied"
168 assert_equal "testfile.txt", copied_issue.attachments.first.filename
181 assert_equal "testfile.txt", copied_issue.attachments.first.filename
169 end
182 end
170
183
171 test "#copy should copy memberships" do
184 test "#copy should copy memberships" do
172 assert @project.valid?
185 assert @project.valid?
173 assert @project.members.empty?
186 assert @project.members.empty?
174 assert @project.copy(@source_project)
187 assert @project.copy(@source_project)
175
188
176 assert_equal @source_project.memberships.size, @project.memberships.size
189 assert_equal @source_project.memberships.size, @project.memberships.size
177 @project.memberships.each do |membership|
190 @project.memberships.each do |membership|
178 assert membership
191 assert membership
179 assert_equal @project, membership.project
192 assert_equal @project, membership.project
180 end
193 end
181 end
194 end
182
195
183 test "#copy should copy memberships with groups and additional roles" do
196 test "#copy should copy memberships with groups and additional roles" do
184 group = Group.create!(:lastname => "Copy group")
197 group = Group.create!(:lastname => "Copy group")
185 user = User.find(7)
198 user = User.find(7)
186 group.users << user
199 group.users << user
187 # group role
200 # group role
188 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
201 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
189 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
202 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
190 # additional role
203 # additional role
191 member.role_ids = [1]
204 member.role_ids = [1]
192
205
193 assert @project.copy(@source_project)
206 assert @project.copy(@source_project)
194 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
207 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
195 assert_not_nil member
208 assert_not_nil member
196 assert_equal [1, 2], member.role_ids.sort
209 assert_equal [1, 2], member.role_ids.sort
197 end
210 end
198
211
199 test "#copy should copy project specific queries" do
212 test "#copy should copy project specific queries" do
200 assert @project.valid?
213 assert @project.valid?
201 assert @project.queries.empty?
214 assert @project.queries.empty?
202 assert @project.copy(@source_project)
215 assert @project.copy(@source_project)
203
216
204 assert_equal @source_project.queries.size, @project.queries.size
217 assert_equal @source_project.queries.size, @project.queries.size
205 @project.queries.each do |query|
218 @project.queries.each do |query|
206 assert query
219 assert query
207 assert_equal @project, query.project
220 assert_equal @project, query.project
208 end
221 end
209 assert_equal @source_project.queries.map(&:user_id).sort, @project.queries.map(&:user_id).sort
222 assert_equal @source_project.queries.map(&:user_id).sort, @project.queries.map(&:user_id).sort
210 end
223 end
211
224
212 test "#copy should copy versions" do
225 test "#copy should copy versions" do
213 @source_project.versions << Version.generate!
226 @source_project.versions << Version.generate!
214 @source_project.versions << Version.generate!
227 @source_project.versions << Version.generate!
215
228
216 assert @project.versions.empty?
229 assert @project.versions.empty?
217 assert @project.copy(@source_project)
230 assert @project.copy(@source_project)
218
231
219 assert_equal @source_project.versions.size, @project.versions.size
232 assert_equal @source_project.versions.size, @project.versions.size
220 @project.versions.each do |version|
233 @project.versions.each do |version|
221 assert version
234 assert version
222 assert_equal @project, version.project
235 assert_equal @project, version.project
223 end
236 end
224 end
237 end
225
238
226 test "#copy should copy wiki" do
239 test "#copy should copy wiki" do
227 assert_difference 'Wiki.count' do
240 assert_difference 'Wiki.count' do
228 assert @project.copy(@source_project)
241 assert @project.copy(@source_project)
229 end
242 end
230
243
231 assert @project.wiki
244 assert @project.wiki
232 assert_not_equal @source_project.wiki, @project.wiki
245 assert_not_equal @source_project.wiki, @project.wiki
233 assert_equal "Start page", @project.wiki.start_page
246 assert_equal "Start page", @project.wiki.start_page
234 end
247 end
235
248
236 test "#copy should copy wiki without wiki module" do
249 test "#copy should copy wiki without wiki module" do
237 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test', :enabled_module_names => [])
250 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test', :enabled_module_names => [])
238 assert_difference 'Wiki.count' do
251 assert_difference 'Wiki.count' do
239 assert project.copy(@source_project)
252 assert project.copy(@source_project)
240 end
253 end
241
254
242 assert project.wiki
255 assert project.wiki
243 end
256 end
244
257
245 test "#copy should copy wiki pages and content with hierarchy" do
258 test "#copy should copy wiki pages and content with hierarchy" do
246 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
259 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
247 assert @project.copy(@source_project)
260 assert @project.copy(@source_project)
248 end
261 end
249
262
250 assert @project.wiki
263 assert @project.wiki
251 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
264 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
252
265
253 @project.wiki.pages.each do |wiki_page|
266 @project.wiki.pages.each do |wiki_page|
254 assert wiki_page.content
267 assert wiki_page.content
255 assert !@source_project.wiki.pages.include?(wiki_page)
268 assert !@source_project.wiki.pages.include?(wiki_page)
256 end
269 end
257
270
258 parent = @project.wiki.find_page('Parent_page')
271 parent = @project.wiki.find_page('Parent_page')
259 child1 = @project.wiki.find_page('Child_page_1')
272 child1 = @project.wiki.find_page('Child_page_1')
260 child2 = @project.wiki.find_page('Child_page_2')
273 child2 = @project.wiki.find_page('Child_page_2')
261 assert_equal parent, child1.parent
274 assert_equal parent, child1.parent
262 assert_equal parent, child2.parent
275 assert_equal parent, child2.parent
263 end
276 end
264
277
265 test "#copy should copy issue categories" do
278 test "#copy should copy issue categories" do
266 assert @project.copy(@source_project)
279 assert @project.copy(@source_project)
267
280
268 assert_equal 2, @project.issue_categories.size
281 assert_equal 2, @project.issue_categories.size
269 @project.issue_categories.each do |issue_category|
282 @project.issue_categories.each do |issue_category|
270 assert !@source_project.issue_categories.include?(issue_category)
283 assert !@source_project.issue_categories.include?(issue_category)
271 end
284 end
272 end
285 end
273
286
274 test "#copy should copy boards" do
287 test "#copy should copy boards" do
275 assert @project.copy(@source_project)
288 assert @project.copy(@source_project)
276
289
277 assert_equal 1, @project.boards.size
290 assert_equal 1, @project.boards.size
278 @project.boards.each do |board|
291 @project.boards.each do |board|
279 assert !@source_project.boards.include?(board)
292 assert !@source_project.boards.include?(board)
280 end
293 end
281 end
294 end
282
295
283 test "#copy should change the new issues to use the copied issue categories" do
296 test "#copy should change the new issues to use the copied issue categories" do
284 issue = Issue.find(4)
297 issue = Issue.find(4)
285 issue.update_attribute(:category_id, 3)
298 issue.update_attribute(:category_id, 3)
286
299
287 assert @project.copy(@source_project)
300 assert @project.copy(@source_project)
288
301
289 @project.issues.each do |issue|
302 @project.issues.each do |issue|
290 assert issue.category
303 assert issue.category
291 assert_equal "Stock management", issue.category.name # Same name
304 assert_equal "Stock management", issue.category.name # Same name
292 assert_not_equal IssueCategory.find(3), issue.category # Different record
305 assert_not_equal IssueCategory.find(3), issue.category # Different record
293 end
306 end
294 end
307 end
295
308
296 test "#copy should limit copy with :only option" do
309 test "#copy should limit copy with :only option" do
297 assert @project.members.empty?
310 assert @project.members.empty?
298 assert @project.issue_categories.empty?
311 assert @project.issue_categories.empty?
299 assert @source_project.issues.any?
312 assert @source_project.issues.any?
300
313
301 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
314 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
302
315
303 assert @project.members.any?
316 assert @project.members.any?
304 assert @project.issue_categories.any?
317 assert @project.issue_categories.any?
305 assert @project.issues.empty?
318 assert @project.issues.empty?
306 end
319 end
307
320
308 test "#copy should copy subtasks" do
321 test "#copy should copy subtasks" do
309 source = Project.generate!(:tracker_ids => [1])
322 source = Project.generate!(:tracker_ids => [1])
310 issue = Issue.generate_with_descendants!(:project => source)
323 issue = Issue.generate_with_descendants!(:project => source)
311 project = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1])
324 project = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1])
312
325
313 assert_difference 'Project.count' do
326 assert_difference 'Project.count' do
314 assert_difference 'Issue.count', 1+issue.descendants.count do
327 assert_difference 'Issue.count', 1+issue.descendants.count do
315 assert project.copy(source.reload)
328 assert project.copy(source.reload)
316 end
329 end
317 end
330 end
318 copy = Issue.where(:parent_id => nil).order("id DESC").first
331 copy = Issue.where(:parent_id => nil).order("id DESC").first
319 assert_equal project, copy.project
332 assert_equal project, copy.project
320 assert_equal issue.descendants.count, copy.descendants.count
333 assert_equal issue.descendants.count, copy.descendants.count
321 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
334 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
322 assert child_copy.descendants.any?
335 assert child_copy.descendants.any?
323 end
336 end
324 end
337 end
General Comments 0
You need to be logged in to leave comments. Login now