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