##// END OF EJS Templates
Merged r5176 from trunk....
Jean-Philippe Lang -
r5469:c2cd4b7f48be
parent child
Show More
@@ -1,831 +1,831
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 => :destroy, :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', :dependent => :destroy
59 acts_as_nested_set :order => 'name', :dependent => :destroy
60 acts_as_attachable :view_permission => :view_files,
60 acts_as_attachable :view_permission => :view_files,
61 :delete_permission => :manage_files
61 :delete_permission => :manage_files
62
62
63 acts_as_customizable
63 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
67 :author => nil
68
68
69 attr_protected :status
69 attr_protected :status
70
70
71 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier
72 validates_uniqueness_of :identifier
73 validates_associated :repository, :wiki
73 validates_associated :repository, :wiki
74 validates_length_of :name, :maximum => 255
74 validates_length_of :name, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 # donwcase letters, digits, dashes but not digits only
77 # donwcase letters, digits, dashes but not digits only
78 validates_format_of :identifier, :with => /^(?!\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
82 before_destroy :delete_all_members
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 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
512 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
513 else
513 else
514 enabled_modules.clear
514 enabled_modules.clear
515 end
515 end
516 end
516 end
517
517
518 # Returns an array of the enabled modules names
518 # Returns an array of the enabled modules names
519 def enabled_module_names
519 def enabled_module_names
520 enabled_modules.collect(&:name)
520 enabled_modules.collect(&:name)
521 end
521 end
522
522
523 safe_attributes 'name',
523 safe_attributes 'name',
524 'description',
524 'description',
525 'homepage',
525 'homepage',
526 'is_public',
526 'is_public',
527 'identifier',
527 'identifier',
528 'custom_field_values',
528 'custom_field_values',
529 'custom_fields',
529 'custom_fields',
530 'tracker_ids',
530 'tracker_ids',
531 'issue_custom_field_ids'
531 'issue_custom_field_ids'
532
532
533 safe_attributes 'enabled_module_names',
533 safe_attributes 'enabled_module_names',
534 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
534 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
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 # Copies wiki from +project+
621 # Copies wiki from +project+
622 def copy_wiki(project)
622 def copy_wiki(project)
623 # Check that the source project has a wiki first
623 # Check that the source project has a wiki first
624 unless project.wiki.nil?
624 unless project.wiki.nil?
625 self.wiki ||= Wiki.new
625 self.wiki ||= Wiki.new
626 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
626 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
627 wiki_pages_map = {}
627 wiki_pages_map = {}
628 project.wiki.pages.each do |page|
628 project.wiki.pages.each do |page|
629 # Skip pages without content
629 # Skip pages without content
630 next if page.content.nil?
630 next if page.content.nil?
631 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
631 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
632 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
632 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
633 new_wiki_page.content = new_wiki_content
633 new_wiki_page.content = new_wiki_content
634 wiki.pages << new_wiki_page
634 wiki.pages << new_wiki_page
635 wiki_pages_map[page.id] = new_wiki_page
635 wiki_pages_map[page.id] = new_wiki_page
636 end
636 end
637 wiki.save
637 wiki.save
638 # Reproduce page hierarchy
638 # Reproduce page hierarchy
639 project.wiki.pages.each do |page|
639 project.wiki.pages.each do |page|
640 if page.parent_id && wiki_pages_map[page.id]
640 if page.parent_id && wiki_pages_map[page.id]
641 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
641 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
642 wiki_pages_map[page.id].save
642 wiki_pages_map[page.id].save
643 end
643 end
644 end
644 end
645 end
645 end
646 end
646 end
647
647
648 # Copies versions from +project+
648 # Copies versions from +project+
649 def copy_versions(project)
649 def copy_versions(project)
650 project.versions.each do |version|
650 project.versions.each do |version|
651 new_version = Version.new
651 new_version = Version.new
652 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
652 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
653 self.versions << new_version
653 self.versions << new_version
654 end
654 end
655 end
655 end
656
656
657 # Copies issue categories from +project+
657 # Copies issue categories from +project+
658 def copy_issue_categories(project)
658 def copy_issue_categories(project)
659 project.issue_categories.each do |issue_category|
659 project.issue_categories.each do |issue_category|
660 new_issue_category = IssueCategory.new
660 new_issue_category = IssueCategory.new
661 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
661 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
662 self.issue_categories << new_issue_category
662 self.issue_categories << new_issue_category
663 end
663 end
664 end
664 end
665
665
666 # Copies issues from +project+
666 # Copies issues from +project+
667 def copy_issues(project)
667 def copy_issues(project)
668 # Stores the source issue id as a key and the copied issues as the
668 # Stores the source issue id as a key and the copied issues as the
669 # value. Used to map the two togeather for issue relations.
669 # value. Used to map the two togeather for issue relations.
670 issues_map = {}
670 issues_map = {}
671
671
672 # Get issues sorted by root_id, lft so that parent issues
672 # Get issues sorted by root_id, lft so that parent issues
673 # get copied before their children
673 # get copied before their children
674 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
674 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
675 new_issue = Issue.new
675 new_issue = Issue.new
676 new_issue.copy_from(issue)
676 new_issue.copy_from(issue)
677 new_issue.project = self
677 new_issue.project = self
678 # Reassign fixed_versions by name, since names are unique per
678 # Reassign fixed_versions by name, since names are unique per
679 # project and the versions for self are not yet saved
679 # project and the versions for self are not yet saved
680 if issue.fixed_version
680 if issue.fixed_version
681 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
681 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
682 end
682 end
683 # Reassign the category by name, since names are unique per
683 # Reassign the category by name, since names are unique per
684 # project and the categories for self are not yet saved
684 # project and the categories for self are not yet saved
685 if issue.category
685 if issue.category
686 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
686 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
687 end
687 end
688 # Parent issue
688 # Parent issue
689 if issue.parent_id
689 if issue.parent_id
690 if copied_parent = issues_map[issue.parent_id]
690 if copied_parent = issues_map[issue.parent_id]
691 new_issue.parent_issue_id = copied_parent.id
691 new_issue.parent_issue_id = copied_parent.id
692 end
692 end
693 end
693 end
694
694
695 self.issues << new_issue
695 self.issues << new_issue
696 if new_issue.new_record?
696 if new_issue.new_record?
697 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
697 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
698 else
698 else
699 issues_map[issue.id] = new_issue unless new_issue.new_record?
699 issues_map[issue.id] = new_issue unless new_issue.new_record?
700 end
700 end
701 end
701 end
702
702
703 # Relations after in case issues related each other
703 # Relations after in case issues related each other
704 project.issues.each do |issue|
704 project.issues.each do |issue|
705 new_issue = issues_map[issue.id]
705 new_issue = issues_map[issue.id]
706 unless new_issue
706 unless new_issue
707 # Issue was not copied
707 # Issue was not copied
708 next
708 next
709 end
709 end
710
710
711 # Relations
711 # Relations
712 issue.relations_from.each do |source_relation|
712 issue.relations_from.each do |source_relation|
713 new_issue_relation = IssueRelation.new
713 new_issue_relation = IssueRelation.new
714 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
714 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
715 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
715 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
716 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
716 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
717 new_issue_relation.issue_to = source_relation.issue_to
717 new_issue_relation.issue_to = source_relation.issue_to
718 end
718 end
719 new_issue.relations_from << new_issue_relation
719 new_issue.relations_from << new_issue_relation
720 end
720 end
721
721
722 issue.relations_to.each do |source_relation|
722 issue.relations_to.each do |source_relation|
723 new_issue_relation = IssueRelation.new
723 new_issue_relation = IssueRelation.new
724 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
724 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
725 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
725 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
726 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
726 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
727 new_issue_relation.issue_from = source_relation.issue_from
727 new_issue_relation.issue_from = source_relation.issue_from
728 end
728 end
729 new_issue.relations_to << new_issue_relation
729 new_issue.relations_to << new_issue_relation
730 end
730 end
731 end
731 end
732 end
732 end
733
733
734 # Copies members from +project+
734 # Copies members from +project+
735 def copy_members(project)
735 def copy_members(project)
736 # Copy users first, then groups to handle members with inherited and given roles
736 # Copy users first, then groups to handle members with inherited and given roles
737 members_to_copy = []
737 members_to_copy = []
738 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
738 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
739 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
739 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
740
740
741 members_to_copy.each do |member|
741 members_to_copy.each do |member|
742 new_member = Member.new
742 new_member = Member.new
743 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
743 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
744 # only copy non inherited roles
744 # only copy non inherited roles
745 # inherited roles will be added when copying the group membership
745 # inherited roles will be added when copying the group membership
746 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
746 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
747 next if role_ids.empty?
747 next if role_ids.empty?
748 new_member.role_ids = role_ids
748 new_member.role_ids = role_ids
749 new_member.project = self
749 new_member.project = self
750 self.members << new_member
750 self.members << new_member
751 end
751 end
752 end
752 end
753
753
754 # Copies queries from +project+
754 # Copies queries from +project+
755 def copy_queries(project)
755 def copy_queries(project)
756 project.queries.each do |query|
756 project.queries.each do |query|
757 new_query = Query.new
757 new_query = Query.new
758 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
758 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
759 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
759 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
760 new_query.project = self
760 new_query.project = self
761 self.queries << new_query
761 self.queries << new_query
762 end
762 end
763 end
763 end
764
764
765 # Copies boards from +project+
765 # Copies boards from +project+
766 def copy_boards(project)
766 def copy_boards(project)
767 project.boards.each do |board|
767 project.boards.each do |board|
768 new_board = Board.new
768 new_board = Board.new
769 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
769 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
770 new_board.project = self
770 new_board.project = self
771 self.boards << new_board
771 self.boards << new_board
772 end
772 end
773 end
773 end
774
774
775 def allowed_permissions
775 def allowed_permissions
776 @allowed_permissions ||= begin
776 @allowed_permissions ||= begin
777 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
777 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
778 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
778 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
779 end
779 end
780 end
780 end
781
781
782 def allowed_actions
782 def allowed_actions
783 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
783 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
784 end
784 end
785
785
786 # Returns all the active Systemwide and project specific activities
786 # Returns all the active Systemwide and project specific activities
787 def active_activities
787 def active_activities
788 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
788 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
789
789
790 if overridden_activity_ids.empty?
790 if overridden_activity_ids.empty?
791 return TimeEntryActivity.shared.active
791 return TimeEntryActivity.shared.active
792 else
792 else
793 return system_activities_and_project_overrides
793 return system_activities_and_project_overrides
794 end
794 end
795 end
795 end
796
796
797 # Returns all the Systemwide and project specific activities
797 # Returns all the Systemwide and project specific activities
798 # (inactive and active)
798 # (inactive and active)
799 def all_activities
799 def all_activities
800 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
800 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
801
801
802 if overridden_activity_ids.empty?
802 if overridden_activity_ids.empty?
803 return TimeEntryActivity.shared
803 return TimeEntryActivity.shared
804 else
804 else
805 return system_activities_and_project_overrides(true)
805 return system_activities_and_project_overrides(true)
806 end
806 end
807 end
807 end
808
808
809 # Returns the systemwide active activities merged with the project specific overrides
809 # Returns the systemwide active activities merged with the project specific overrides
810 def system_activities_and_project_overrides(include_inactive=false)
810 def system_activities_and_project_overrides(include_inactive=false)
811 if include_inactive
811 if include_inactive
812 return TimeEntryActivity.shared.
812 return TimeEntryActivity.shared.
813 find(:all,
813 find(:all,
814 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
814 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
815 self.time_entry_activities
815 self.time_entry_activities
816 else
816 else
817 return TimeEntryActivity.shared.active.
817 return TimeEntryActivity.shared.active.
818 find(:all,
818 find(:all,
819 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
819 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
820 self.time_entry_activities.active
820 self.time_entry_activities.active
821 end
821 end
822 end
822 end
823
823
824 # Archives subprojects recursively
824 # Archives subprojects recursively
825 def archive!
825 def archive!
826 children.each do |subproject|
826 children.each do |subproject|
827 subproject.send :archive!
827 subproject.send :archive!
828 end
828 end
829 update_attribute :status, STATUS_ARCHIVED
829 update_attribute :status, STATUS_ARCHIVED
830 end
830 end
831 end
831 end
@@ -1,1090 +1,1090
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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_destroying_root_projects_should_clear_data
185 def test_destroying_root_projects_should_clear_data
186 Project.roots.each do |root|
186 Project.roots.each do |root|
187 root.destroy
187 root.destroy
188 end
188 end
189
189
190 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
190 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
191 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
191 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
192 assert_equal 0, MemberRole.count
192 assert_equal 0, MemberRole.count
193 assert_equal 0, Issue.count
193 assert_equal 0, Issue.count
194 assert_equal 0, Journal.count
194 assert_equal 0, Journal.count
195 assert_equal 0, JournalDetail.count
195 assert_equal 0, JournalDetail.count
196 assert_equal 0, Attachment.count
196 assert_equal 0, Attachment.count
197 assert_equal 0, EnabledModule.count
197 assert_equal 0, EnabledModule.count
198 assert_equal 0, IssueCategory.count
198 assert_equal 0, IssueCategory.count
199 assert_equal 0, IssueRelation.count
199 assert_equal 0, IssueRelation.count
200 assert_equal 0, Board.count
200 assert_equal 0, Board.count
201 assert_equal 0, Message.count
201 assert_equal 0, Message.count
202 assert_equal 0, News.count
202 assert_equal 0, News.count
203 assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL")
203 assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL")
204 assert_equal 0, Repository.count
204 assert_equal 0, Repository.count
205 assert_equal 0, Changeset.count
205 assert_equal 0, Changeset.count
206 assert_equal 0, Change.count
206 assert_equal 0, Change.count
207 #assert_equal 0, Comment.count
207 assert_equal 0, Comment.count
208 assert_equal 0, TimeEntry.count
208 assert_equal 0, TimeEntry.count
209 assert_equal 0, Version.count
209 assert_equal 0, Version.count
210 assert_equal 0, Watcher.count
210 assert_equal 0, Watcher.count
211 assert_equal 0, Wiki.count
211 assert_equal 0, Wiki.count
212 assert_equal 0, WikiPage.count
212 assert_equal 0, WikiPage.count
213 assert_equal 0, WikiContent.count
213 assert_equal 0, WikiContent.count
214 assert_equal 0, WikiContent::Version.count
214 assert_equal 0, WikiContent::Version.count
215 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
215 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
216 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
216 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
217 assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
217 assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
218 end
218 end
219
219
220 def test_move_an_orphan_project_to_a_root_project
220 def test_move_an_orphan_project_to_a_root_project
221 sub = Project.find(2)
221 sub = Project.find(2)
222 sub.set_parent! @ecookbook
222 sub.set_parent! @ecookbook
223 assert_equal @ecookbook.id, sub.parent.id
223 assert_equal @ecookbook.id, sub.parent.id
224 @ecookbook.reload
224 @ecookbook.reload
225 assert_equal 4, @ecookbook.children.size
225 assert_equal 4, @ecookbook.children.size
226 end
226 end
227
227
228 def test_move_an_orphan_project_to_a_subproject
228 def test_move_an_orphan_project_to_a_subproject
229 sub = Project.find(2)
229 sub = Project.find(2)
230 assert sub.set_parent!(@ecookbook_sub1)
230 assert sub.set_parent!(@ecookbook_sub1)
231 end
231 end
232
232
233 def test_move_a_root_project_to_a_project
233 def test_move_a_root_project_to_a_project
234 sub = @ecookbook
234 sub = @ecookbook
235 assert sub.set_parent!(Project.find(2))
235 assert sub.set_parent!(Project.find(2))
236 end
236 end
237
237
238 def test_should_not_move_a_project_to_its_children
238 def test_should_not_move_a_project_to_its_children
239 sub = @ecookbook
239 sub = @ecookbook
240 assert !(sub.set_parent!(Project.find(3)))
240 assert !(sub.set_parent!(Project.find(3)))
241 end
241 end
242
242
243 def test_set_parent_should_add_roots_in_alphabetical_order
243 def test_set_parent_should_add_roots_in_alphabetical_order
244 ProjectCustomField.delete_all
244 ProjectCustomField.delete_all
245 Project.delete_all
245 Project.delete_all
246 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
246 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
247 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
247 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
248 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
248 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
249 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
249 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
250
250
251 assert_equal 4, Project.count
251 assert_equal 4, Project.count
252 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
252 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
253 end
253 end
254
254
255 def test_set_parent_should_add_children_in_alphabetical_order
255 def test_set_parent_should_add_children_in_alphabetical_order
256 ProjectCustomField.delete_all
256 ProjectCustomField.delete_all
257 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
257 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
258 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
258 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
259 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
259 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
260 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
260 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
261 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
261 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
262
262
263 parent.reload
263 parent.reload
264 assert_equal 4, parent.children.size
264 assert_equal 4, parent.children.size
265 assert_equal parent.children.sort_by(&:name), parent.children
265 assert_equal parent.children.sort_by(&:name), parent.children
266 end
266 end
267
267
268 def test_rebuild_should_sort_children_alphabetically
268 def test_rebuild_should_sort_children_alphabetically
269 ProjectCustomField.delete_all
269 ProjectCustomField.delete_all
270 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
270 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
271 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
271 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
272 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
272 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
273 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
273 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
274 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
274 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
275
275
276 Project.update_all("lft = NULL, rgt = NULL")
276 Project.update_all("lft = NULL, rgt = NULL")
277 Project.rebuild!
277 Project.rebuild!
278
278
279 parent.reload
279 parent.reload
280 assert_equal 4, parent.children.size
280 assert_equal 4, parent.children.size
281 assert_equal parent.children.sort_by(&:name), parent.children
281 assert_equal parent.children.sort_by(&:name), parent.children
282 end
282 end
283
283
284
284
285 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
285 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
286 # Parent issue with a hierarchy project's fixed version
286 # Parent issue with a hierarchy project's fixed version
287 parent_issue = Issue.find(1)
287 parent_issue = Issue.find(1)
288 parent_issue.update_attribute(:fixed_version_id, 4)
288 parent_issue.update_attribute(:fixed_version_id, 4)
289 parent_issue.reload
289 parent_issue.reload
290 assert_equal 4, parent_issue.fixed_version_id
290 assert_equal 4, parent_issue.fixed_version_id
291
291
292 # Should keep fixed versions for the issues
292 # Should keep fixed versions for the issues
293 issue_with_local_fixed_version = Issue.find(5)
293 issue_with_local_fixed_version = Issue.find(5)
294 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
294 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
295 issue_with_local_fixed_version.reload
295 issue_with_local_fixed_version.reload
296 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
296 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
297
297
298 # Local issue with hierarchy fixed_version
298 # Local issue with hierarchy fixed_version
299 issue_with_hierarchy_fixed_version = Issue.find(13)
299 issue_with_hierarchy_fixed_version = Issue.find(13)
300 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
300 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
301 issue_with_hierarchy_fixed_version.reload
301 issue_with_hierarchy_fixed_version.reload
302 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
302 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
303
303
304 # Move project out of the issue's hierarchy
304 # Move project out of the issue's hierarchy
305 moved_project = Project.find(3)
305 moved_project = Project.find(3)
306 moved_project.set_parent!(Project.find(2))
306 moved_project.set_parent!(Project.find(2))
307 parent_issue.reload
307 parent_issue.reload
308 issue_with_local_fixed_version.reload
308 issue_with_local_fixed_version.reload
309 issue_with_hierarchy_fixed_version.reload
309 issue_with_hierarchy_fixed_version.reload
310
310
311 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
311 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
312 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"
312 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"
313 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
313 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
314 end
314 end
315
315
316 def test_parent
316 def test_parent
317 p = Project.find(6).parent
317 p = Project.find(6).parent
318 assert p.is_a?(Project)
318 assert p.is_a?(Project)
319 assert_equal 5, p.id
319 assert_equal 5, p.id
320 end
320 end
321
321
322 def test_ancestors
322 def test_ancestors
323 a = Project.find(6).ancestors
323 a = Project.find(6).ancestors
324 assert a.first.is_a?(Project)
324 assert a.first.is_a?(Project)
325 assert_equal [1, 5], a.collect(&:id)
325 assert_equal [1, 5], a.collect(&:id)
326 end
326 end
327
327
328 def test_root
328 def test_root
329 r = Project.find(6).root
329 r = Project.find(6).root
330 assert r.is_a?(Project)
330 assert r.is_a?(Project)
331 assert_equal 1, r.id
331 assert_equal 1, r.id
332 end
332 end
333
333
334 def test_children
334 def test_children
335 c = Project.find(1).children
335 c = Project.find(1).children
336 assert c.first.is_a?(Project)
336 assert c.first.is_a?(Project)
337 assert_equal [5, 3, 4], c.collect(&:id)
337 assert_equal [5, 3, 4], c.collect(&:id)
338 end
338 end
339
339
340 def test_descendants
340 def test_descendants
341 d = Project.find(1).descendants
341 d = Project.find(1).descendants
342 assert d.first.is_a?(Project)
342 assert d.first.is_a?(Project)
343 assert_equal [5, 6, 3, 4], d.collect(&:id)
343 assert_equal [5, 6, 3, 4], d.collect(&:id)
344 end
344 end
345
345
346 def test_allowed_parents_should_be_empty_for_non_member_user
346 def test_allowed_parents_should_be_empty_for_non_member_user
347 Role.non_member.add_permission!(:add_project)
347 Role.non_member.add_permission!(:add_project)
348 user = User.find(9)
348 user = User.find(9)
349 assert user.memberships.empty?
349 assert user.memberships.empty?
350 User.current = user
350 User.current = user
351 assert Project.new.allowed_parents.compact.empty?
351 assert Project.new.allowed_parents.compact.empty?
352 end
352 end
353
353
354 def test_allowed_parents_with_add_subprojects_permission
354 def test_allowed_parents_with_add_subprojects_permission
355 Role.find(1).remove_permission!(:add_project)
355 Role.find(1).remove_permission!(:add_project)
356 Role.find(1).add_permission!(:add_subprojects)
356 Role.find(1).add_permission!(:add_subprojects)
357 User.current = User.find(2)
357 User.current = User.find(2)
358 # new project
358 # new project
359 assert !Project.new.allowed_parents.include?(nil)
359 assert !Project.new.allowed_parents.include?(nil)
360 assert Project.new.allowed_parents.include?(Project.find(1))
360 assert Project.new.allowed_parents.include?(Project.find(1))
361 # existing root project
361 # existing root project
362 assert Project.find(1).allowed_parents.include?(nil)
362 assert Project.find(1).allowed_parents.include?(nil)
363 # existing child
363 # existing child
364 assert Project.find(3).allowed_parents.include?(Project.find(1))
364 assert Project.find(3).allowed_parents.include?(Project.find(1))
365 assert !Project.find(3).allowed_parents.include?(nil)
365 assert !Project.find(3).allowed_parents.include?(nil)
366 end
366 end
367
367
368 def test_allowed_parents_with_add_project_permission
368 def test_allowed_parents_with_add_project_permission
369 Role.find(1).add_permission!(:add_project)
369 Role.find(1).add_permission!(:add_project)
370 Role.find(1).remove_permission!(:add_subprojects)
370 Role.find(1).remove_permission!(:add_subprojects)
371 User.current = User.find(2)
371 User.current = User.find(2)
372 # new project
372 # new project
373 assert Project.new.allowed_parents.include?(nil)
373 assert Project.new.allowed_parents.include?(nil)
374 assert !Project.new.allowed_parents.include?(Project.find(1))
374 assert !Project.new.allowed_parents.include?(Project.find(1))
375 # existing root project
375 # existing root project
376 assert Project.find(1).allowed_parents.include?(nil)
376 assert Project.find(1).allowed_parents.include?(nil)
377 # existing child
377 # existing child
378 assert Project.find(3).allowed_parents.include?(Project.find(1))
378 assert Project.find(3).allowed_parents.include?(Project.find(1))
379 assert Project.find(3).allowed_parents.include?(nil)
379 assert Project.find(3).allowed_parents.include?(nil)
380 end
380 end
381
381
382 def test_allowed_parents_with_add_project_and_subprojects_permission
382 def test_allowed_parents_with_add_project_and_subprojects_permission
383 Role.find(1).add_permission!(:add_project)
383 Role.find(1).add_permission!(:add_project)
384 Role.find(1).add_permission!(:add_subprojects)
384 Role.find(1).add_permission!(:add_subprojects)
385 User.current = User.find(2)
385 User.current = User.find(2)
386 # new project
386 # new project
387 assert Project.new.allowed_parents.include?(nil)
387 assert Project.new.allowed_parents.include?(nil)
388 assert Project.new.allowed_parents.include?(Project.find(1))
388 assert Project.new.allowed_parents.include?(Project.find(1))
389 # existing root project
389 # existing root project
390 assert Project.find(1).allowed_parents.include?(nil)
390 assert Project.find(1).allowed_parents.include?(nil)
391 # existing child
391 # existing child
392 assert Project.find(3).allowed_parents.include?(Project.find(1))
392 assert Project.find(3).allowed_parents.include?(Project.find(1))
393 assert Project.find(3).allowed_parents.include?(nil)
393 assert Project.find(3).allowed_parents.include?(nil)
394 end
394 end
395
395
396 def test_users_by_role
396 def test_users_by_role
397 users_by_role = Project.find(1).users_by_role
397 users_by_role = Project.find(1).users_by_role
398 assert_kind_of Hash, users_by_role
398 assert_kind_of Hash, users_by_role
399 role = Role.find(1)
399 role = Role.find(1)
400 assert_kind_of Array, users_by_role[role]
400 assert_kind_of Array, users_by_role[role]
401 assert users_by_role[role].include?(User.find(2))
401 assert users_by_role[role].include?(User.find(2))
402 end
402 end
403
403
404 def test_rolled_up_trackers
404 def test_rolled_up_trackers
405 parent = Project.find(1)
405 parent = Project.find(1)
406 parent.trackers = Tracker.find([1,2])
406 parent.trackers = Tracker.find([1,2])
407 child = parent.children.find(3)
407 child = parent.children.find(3)
408
408
409 assert_equal [1, 2], parent.tracker_ids
409 assert_equal [1, 2], parent.tracker_ids
410 assert_equal [2, 3], child.trackers.collect(&:id)
410 assert_equal [2, 3], child.trackers.collect(&:id)
411
411
412 assert_kind_of Tracker, parent.rolled_up_trackers.first
412 assert_kind_of Tracker, parent.rolled_up_trackers.first
413 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
413 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
414
414
415 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
415 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
416 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
416 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
417 end
417 end
418
418
419 def test_rolled_up_trackers_should_ignore_archived_subprojects
419 def test_rolled_up_trackers_should_ignore_archived_subprojects
420 parent = Project.find(1)
420 parent = Project.find(1)
421 parent.trackers = Tracker.find([1,2])
421 parent.trackers = Tracker.find([1,2])
422 child = parent.children.find(3)
422 child = parent.children.find(3)
423 child.trackers = Tracker.find([1,3])
423 child.trackers = Tracker.find([1,3])
424 parent.children.each(&:archive)
424 parent.children.each(&:archive)
425
425
426 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
426 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
427 end
427 end
428
428
429 context "#rolled_up_versions" do
429 context "#rolled_up_versions" do
430 setup do
430 setup do
431 @project = Project.generate!
431 @project = Project.generate!
432 @parent_version_1 = Version.generate!(:project => @project)
432 @parent_version_1 = Version.generate!(:project => @project)
433 @parent_version_2 = Version.generate!(:project => @project)
433 @parent_version_2 = Version.generate!(:project => @project)
434 end
434 end
435
435
436 should "include the versions for the current project" do
436 should "include the versions for the current project" do
437 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
437 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
438 end
438 end
439
439
440 should "include versions for a subproject" do
440 should "include versions for a subproject" do
441 @subproject = Project.generate!
441 @subproject = Project.generate!
442 @subproject.set_parent!(@project)
442 @subproject.set_parent!(@project)
443 @subproject_version = Version.generate!(:project => @subproject)
443 @subproject_version = Version.generate!(:project => @subproject)
444
444
445 assert_same_elements [
445 assert_same_elements [
446 @parent_version_1,
446 @parent_version_1,
447 @parent_version_2,
447 @parent_version_2,
448 @subproject_version
448 @subproject_version
449 ], @project.rolled_up_versions
449 ], @project.rolled_up_versions
450 end
450 end
451
451
452 should "include versions for a sub-subproject" do
452 should "include versions for a sub-subproject" do
453 @subproject = Project.generate!
453 @subproject = Project.generate!
454 @subproject.set_parent!(@project)
454 @subproject.set_parent!(@project)
455 @sub_subproject = Project.generate!
455 @sub_subproject = Project.generate!
456 @sub_subproject.set_parent!(@subproject)
456 @sub_subproject.set_parent!(@subproject)
457 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
457 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
458
458
459 @project.reload
459 @project.reload
460
460
461 assert_same_elements [
461 assert_same_elements [
462 @parent_version_1,
462 @parent_version_1,
463 @parent_version_2,
463 @parent_version_2,
464 @sub_subproject_version
464 @sub_subproject_version
465 ], @project.rolled_up_versions
465 ], @project.rolled_up_versions
466 end
466 end
467
467
468
468
469 should "only check active projects" do
469 should "only check active projects" do
470 @subproject = Project.generate!
470 @subproject = Project.generate!
471 @subproject.set_parent!(@project)
471 @subproject.set_parent!(@project)
472 @subproject_version = Version.generate!(:project => @subproject)
472 @subproject_version = Version.generate!(:project => @subproject)
473 assert @subproject.archive
473 assert @subproject.archive
474
474
475 @project.reload
475 @project.reload
476
476
477 assert !@subproject.active?
477 assert !@subproject.active?
478 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
478 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
479 end
479 end
480 end
480 end
481
481
482 def test_shared_versions_none_sharing
482 def test_shared_versions_none_sharing
483 p = Project.find(5)
483 p = Project.find(5)
484 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
484 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
485 assert p.shared_versions.include?(v)
485 assert p.shared_versions.include?(v)
486 assert !p.children.first.shared_versions.include?(v)
486 assert !p.children.first.shared_versions.include?(v)
487 assert !p.root.shared_versions.include?(v)
487 assert !p.root.shared_versions.include?(v)
488 assert !p.siblings.first.shared_versions.include?(v)
488 assert !p.siblings.first.shared_versions.include?(v)
489 assert !p.root.siblings.first.shared_versions.include?(v)
489 assert !p.root.siblings.first.shared_versions.include?(v)
490 end
490 end
491
491
492 def test_shared_versions_descendants_sharing
492 def test_shared_versions_descendants_sharing
493 p = Project.find(5)
493 p = Project.find(5)
494 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
494 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
495 assert p.shared_versions.include?(v)
495 assert p.shared_versions.include?(v)
496 assert p.children.first.shared_versions.include?(v)
496 assert p.children.first.shared_versions.include?(v)
497 assert !p.root.shared_versions.include?(v)
497 assert !p.root.shared_versions.include?(v)
498 assert !p.siblings.first.shared_versions.include?(v)
498 assert !p.siblings.first.shared_versions.include?(v)
499 assert !p.root.siblings.first.shared_versions.include?(v)
499 assert !p.root.siblings.first.shared_versions.include?(v)
500 end
500 end
501
501
502 def test_shared_versions_hierarchy_sharing
502 def test_shared_versions_hierarchy_sharing
503 p = Project.find(5)
503 p = Project.find(5)
504 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
504 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
505 assert p.shared_versions.include?(v)
505 assert p.shared_versions.include?(v)
506 assert p.children.first.shared_versions.include?(v)
506 assert p.children.first.shared_versions.include?(v)
507 assert p.root.shared_versions.include?(v)
507 assert p.root.shared_versions.include?(v)
508 assert !p.siblings.first.shared_versions.include?(v)
508 assert !p.siblings.first.shared_versions.include?(v)
509 assert !p.root.siblings.first.shared_versions.include?(v)
509 assert !p.root.siblings.first.shared_versions.include?(v)
510 end
510 end
511
511
512 def test_shared_versions_tree_sharing
512 def test_shared_versions_tree_sharing
513 p = Project.find(5)
513 p = Project.find(5)
514 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
514 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
515 assert p.shared_versions.include?(v)
515 assert p.shared_versions.include?(v)
516 assert p.children.first.shared_versions.include?(v)
516 assert p.children.first.shared_versions.include?(v)
517 assert p.root.shared_versions.include?(v)
517 assert p.root.shared_versions.include?(v)
518 assert p.siblings.first.shared_versions.include?(v)
518 assert p.siblings.first.shared_versions.include?(v)
519 assert !p.root.siblings.first.shared_versions.include?(v)
519 assert !p.root.siblings.first.shared_versions.include?(v)
520 end
520 end
521
521
522 def test_shared_versions_system_sharing
522 def test_shared_versions_system_sharing
523 p = Project.find(5)
523 p = Project.find(5)
524 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
524 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
525 assert p.shared_versions.include?(v)
525 assert p.shared_versions.include?(v)
526 assert p.children.first.shared_versions.include?(v)
526 assert p.children.first.shared_versions.include?(v)
527 assert p.root.shared_versions.include?(v)
527 assert p.root.shared_versions.include?(v)
528 assert p.siblings.first.shared_versions.include?(v)
528 assert p.siblings.first.shared_versions.include?(v)
529 assert p.root.siblings.first.shared_versions.include?(v)
529 assert p.root.siblings.first.shared_versions.include?(v)
530 end
530 end
531
531
532 def test_shared_versions
532 def test_shared_versions
533 parent = Project.find(1)
533 parent = Project.find(1)
534 child = parent.children.find(3)
534 child = parent.children.find(3)
535 private_child = parent.children.find(5)
535 private_child = parent.children.find(5)
536
536
537 assert_equal [1,2,3], parent.version_ids.sort
537 assert_equal [1,2,3], parent.version_ids.sort
538 assert_equal [4], child.version_ids
538 assert_equal [4], child.version_ids
539 assert_equal [6], private_child.version_ids
539 assert_equal [6], private_child.version_ids
540 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
540 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
541
541
542 assert_equal 6, parent.shared_versions.size
542 assert_equal 6, parent.shared_versions.size
543 parent.shared_versions.each do |version|
543 parent.shared_versions.each do |version|
544 assert_kind_of Version, version
544 assert_kind_of Version, version
545 end
545 end
546
546
547 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
547 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
548 end
548 end
549
549
550 def test_shared_versions_should_ignore_archived_subprojects
550 def test_shared_versions_should_ignore_archived_subprojects
551 parent = Project.find(1)
551 parent = Project.find(1)
552 child = parent.children.find(3)
552 child = parent.children.find(3)
553 child.archive
553 child.archive
554 parent.reload
554 parent.reload
555
555
556 assert_equal [1,2,3], parent.version_ids.sort
556 assert_equal [1,2,3], parent.version_ids.sort
557 assert_equal [4], child.version_ids
557 assert_equal [4], child.version_ids
558 assert !parent.shared_versions.collect(&:id).include?(4)
558 assert !parent.shared_versions.collect(&:id).include?(4)
559 end
559 end
560
560
561 def test_shared_versions_visible_to_user
561 def test_shared_versions_visible_to_user
562 user = User.find(3)
562 user = User.find(3)
563 parent = Project.find(1)
563 parent = Project.find(1)
564 child = parent.children.find(5)
564 child = parent.children.find(5)
565
565
566 assert_equal [1,2,3], parent.version_ids.sort
566 assert_equal [1,2,3], parent.version_ids.sort
567 assert_equal [6], child.version_ids
567 assert_equal [6], child.version_ids
568
568
569 versions = parent.shared_versions.visible(user)
569 versions = parent.shared_versions.visible(user)
570
570
571 assert_equal 4, versions.size
571 assert_equal 4, versions.size
572 versions.each do |version|
572 versions.each do |version|
573 assert_kind_of Version, version
573 assert_kind_of Version, version
574 end
574 end
575
575
576 assert !versions.collect(&:id).include?(6)
576 assert !versions.collect(&:id).include?(6)
577 end
577 end
578
578
579
579
580 def test_next_identifier
580 def test_next_identifier
581 ProjectCustomField.delete_all
581 ProjectCustomField.delete_all
582 Project.create!(:name => 'last', :identifier => 'p2008040')
582 Project.create!(:name => 'last', :identifier => 'p2008040')
583 assert_equal 'p2008041', Project.next_identifier
583 assert_equal 'p2008041', Project.next_identifier
584 end
584 end
585
585
586 def test_next_identifier_first_project
586 def test_next_identifier_first_project
587 Project.delete_all
587 Project.delete_all
588 assert_nil Project.next_identifier
588 assert_nil Project.next_identifier
589 end
589 end
590
590
591 def test_enabled_module_names
591 def test_enabled_module_names
592 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
592 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
593 project = Project.new
593 project = Project.new
594
594
595 project.enabled_module_names = %w(issue_tracking news)
595 project.enabled_module_names = %w(issue_tracking news)
596 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
596 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
597 end
597 end
598 end
598 end
599
599
600 def test_enabled_module_names_should_not_recreate_enabled_modules
600 def test_enabled_module_names_should_not_recreate_enabled_modules
601 project = Project.find(1)
601 project = Project.find(1)
602 # Remove one module
602 # Remove one module
603 modules = project.enabled_modules.slice(0..-2)
603 modules = project.enabled_modules.slice(0..-2)
604 assert modules.any?
604 assert modules.any?
605 assert_difference 'EnabledModule.count', -1 do
605 assert_difference 'EnabledModule.count', -1 do
606 project.enabled_module_names = modules.collect(&:name)
606 project.enabled_module_names = modules.collect(&:name)
607 end
607 end
608 project.reload
608 project.reload
609 # Ids should be preserved
609 # Ids should be preserved
610 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
610 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
611 end
611 end
612
612
613 def test_copy_from_existing_project
613 def test_copy_from_existing_project
614 source_project = Project.find(1)
614 source_project = Project.find(1)
615 copied_project = Project.copy_from(1)
615 copied_project = Project.copy_from(1)
616
616
617 assert copied_project
617 assert copied_project
618 # Cleared attributes
618 # Cleared attributes
619 assert copied_project.id.blank?
619 assert copied_project.id.blank?
620 assert copied_project.name.blank?
620 assert copied_project.name.blank?
621 assert copied_project.identifier.blank?
621 assert copied_project.identifier.blank?
622
622
623 # Duplicated attributes
623 # Duplicated attributes
624 assert_equal source_project.description, copied_project.description
624 assert_equal source_project.description, copied_project.description
625 assert_equal source_project.enabled_modules, copied_project.enabled_modules
625 assert_equal source_project.enabled_modules, copied_project.enabled_modules
626 assert_equal source_project.trackers, copied_project.trackers
626 assert_equal source_project.trackers, copied_project.trackers
627
627
628 # Default attributes
628 # Default attributes
629 assert_equal 1, copied_project.status
629 assert_equal 1, copied_project.status
630 end
630 end
631
631
632 def test_activities_should_use_the_system_activities
632 def test_activities_should_use_the_system_activities
633 project = Project.find(1)
633 project = Project.find(1)
634 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
634 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
635 end
635 end
636
636
637
637
638 def test_activities_should_use_the_project_specific_activities
638 def test_activities_should_use_the_project_specific_activities
639 project = Project.find(1)
639 project = Project.find(1)
640 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
640 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
641 assert overridden_activity.save!
641 assert overridden_activity.save!
642
642
643 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
643 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
644 end
644 end
645
645
646 def test_activities_should_not_include_the_inactive_project_specific_activities
646 def test_activities_should_not_include_the_inactive_project_specific_activities
647 project = Project.find(1)
647 project = Project.find(1)
648 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
648 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
649 assert overridden_activity.save!
649 assert overridden_activity.save!
650
650
651 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
651 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
652 end
652 end
653
653
654 def test_activities_should_not_include_project_specific_activities_from_other_projects
654 def test_activities_should_not_include_project_specific_activities_from_other_projects
655 project = Project.find(1)
655 project = Project.find(1)
656 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
656 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
657 assert overridden_activity.save!
657 assert overridden_activity.save!
658
658
659 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
659 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
660 end
660 end
661
661
662 def test_activities_should_handle_nils
662 def test_activities_should_handle_nils
663 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
663 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
664 TimeEntryActivity.delete_all
664 TimeEntryActivity.delete_all
665
665
666 # No activities
666 # No activities
667 project = Project.find(1)
667 project = Project.find(1)
668 assert project.activities.empty?
668 assert project.activities.empty?
669
669
670 # No system, one overridden
670 # No system, one overridden
671 assert overridden_activity.save!
671 assert overridden_activity.save!
672 project.reload
672 project.reload
673 assert_equal [overridden_activity], project.activities
673 assert_equal [overridden_activity], project.activities
674 end
674 end
675
675
676 def test_activities_should_override_system_activities_with_project_activities
676 def test_activities_should_override_system_activities_with_project_activities
677 project = Project.find(1)
677 project = Project.find(1)
678 parent_activity = TimeEntryActivity.find(:first)
678 parent_activity = TimeEntryActivity.find(:first)
679 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
679 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
680 assert overridden_activity.save!
680 assert overridden_activity.save!
681
681
682 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
682 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
683 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
683 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
684 end
684 end
685
685
686 def test_activities_should_include_inactive_activities_if_specified
686 def test_activities_should_include_inactive_activities_if_specified
687 project = Project.find(1)
687 project = Project.find(1)
688 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
688 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
689 assert overridden_activity.save!
689 assert overridden_activity.save!
690
690
691 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
691 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
692 end
692 end
693
693
694 test 'activities should not include active System activities if the project has an override that is inactive' do
694 test 'activities should not include active System activities if the project has an override that is inactive' do
695 project = Project.find(1)
695 project = Project.find(1)
696 system_activity = TimeEntryActivity.find_by_name('Design')
696 system_activity = TimeEntryActivity.find_by_name('Design')
697 assert system_activity.active?
697 assert system_activity.active?
698 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
698 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
699 assert overridden_activity.save!
699 assert overridden_activity.save!
700
700
701 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
701 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
702 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
702 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
703 end
703 end
704
704
705 def test_close_completed_versions
705 def test_close_completed_versions
706 Version.update_all("status = 'open'")
706 Version.update_all("status = 'open'")
707 project = Project.find(1)
707 project = Project.find(1)
708 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
708 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
709 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
709 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
710 project.close_completed_versions
710 project.close_completed_versions
711 project.reload
711 project.reload
712 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
712 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
713 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
713 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
714 end
714 end
715
715
716 context "Project#copy" do
716 context "Project#copy" do
717 setup do
717 setup do
718 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
718 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
719 Project.destroy_all :identifier => "copy-test"
719 Project.destroy_all :identifier => "copy-test"
720 @source_project = Project.find(2)
720 @source_project = Project.find(2)
721 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
721 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
722 @project.trackers = @source_project.trackers
722 @project.trackers = @source_project.trackers
723 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
723 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
724 end
724 end
725
725
726 should "copy issues" do
726 should "copy issues" do
727 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
727 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
728 :subject => "copy issue status",
728 :subject => "copy issue status",
729 :tracker_id => 1,
729 :tracker_id => 1,
730 :assigned_to_id => 2,
730 :assigned_to_id => 2,
731 :project_id => @source_project.id)
731 :project_id => @source_project.id)
732 assert @project.valid?
732 assert @project.valid?
733 assert @project.issues.empty?
733 assert @project.issues.empty?
734 assert @project.copy(@source_project)
734 assert @project.copy(@source_project)
735
735
736 assert_equal @source_project.issues.size, @project.issues.size
736 assert_equal @source_project.issues.size, @project.issues.size
737 @project.issues.each do |issue|
737 @project.issues.each do |issue|
738 assert issue.valid?
738 assert issue.valid?
739 assert ! issue.assigned_to.blank?
739 assert ! issue.assigned_to.blank?
740 assert_equal @project, issue.project
740 assert_equal @project, issue.project
741 end
741 end
742
742
743 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
743 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
744 assert copied_issue
744 assert copied_issue
745 assert copied_issue.status
745 assert copied_issue.status
746 assert_equal "Closed", copied_issue.status.name
746 assert_equal "Closed", copied_issue.status.name
747 end
747 end
748
748
749 should "change the new issues to use the copied version" do
749 should "change the new issues to use the copied version" do
750 User.current = User.find(1)
750 User.current = User.find(1)
751 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
751 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
752 @source_project.versions << assigned_version
752 @source_project.versions << assigned_version
753 assert_equal 3, @source_project.versions.size
753 assert_equal 3, @source_project.versions.size
754 Issue.generate_for_project!(@source_project,
754 Issue.generate_for_project!(@source_project,
755 :fixed_version_id => assigned_version.id,
755 :fixed_version_id => assigned_version.id,
756 :subject => "change the new issues to use the copied version",
756 :subject => "change the new issues to use the copied version",
757 :tracker_id => 1,
757 :tracker_id => 1,
758 :project_id => @source_project.id)
758 :project_id => @source_project.id)
759
759
760 assert @project.copy(@source_project)
760 assert @project.copy(@source_project)
761 @project.reload
761 @project.reload
762 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
762 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
763
763
764 assert copied_issue
764 assert copied_issue
765 assert copied_issue.fixed_version
765 assert copied_issue.fixed_version
766 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
766 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
767 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
767 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
768 end
768 end
769
769
770 should "copy issue relations" do
770 should "copy issue relations" do
771 Setting.cross_project_issue_relations = '1'
771 Setting.cross_project_issue_relations = '1'
772
772
773 second_issue = Issue.generate!(:status_id => 5,
773 second_issue = Issue.generate!(:status_id => 5,
774 :subject => "copy issue relation",
774 :subject => "copy issue relation",
775 :tracker_id => 1,
775 :tracker_id => 1,
776 :assigned_to_id => 2,
776 :assigned_to_id => 2,
777 :project_id => @source_project.id)
777 :project_id => @source_project.id)
778 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
778 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
779 :issue_to => second_issue,
779 :issue_to => second_issue,
780 :relation_type => "relates")
780 :relation_type => "relates")
781 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
781 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
782 :issue_to => second_issue,
782 :issue_to => second_issue,
783 :relation_type => "duplicates")
783 :relation_type => "duplicates")
784
784
785 assert @project.copy(@source_project)
785 assert @project.copy(@source_project)
786 assert_equal @source_project.issues.count, @project.issues.count
786 assert_equal @source_project.issues.count, @project.issues.count
787 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
787 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
788 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
788 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
789
789
790 # First issue with a relation on project
790 # First issue with a relation on project
791 assert_equal 1, copied_issue.relations.size, "Relation not copied"
791 assert_equal 1, copied_issue.relations.size, "Relation not copied"
792 copied_relation = copied_issue.relations.first
792 copied_relation = copied_issue.relations.first
793 assert_equal "relates", copied_relation.relation_type
793 assert_equal "relates", copied_relation.relation_type
794 assert_equal copied_second_issue.id, copied_relation.issue_to_id
794 assert_equal copied_second_issue.id, copied_relation.issue_to_id
795 assert_not_equal source_relation.id, copied_relation.id
795 assert_not_equal source_relation.id, copied_relation.id
796
796
797 # Second issue with a cross project relation
797 # Second issue with a cross project relation
798 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
798 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
799 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
799 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
800 assert_equal "duplicates", copied_relation.relation_type
800 assert_equal "duplicates", copied_relation.relation_type
801 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
801 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
802 assert_not_equal source_relation_cross_project.id, copied_relation.id
802 assert_not_equal source_relation_cross_project.id, copied_relation.id
803 end
803 end
804
804
805 should "copy memberships" do
805 should "copy memberships" do
806 assert @project.valid?
806 assert @project.valid?
807 assert @project.members.empty?
807 assert @project.members.empty?
808 assert @project.copy(@source_project)
808 assert @project.copy(@source_project)
809
809
810 assert_equal @source_project.memberships.size, @project.memberships.size
810 assert_equal @source_project.memberships.size, @project.memberships.size
811 @project.memberships.each do |membership|
811 @project.memberships.each do |membership|
812 assert membership
812 assert membership
813 assert_equal @project, membership.project
813 assert_equal @project, membership.project
814 end
814 end
815 end
815 end
816
816
817 should "copy memberships with groups and additional roles" do
817 should "copy memberships with groups and additional roles" do
818 group = Group.create!(:lastname => "Copy group")
818 group = Group.create!(:lastname => "Copy group")
819 user = User.find(7)
819 user = User.find(7)
820 group.users << user
820 group.users << user
821 # group role
821 # group role
822 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
822 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
823 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
823 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
824 # additional role
824 # additional role
825 member.role_ids = [1]
825 member.role_ids = [1]
826
826
827 assert @project.copy(@source_project)
827 assert @project.copy(@source_project)
828 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
828 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
829 assert_not_nil member
829 assert_not_nil member
830 assert_equal [1, 2], member.role_ids.sort
830 assert_equal [1, 2], member.role_ids.sort
831 end
831 end
832
832
833 should "copy project specific queries" do
833 should "copy project specific queries" do
834 assert @project.valid?
834 assert @project.valid?
835 assert @project.queries.empty?
835 assert @project.queries.empty?
836 assert @project.copy(@source_project)
836 assert @project.copy(@source_project)
837
837
838 assert_equal @source_project.queries.size, @project.queries.size
838 assert_equal @source_project.queries.size, @project.queries.size
839 @project.queries.each do |query|
839 @project.queries.each do |query|
840 assert query
840 assert query
841 assert_equal @project, query.project
841 assert_equal @project, query.project
842 end
842 end
843 end
843 end
844
844
845 should "copy versions" do
845 should "copy versions" do
846 @source_project.versions << Version.generate!
846 @source_project.versions << Version.generate!
847 @source_project.versions << Version.generate!
847 @source_project.versions << Version.generate!
848
848
849 assert @project.versions.empty?
849 assert @project.versions.empty?
850 assert @project.copy(@source_project)
850 assert @project.copy(@source_project)
851
851
852 assert_equal @source_project.versions.size, @project.versions.size
852 assert_equal @source_project.versions.size, @project.versions.size
853 @project.versions.each do |version|
853 @project.versions.each do |version|
854 assert version
854 assert version
855 assert_equal @project, version.project
855 assert_equal @project, version.project
856 end
856 end
857 end
857 end
858
858
859 should "copy wiki" do
859 should "copy wiki" do
860 assert_difference 'Wiki.count' do
860 assert_difference 'Wiki.count' do
861 assert @project.copy(@source_project)
861 assert @project.copy(@source_project)
862 end
862 end
863
863
864 assert @project.wiki
864 assert @project.wiki
865 assert_not_equal @source_project.wiki, @project.wiki
865 assert_not_equal @source_project.wiki, @project.wiki
866 assert_equal "Start page", @project.wiki.start_page
866 assert_equal "Start page", @project.wiki.start_page
867 end
867 end
868
868
869 should "copy wiki pages and content with hierarchy" do
869 should "copy wiki pages and content with hierarchy" do
870 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
870 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
871 assert @project.copy(@source_project)
871 assert @project.copy(@source_project)
872 end
872 end
873
873
874 assert @project.wiki
874 assert @project.wiki
875 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
875 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
876
876
877 @project.wiki.pages.each do |wiki_page|
877 @project.wiki.pages.each do |wiki_page|
878 assert wiki_page.content
878 assert wiki_page.content
879 assert !@source_project.wiki.pages.include?(wiki_page)
879 assert !@source_project.wiki.pages.include?(wiki_page)
880 end
880 end
881
881
882 parent = @project.wiki.find_page('Parent_page')
882 parent = @project.wiki.find_page('Parent_page')
883 child1 = @project.wiki.find_page('Child_page_1')
883 child1 = @project.wiki.find_page('Child_page_1')
884 child2 = @project.wiki.find_page('Child_page_2')
884 child2 = @project.wiki.find_page('Child_page_2')
885 assert_equal parent, child1.parent
885 assert_equal parent, child1.parent
886 assert_equal parent, child2.parent
886 assert_equal parent, child2.parent
887 end
887 end
888
888
889 should "copy issue categories" do
889 should "copy issue categories" do
890 assert @project.copy(@source_project)
890 assert @project.copy(@source_project)
891
891
892 assert_equal 2, @project.issue_categories.size
892 assert_equal 2, @project.issue_categories.size
893 @project.issue_categories.each do |issue_category|
893 @project.issue_categories.each do |issue_category|
894 assert !@source_project.issue_categories.include?(issue_category)
894 assert !@source_project.issue_categories.include?(issue_category)
895 end
895 end
896 end
896 end
897
897
898 should "copy boards" do
898 should "copy boards" do
899 assert @project.copy(@source_project)
899 assert @project.copy(@source_project)
900
900
901 assert_equal 1, @project.boards.size
901 assert_equal 1, @project.boards.size
902 @project.boards.each do |board|
902 @project.boards.each do |board|
903 assert !@source_project.boards.include?(board)
903 assert !@source_project.boards.include?(board)
904 end
904 end
905 end
905 end
906
906
907 should "change the new issues to use the copied issue categories" do
907 should "change the new issues to use the copied issue categories" do
908 issue = Issue.find(4)
908 issue = Issue.find(4)
909 issue.update_attribute(:category_id, 3)
909 issue.update_attribute(:category_id, 3)
910
910
911 assert @project.copy(@source_project)
911 assert @project.copy(@source_project)
912
912
913 @project.issues.each do |issue|
913 @project.issues.each do |issue|
914 assert issue.category
914 assert issue.category
915 assert_equal "Stock management", issue.category.name # Same name
915 assert_equal "Stock management", issue.category.name # Same name
916 assert_not_equal IssueCategory.find(3), issue.category # Different record
916 assert_not_equal IssueCategory.find(3), issue.category # Different record
917 end
917 end
918 end
918 end
919
919
920 should "limit copy with :only option" do
920 should "limit copy with :only option" do
921 assert @project.members.empty?
921 assert @project.members.empty?
922 assert @project.issue_categories.empty?
922 assert @project.issue_categories.empty?
923 assert @source_project.issues.any?
923 assert @source_project.issues.any?
924
924
925 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
925 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
926
926
927 assert @project.members.any?
927 assert @project.members.any?
928 assert @project.issue_categories.any?
928 assert @project.issue_categories.any?
929 assert @project.issues.empty?
929 assert @project.issues.empty?
930 end
930 end
931
931
932 end
932 end
933
933
934 context "#start_date" do
934 context "#start_date" do
935 setup do
935 setup do
936 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
936 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
937 @project = Project.generate!(:identifier => 'test0')
937 @project = Project.generate!(:identifier => 'test0')
938 @project.trackers << Tracker.generate!
938 @project.trackers << Tracker.generate!
939 end
939 end
940
940
941 should "be nil if there are no issues on the project" do
941 should "be nil if there are no issues on the project" do
942 assert_nil @project.start_date
942 assert_nil @project.start_date
943 end
943 end
944
944
945 should "be tested when issues have no start date"
945 should "be tested when issues have no start date"
946
946
947 should "be the earliest start date of it's issues" do
947 should "be the earliest start date of it's issues" do
948 early = 7.days.ago.to_date
948 early = 7.days.ago.to_date
949 Issue.generate_for_project!(@project, :start_date => Date.today)
949 Issue.generate_for_project!(@project, :start_date => Date.today)
950 Issue.generate_for_project!(@project, :start_date => early)
950 Issue.generate_for_project!(@project, :start_date => early)
951
951
952 assert_equal early, @project.start_date
952 assert_equal early, @project.start_date
953 end
953 end
954
954
955 end
955 end
956
956
957 context "#due_date" do
957 context "#due_date" do
958 setup do
958 setup do
959 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
959 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
960 @project = Project.generate!(:identifier => 'test0')
960 @project = Project.generate!(:identifier => 'test0')
961 @project.trackers << Tracker.generate!
961 @project.trackers << Tracker.generate!
962 end
962 end
963
963
964 should "be nil if there are no issues on the project" do
964 should "be nil if there are no issues on the project" do
965 assert_nil @project.due_date
965 assert_nil @project.due_date
966 end
966 end
967
967
968 should "be tested when issues have no due date"
968 should "be tested when issues have no due date"
969
969
970 should "be the latest due date of it's issues" do
970 should "be the latest due date of it's issues" do
971 future = 7.days.from_now.to_date
971 future = 7.days.from_now.to_date
972 Issue.generate_for_project!(@project, :due_date => future)
972 Issue.generate_for_project!(@project, :due_date => future)
973 Issue.generate_for_project!(@project, :due_date => Date.today)
973 Issue.generate_for_project!(@project, :due_date => Date.today)
974
974
975 assert_equal future, @project.due_date
975 assert_equal future, @project.due_date
976 end
976 end
977
977
978 should "be the latest due date of it's versions" do
978 should "be the latest due date of it's versions" do
979 future = 7.days.from_now.to_date
979 future = 7.days.from_now.to_date
980 @project.versions << Version.generate!(:effective_date => future)
980 @project.versions << Version.generate!(:effective_date => future)
981 @project.versions << Version.generate!(:effective_date => Date.today)
981 @project.versions << Version.generate!(:effective_date => Date.today)
982
982
983
983
984 assert_equal future, @project.due_date
984 assert_equal future, @project.due_date
985
985
986 end
986 end
987
987
988 should "pick the latest date from it's issues and versions" do
988 should "pick the latest date from it's issues and versions" do
989 future = 7.days.from_now.to_date
989 future = 7.days.from_now.to_date
990 far_future = 14.days.from_now.to_date
990 far_future = 14.days.from_now.to_date
991 Issue.generate_for_project!(@project, :due_date => far_future)
991 Issue.generate_for_project!(@project, :due_date => far_future)
992 @project.versions << Version.generate!(:effective_date => future)
992 @project.versions << Version.generate!(:effective_date => future)
993
993
994 assert_equal far_future, @project.due_date
994 assert_equal far_future, @project.due_date
995 end
995 end
996
996
997 end
997 end
998
998
999 context "Project#completed_percent" do
999 context "Project#completed_percent" do
1000 setup do
1000 setup do
1001 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1001 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1002 @project = Project.generate!(:identifier => 'test0')
1002 @project = Project.generate!(:identifier => 'test0')
1003 @project.trackers << Tracker.generate!
1003 @project.trackers << Tracker.generate!
1004 end
1004 end
1005
1005
1006 context "no versions" do
1006 context "no versions" do
1007 should "be 100" do
1007 should "be 100" do
1008 assert_equal 100, @project.completed_percent
1008 assert_equal 100, @project.completed_percent
1009 end
1009 end
1010 end
1010 end
1011
1011
1012 context "with versions" do
1012 context "with versions" do
1013 should "return 0 if the versions have no issues" do
1013 should "return 0 if the versions have no issues" do
1014 Version.generate!(:project => @project)
1014 Version.generate!(:project => @project)
1015 Version.generate!(:project => @project)
1015 Version.generate!(:project => @project)
1016
1016
1017 assert_equal 0, @project.completed_percent
1017 assert_equal 0, @project.completed_percent
1018 end
1018 end
1019
1019
1020 should "return 100 if the version has only closed issues" do
1020 should "return 100 if the version has only closed issues" do
1021 v1 = Version.generate!(:project => @project)
1021 v1 = Version.generate!(:project => @project)
1022 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
1022 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
1023 v2 = Version.generate!(:project => @project)
1023 v2 = Version.generate!(:project => @project)
1024 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
1024 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
1025
1025
1026 assert_equal 100, @project.completed_percent
1026 assert_equal 100, @project.completed_percent
1027 end
1027 end
1028
1028
1029 should "return the averaged completed percent of the versions (not weighted)" do
1029 should "return the averaged completed percent of the versions (not weighted)" do
1030 v1 = Version.generate!(:project => @project)
1030 v1 = Version.generate!(:project => @project)
1031 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
1031 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
1032 v2 = Version.generate!(:project => @project)
1032 v2 = Version.generate!(:project => @project)
1033 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
1033 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
1034
1034
1035 assert_equal 50, @project.completed_percent
1035 assert_equal 50, @project.completed_percent
1036 end
1036 end
1037
1037
1038 end
1038 end
1039 end
1039 end
1040
1040
1041 context "#notified_users" do
1041 context "#notified_users" do
1042 setup do
1042 setup do
1043 @project = Project.generate!
1043 @project = Project.generate!
1044 @role = Role.generate!
1044 @role = Role.generate!
1045
1045
1046 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1046 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1047 Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1047 Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1048
1048
1049 @all_events_user = User.generate!(:mail_notification => 'all')
1049 @all_events_user = User.generate!(:mail_notification => 'all')
1050 Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user)
1050 Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user)
1051
1051
1052 @no_events_user = User.generate!(:mail_notification => 'none')
1052 @no_events_user = User.generate!(:mail_notification => 'none')
1053 Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user)
1053 Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user)
1054
1054
1055 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1055 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1056 Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1056 Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1057
1057
1058 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1058 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1059 Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1059 Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1060
1060
1061 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1061 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1062 Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1062 Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1063 end
1063 end
1064
1064
1065 should "include members with a mail notification" do
1065 should "include members with a mail notification" do
1066 assert @project.notified_users.include?(@user_with_membership_notification)
1066 assert @project.notified_users.include?(@user_with_membership_notification)
1067 end
1067 end
1068
1068
1069 should "include users with the 'all' notification option" do
1069 should "include users with the 'all' notification option" do
1070 assert @project.notified_users.include?(@all_events_user)
1070 assert @project.notified_users.include?(@all_events_user)
1071 end
1071 end
1072
1072
1073 should "not include users with the 'none' notification option" do
1073 should "not include users with the 'none' notification option" do
1074 assert !@project.notified_users.include?(@no_events_user)
1074 assert !@project.notified_users.include?(@no_events_user)
1075 end
1075 end
1076
1076
1077 should "not include users with the 'only_my_events' notification option" do
1077 should "not include users with the 'only_my_events' notification option" do
1078 assert !@project.notified_users.include?(@only_my_events_user)
1078 assert !@project.notified_users.include?(@only_my_events_user)
1079 end
1079 end
1080
1080
1081 should "not include users with the 'only_assigned' notification option" do
1081 should "not include users with the 'only_assigned' notification option" do
1082 assert !@project.notified_users.include?(@only_assigned_user)
1082 assert !@project.notified_users.include?(@only_assigned_user)
1083 end
1083 end
1084
1084
1085 should "not include users with the 'only_owner' notification option" do
1085 should "not include users with the 'only_owner' notification option" do
1086 assert !@project.notified_users.include?(@only_owned_user)
1086 assert !@project.notified_users.include?(@only_owned_user)
1087 end
1087 end
1088 end
1088 end
1089
1089
1090 end
1090 end
General Comments 0
You need to be logged in to leave comments. Login now