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