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