##// END OF EJS Templates
Added Project#enable_module! and Project#disable_module! (#7115)...
Jean-Baptiste Barth -
r5978:88bd76b0e494
parent child
Show More
@@ -1,857 +1,877
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 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 => :destroy, :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 {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
87 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
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 visible(user).find(:all, :limit => count, :order => "created_on DESC")
118 visible(user).find(:all, :limit => count, :order => "created_on DESC")
119 end
119 end
120
120
121 def self.visible_by(user=nil)
121 def self.visible_by(user=nil)
122 ActiveSupport::Deprecation.warn "Project.visible_by is deprecated and will be removed in Redmine 1.3.0. Use Project.visible_condition instead."
122 ActiveSupport::Deprecation.warn "Project.visible_by is deprecated and will be removed in Redmine 1.3.0. Use Project.visible_condition instead."
123 visible_condition(user || User.current)
123 visible_condition(user || User.current)
124 end
124 end
125
125
126 # Returns a SQL conditions string used to find all projects visible by the specified user.
126 # Returns a SQL conditions string used to find all projects visible by the specified user.
127 #
127 #
128 # Examples:
128 # Examples:
129 # Project.visible_condition(admin) => "projects.status = 1"
129 # Project.visible_condition(admin) => "projects.status = 1"
130 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
130 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
131 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
131 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
132 def self.visible_condition(user, options={})
132 def self.visible_condition(user, options={})
133 allowed_to_condition(user, :view_project, options)
133 allowed_to_condition(user, :view_project, options)
134 end
134 end
135
135
136 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
136 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
137 #
137 #
138 # Valid options:
138 # Valid options:
139 # * :project => limit the condition to project
139 # * :project => limit the condition to project
140 # * :with_subprojects => limit the condition to project and its subprojects
140 # * :with_subprojects => limit the condition to project and its subprojects
141 # * :member => limit the condition to the user projects
141 # * :member => limit the condition to the user projects
142 def self.allowed_to_condition(user, permission, options={})
142 def self.allowed_to_condition(user, permission, options={})
143 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
143 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
144 if perm = Redmine::AccessControl.permission(permission)
144 if perm = Redmine::AccessControl.permission(permission)
145 unless perm.project_module.nil?
145 unless perm.project_module.nil?
146 # If the permission belongs to a project module, make sure the module is enabled
146 # If the permission belongs to a project module, make sure the module is enabled
147 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
147 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
148 end
148 end
149 end
149 end
150 if options[:project]
150 if options[:project]
151 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
151 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
152 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
152 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
153 base_statement = "(#{project_statement}) AND (#{base_statement})"
153 base_statement = "(#{project_statement}) AND (#{base_statement})"
154 end
154 end
155
155
156 if user.admin?
156 if user.admin?
157 base_statement
157 base_statement
158 else
158 else
159 statement_by_role = {}
159 statement_by_role = {}
160 unless options[:member]
160 unless options[:member]
161 role = user.logged? ? Role.non_member : Role.anonymous
161 role = user.logged? ? Role.non_member : Role.anonymous
162 if role.allowed_to?(permission)
162 if role.allowed_to?(permission)
163 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
163 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
164 end
164 end
165 end
165 end
166 if user.logged?
166 if user.logged?
167 user.projects_by_role.each do |role, projects|
167 user.projects_by_role.each do |role, projects|
168 if role.allowed_to?(permission)
168 if role.allowed_to?(permission)
169 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
169 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
170 end
170 end
171 end
171 end
172 end
172 end
173 if statement_by_role.empty?
173 if statement_by_role.empty?
174 "1=0"
174 "1=0"
175 else
175 else
176 if block_given?
176 if block_given?
177 statement_by_role.each do |role, statement|
177 statement_by_role.each do |role, statement|
178 if s = yield(role, user)
178 if s = yield(role, user)
179 statement_by_role[role] = "(#{statement} AND (#{s}))"
179 statement_by_role[role] = "(#{statement} AND (#{s}))"
180 end
180 end
181 end
181 end
182 end
182 end
183 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
183 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
184 end
184 end
185 end
185 end
186 end
186 end
187
187
188 # Returns the Systemwide and project specific activities
188 # Returns the Systemwide and project specific activities
189 def activities(include_inactive=false)
189 def activities(include_inactive=false)
190 if include_inactive
190 if include_inactive
191 return all_activities
191 return all_activities
192 else
192 else
193 return active_activities
193 return active_activities
194 end
194 end
195 end
195 end
196
196
197 # Will create a new Project specific Activity or update an existing one
197 # Will create a new Project specific Activity or update an existing one
198 #
198 #
199 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
199 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
200 # does not successfully save.
200 # does not successfully save.
201 def update_or_create_time_entry_activity(id, activity_hash)
201 def update_or_create_time_entry_activity(id, activity_hash)
202 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
202 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
203 self.create_time_entry_activity_if_needed(activity_hash)
203 self.create_time_entry_activity_if_needed(activity_hash)
204 else
204 else
205 activity = project.time_entry_activities.find_by_id(id.to_i)
205 activity = project.time_entry_activities.find_by_id(id.to_i)
206 activity.update_attributes(activity_hash) if activity
206 activity.update_attributes(activity_hash) if activity
207 end
207 end
208 end
208 end
209
209
210 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
210 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
211 #
211 #
212 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
212 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
213 # does not successfully save.
213 # does not successfully save.
214 def create_time_entry_activity_if_needed(activity)
214 def create_time_entry_activity_if_needed(activity)
215 if activity['parent_id']
215 if activity['parent_id']
216
216
217 parent_activity = TimeEntryActivity.find(activity['parent_id'])
217 parent_activity = TimeEntryActivity.find(activity['parent_id'])
218 activity['name'] = parent_activity.name
218 activity['name'] = parent_activity.name
219 activity['position'] = parent_activity.position
219 activity['position'] = parent_activity.position
220
220
221 if Enumeration.overridding_change?(activity, parent_activity)
221 if Enumeration.overridding_change?(activity, parent_activity)
222 project_activity = self.time_entry_activities.create(activity)
222 project_activity = self.time_entry_activities.create(activity)
223
223
224 if project_activity.new_record?
224 if project_activity.new_record?
225 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
225 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
226 else
226 else
227 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
227 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
228 end
228 end
229 end
229 end
230 end
230 end
231 end
231 end
232
232
233 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
233 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
234 #
234 #
235 # Examples:
235 # Examples:
236 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
236 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
237 # project.project_condition(false) => "projects.id = 1"
237 # project.project_condition(false) => "projects.id = 1"
238 def project_condition(with_subprojects)
238 def project_condition(with_subprojects)
239 cond = "#{Project.table_name}.id = #{id}"
239 cond = "#{Project.table_name}.id = #{id}"
240 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
240 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
241 cond
241 cond
242 end
242 end
243
243
244 def self.find(*args)
244 def self.find(*args)
245 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
245 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
246 project = find_by_identifier(*args)
246 project = find_by_identifier(*args)
247 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
247 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
248 project
248 project
249 else
249 else
250 super
250 super
251 end
251 end
252 end
252 end
253
253
254 def to_param
254 def to_param
255 # id is used for projects with a numeric identifier (compatibility)
255 # id is used for projects with a numeric identifier (compatibility)
256 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
256 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
257 end
257 end
258
258
259 def active?
259 def active?
260 self.status == STATUS_ACTIVE
260 self.status == STATUS_ACTIVE
261 end
261 end
262
262
263 def archived?
263 def archived?
264 self.status == STATUS_ARCHIVED
264 self.status == STATUS_ARCHIVED
265 end
265 end
266
266
267 # Archives the project and its descendants
267 # Archives the project and its descendants
268 def archive
268 def archive
269 # Check that there is no issue of a non descendant project that is assigned
269 # Check that there is no issue of a non descendant project that is assigned
270 # to one of the project or descendant versions
270 # to one of the project or descendant versions
271 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
271 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
272 if v_ids.any? && Issue.find(:first, :include => :project,
272 if v_ids.any? && Issue.find(:first, :include => :project,
273 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
273 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
274 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
274 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
275 return false
275 return false
276 end
276 end
277 Project.transaction do
277 Project.transaction do
278 archive!
278 archive!
279 end
279 end
280 true
280 true
281 end
281 end
282
282
283 # Unarchives the project
283 # Unarchives the project
284 # All its ancestors must be active
284 # All its ancestors must be active
285 def unarchive
285 def unarchive
286 return false if ancestors.detect {|a| !a.active?}
286 return false if ancestors.detect {|a| !a.active?}
287 update_attribute :status, STATUS_ACTIVE
287 update_attribute :status, STATUS_ACTIVE
288 end
288 end
289
289
290 # Returns an array of projects the project can be moved to
290 # Returns an array of projects the project can be moved to
291 # by the current user
291 # by the current user
292 def allowed_parents
292 def allowed_parents
293 return @allowed_parents if @allowed_parents
293 return @allowed_parents if @allowed_parents
294 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
294 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
295 @allowed_parents = @allowed_parents - self_and_descendants
295 @allowed_parents = @allowed_parents - self_and_descendants
296 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
296 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
297 @allowed_parents << nil
297 @allowed_parents << nil
298 end
298 end
299 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
299 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
300 @allowed_parents << parent
300 @allowed_parents << parent
301 end
301 end
302 @allowed_parents
302 @allowed_parents
303 end
303 end
304
304
305 # Sets the parent of the project with authorization check
305 # Sets the parent of the project with authorization check
306 def set_allowed_parent!(p)
306 def set_allowed_parent!(p)
307 unless p.nil? || p.is_a?(Project)
307 unless p.nil? || p.is_a?(Project)
308 if p.to_s.blank?
308 if p.to_s.blank?
309 p = nil
309 p = nil
310 else
310 else
311 p = Project.find_by_id(p)
311 p = Project.find_by_id(p)
312 return false unless p
312 return false unless p
313 end
313 end
314 end
314 end
315 if p.nil?
315 if p.nil?
316 if !new_record? && allowed_parents.empty?
316 if !new_record? && allowed_parents.empty?
317 return false
317 return false
318 end
318 end
319 elsif !allowed_parents.include?(p)
319 elsif !allowed_parents.include?(p)
320 return false
320 return false
321 end
321 end
322 set_parent!(p)
322 set_parent!(p)
323 end
323 end
324
324
325 # Sets the parent of the project
325 # Sets the parent of the project
326 # Argument can be either a Project, a String, a Fixnum or nil
326 # Argument can be either a Project, a String, a Fixnum or nil
327 def set_parent!(p)
327 def set_parent!(p)
328 unless p.nil? || p.is_a?(Project)
328 unless p.nil? || p.is_a?(Project)
329 if p.to_s.blank?
329 if p.to_s.blank?
330 p = nil
330 p = nil
331 else
331 else
332 p = Project.find_by_id(p)
332 p = Project.find_by_id(p)
333 return false unless p
333 return false unless p
334 end
334 end
335 end
335 end
336 if p == parent && !p.nil?
336 if p == parent && !p.nil?
337 # Nothing to do
337 # Nothing to do
338 true
338 true
339 elsif p.nil? || (p.active? && move_possible?(p))
339 elsif p.nil? || (p.active? && move_possible?(p))
340 # Insert the project so that target's children or root projects stay alphabetically sorted
340 # Insert the project so that target's children or root projects stay alphabetically sorted
341 sibs = (p.nil? ? self.class.roots : p.children)
341 sibs = (p.nil? ? self.class.roots : p.children)
342 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
342 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
343 if to_be_inserted_before
343 if to_be_inserted_before
344 move_to_left_of(to_be_inserted_before)
344 move_to_left_of(to_be_inserted_before)
345 elsif p.nil?
345 elsif p.nil?
346 if sibs.empty?
346 if sibs.empty?
347 # move_to_root adds the project in first (ie. left) position
347 # move_to_root adds the project in first (ie. left) position
348 move_to_root
348 move_to_root
349 else
349 else
350 move_to_right_of(sibs.last) unless self == sibs.last
350 move_to_right_of(sibs.last) unless self == sibs.last
351 end
351 end
352 else
352 else
353 # move_to_child_of adds the project in last (ie.right) position
353 # move_to_child_of adds the project in last (ie.right) position
354 move_to_child_of(p)
354 move_to_child_of(p)
355 end
355 end
356 Issue.update_versions_from_hierarchy_change(self)
356 Issue.update_versions_from_hierarchy_change(self)
357 true
357 true
358 else
358 else
359 # Can not move to the given target
359 # Can not move to the given target
360 false
360 false
361 end
361 end
362 end
362 end
363
363
364 # Returns an array of the trackers used by the project and its active sub projects
364 # Returns an array of the trackers used by the project and its active sub projects
365 def rolled_up_trackers
365 def rolled_up_trackers
366 @rolled_up_trackers ||=
366 @rolled_up_trackers ||=
367 Tracker.find(:all, :joins => :projects,
367 Tracker.find(:all, :joins => :projects,
368 :select => "DISTINCT #{Tracker.table_name}.*",
368 :select => "DISTINCT #{Tracker.table_name}.*",
369 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
369 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
370 :order => "#{Tracker.table_name}.position")
370 :order => "#{Tracker.table_name}.position")
371 end
371 end
372
372
373 # Closes open and locked project versions that are completed
373 # Closes open and locked project versions that are completed
374 def close_completed_versions
374 def close_completed_versions
375 Version.transaction do
375 Version.transaction do
376 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
376 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
377 if version.completed?
377 if version.completed?
378 version.update_attribute(:status, 'closed')
378 version.update_attribute(:status, 'closed')
379 end
379 end
380 end
380 end
381 end
381 end
382 end
382 end
383
383
384 # Returns a scope of the Versions on subprojects
384 # Returns a scope of the Versions on subprojects
385 def rolled_up_versions
385 def rolled_up_versions
386 @rolled_up_versions ||=
386 @rolled_up_versions ||=
387 Version.scoped(:include => :project,
387 Version.scoped(:include => :project,
388 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
388 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
389 end
389 end
390
390
391 # Returns a scope of the Versions used by the project
391 # Returns a scope of the Versions used by the project
392 def shared_versions
392 def shared_versions
393 @shared_versions ||= begin
393 @shared_versions ||= begin
394 r = root? ? self : root
394 r = root? ? self : root
395 Version.scoped(:include => :project,
395 Version.scoped(:include => :project,
396 :conditions => "#{Project.table_name}.id = #{id}" +
396 :conditions => "#{Project.table_name}.id = #{id}" +
397 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
397 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
398 " #{Version.table_name}.sharing = 'system'" +
398 " #{Version.table_name}.sharing = 'system'" +
399 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
399 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
400 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
400 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
401 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
401 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
402 "))")
402 "))")
403 end
403 end
404 end
404 end
405
405
406 # Returns a hash of project users grouped by role
406 # Returns a hash of project users grouped by role
407 def users_by_role
407 def users_by_role
408 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
408 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
409 m.roles.each do |r|
409 m.roles.each do |r|
410 h[r] ||= []
410 h[r] ||= []
411 h[r] << m.user
411 h[r] << m.user
412 end
412 end
413 h
413 h
414 end
414 end
415 end
415 end
416
416
417 # Deletes all project's members
417 # Deletes all project's members
418 def delete_all_members
418 def delete_all_members
419 me, mr = Member.table_name, MemberRole.table_name
419 me, mr = Member.table_name, MemberRole.table_name
420 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
420 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
421 Member.delete_all(['project_id = ?', id])
421 Member.delete_all(['project_id = ?', id])
422 end
422 end
423
423
424 # Users issues can be assigned to
424 # Users issues can be assigned to
425 def assignable_users
425 def assignable_users
426 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
426 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
427 end
427 end
428
428
429 # Returns the mail adresses of users that should be always notified on project events
429 # Returns the mail adresses of users that should be always notified on project events
430 def recipients
430 def recipients
431 notified_users.collect {|user| user.mail}
431 notified_users.collect {|user| user.mail}
432 end
432 end
433
433
434 # Returns the users that should be notified on project events
434 # Returns the users that should be notified on project events
435 def notified_users
435 def notified_users
436 # TODO: User part should be extracted to User#notify_about?
436 # TODO: User part should be extracted to User#notify_about?
437 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
437 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
438 end
438 end
439
439
440 # Returns an array of all custom fields enabled for project issues
440 # Returns an array of all custom fields enabled for project issues
441 # (explictly associated custom fields and custom fields enabled for all projects)
441 # (explictly associated custom fields and custom fields enabled for all projects)
442 def all_issue_custom_fields
442 def all_issue_custom_fields
443 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
443 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
444 end
444 end
445
445
446 # Returns an array of all custom fields enabled for project time entries
446 # Returns an array of all custom fields enabled for project time entries
447 # (explictly associated custom fields and custom fields enabled for all projects)
447 # (explictly associated custom fields and custom fields enabled for all projects)
448 def all_time_entry_custom_fields
448 def all_time_entry_custom_fields
449 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
449 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
450 end
450 end
451
451
452 def project
452 def project
453 self
453 self
454 end
454 end
455
455
456 def <=>(project)
456 def <=>(project)
457 name.downcase <=> project.name.downcase
457 name.downcase <=> project.name.downcase
458 end
458 end
459
459
460 def to_s
460 def to_s
461 name
461 name
462 end
462 end
463
463
464 # Returns a short description of the projects (first lines)
464 # Returns a short description of the projects (first lines)
465 def short_description(length = 255)
465 def short_description(length = 255)
466 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
466 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
467 end
467 end
468
468
469 def css_classes
469 def css_classes
470 s = 'project'
470 s = 'project'
471 s << ' root' if root?
471 s << ' root' if root?
472 s << ' child' if child?
472 s << ' child' if child?
473 s << (leaf? ? ' leaf' : ' parent')
473 s << (leaf? ? ' leaf' : ' parent')
474 s
474 s
475 end
475 end
476
476
477 # The earliest start date of a project, based on it's issues and versions
477 # The earliest start date of a project, based on it's issues and versions
478 def start_date
478 def start_date
479 [
479 [
480 issues.minimum('start_date'),
480 issues.minimum('start_date'),
481 shared_versions.collect(&:effective_date),
481 shared_versions.collect(&:effective_date),
482 shared_versions.collect(&:start_date)
482 shared_versions.collect(&:start_date)
483 ].flatten.compact.min
483 ].flatten.compact.min
484 end
484 end
485
485
486 # The latest due date of an issue or version
486 # The latest due date of an issue or version
487 def due_date
487 def due_date
488 [
488 [
489 issues.maximum('due_date'),
489 issues.maximum('due_date'),
490 shared_versions.collect(&:effective_date),
490 shared_versions.collect(&:effective_date),
491 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
491 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
492 ].flatten.compact.max
492 ].flatten.compact.max
493 end
493 end
494
494
495 def overdue?
495 def overdue?
496 active? && !due_date.nil? && (due_date < Date.today)
496 active? && !due_date.nil? && (due_date < Date.today)
497 end
497 end
498
498
499 # Returns the percent completed for this project, based on the
499 # Returns the percent completed for this project, based on the
500 # progress on it's versions.
500 # progress on it's versions.
501 def completed_percent(options={:include_subprojects => false})
501 def completed_percent(options={:include_subprojects => false})
502 if options.delete(:include_subprojects)
502 if options.delete(:include_subprojects)
503 total = self_and_descendants.collect(&:completed_percent).sum
503 total = self_and_descendants.collect(&:completed_percent).sum
504
504
505 total / self_and_descendants.count
505 total / self_and_descendants.count
506 else
506 else
507 if versions.count > 0
507 if versions.count > 0
508 total = versions.collect(&:completed_pourcent).sum
508 total = versions.collect(&:completed_pourcent).sum
509
509
510 total / versions.count
510 total / versions.count
511 else
511 else
512 100
512 100
513 end
513 end
514 end
514 end
515 end
515 end
516
516
517 # Return true if this project is allowed to do the specified action.
517 # Return true if this project is allowed to do the specified action.
518 # action can be:
518 # action can be:
519 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
519 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
520 # * a permission Symbol (eg. :edit_project)
520 # * a permission Symbol (eg. :edit_project)
521 def allows_to?(action)
521 def allows_to?(action)
522 if action.is_a? Hash
522 if action.is_a? Hash
523 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
523 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
524 else
524 else
525 allowed_permissions.include? action
525 allowed_permissions.include? action
526 end
526 end
527 end
527 end
528
528
529 def module_enabled?(module_name)
529 def module_enabled?(module_name)
530 module_name = module_name.to_s
530 module_name = module_name.to_s
531 enabled_modules.detect {|m| m.name == module_name}
531 enabled_modules.detect {|m| m.name == module_name}
532 end
532 end
533
533
534 def enabled_module_names=(module_names)
534 def enabled_module_names=(module_names)
535 if module_names && module_names.is_a?(Array)
535 if module_names && module_names.is_a?(Array)
536 module_names = module_names.collect(&:to_s).reject(&:blank?)
536 module_names = module_names.collect(&:to_s).reject(&:blank?)
537 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
537 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
538 else
538 else
539 enabled_modules.clear
539 enabled_modules.clear
540 end
540 end
541 end
541 end
542
542
543 # Returns an array of the enabled modules names
543 # Returns an array of the enabled modules names
544 def enabled_module_names
544 def enabled_module_names
545 enabled_modules.collect(&:name)
545 enabled_modules.collect(&:name)
546 end
546 end
547
547
548 # Enable a specific module
549 #
550 # Examples:
551 # project.enable_module!(:issue_tracking)
552 # project.enable_module!("issue_tracking")
553 def enable_module!(name)
554 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
555 end
556
557 # Disable a module if it exists
558 #
559 # Examples:
560 # project.disable_module!(:issue_tracking)
561 # project.disable_module!("issue_tracking")
562 # project.disable_module!(project.enabled_modules.first)
563 def disable_module!(target)
564 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
565 target.destroy unless target.blank?
566 end
567
548 safe_attributes 'name',
568 safe_attributes 'name',
549 'description',
569 'description',
550 'homepage',
570 'homepage',
551 'is_public',
571 'is_public',
552 'identifier',
572 'identifier',
553 'custom_field_values',
573 'custom_field_values',
554 'custom_fields',
574 'custom_fields',
555 'tracker_ids',
575 'tracker_ids',
556 'issue_custom_field_ids'
576 'issue_custom_field_ids'
557
577
558 safe_attributes 'enabled_module_names',
578 safe_attributes 'enabled_module_names',
559 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
579 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
560
580
561 # Returns an array of projects that are in this project's hierarchy
581 # Returns an array of projects that are in this project's hierarchy
562 #
582 #
563 # Example: parents, children, siblings
583 # Example: parents, children, siblings
564 def hierarchy
584 def hierarchy
565 parents = project.self_and_ancestors || []
585 parents = project.self_and_ancestors || []
566 descendants = project.descendants || []
586 descendants = project.descendants || []
567 project_hierarchy = parents | descendants # Set union
587 project_hierarchy = parents | descendants # Set union
568 end
588 end
569
589
570 # Returns an auto-generated project identifier based on the last identifier used
590 # Returns an auto-generated project identifier based on the last identifier used
571 def self.next_identifier
591 def self.next_identifier
572 p = Project.find(:first, :order => 'created_on DESC')
592 p = Project.find(:first, :order => 'created_on DESC')
573 p.nil? ? nil : p.identifier.to_s.succ
593 p.nil? ? nil : p.identifier.to_s.succ
574 end
594 end
575
595
576 # Copies and saves the Project instance based on the +project+.
596 # Copies and saves the Project instance based on the +project+.
577 # Duplicates the source project's:
597 # Duplicates the source project's:
578 # * Wiki
598 # * Wiki
579 # * Versions
599 # * Versions
580 # * Categories
600 # * Categories
581 # * Issues
601 # * Issues
582 # * Members
602 # * Members
583 # * Queries
603 # * Queries
584 #
604 #
585 # Accepts an +options+ argument to specify what to copy
605 # Accepts an +options+ argument to specify what to copy
586 #
606 #
587 # Examples:
607 # Examples:
588 # project.copy(1) # => copies everything
608 # project.copy(1) # => copies everything
589 # project.copy(1, :only => 'members') # => copies members only
609 # project.copy(1, :only => 'members') # => copies members only
590 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
610 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
591 def copy(project, options={})
611 def copy(project, options={})
592 project = project.is_a?(Project) ? project : Project.find(project)
612 project = project.is_a?(Project) ? project : Project.find(project)
593
613
594 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
614 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
595 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
615 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
596
616
597 Project.transaction do
617 Project.transaction do
598 if save
618 if save
599 reload
619 reload
600 to_be_copied.each do |name|
620 to_be_copied.each do |name|
601 send "copy_#{name}", project
621 send "copy_#{name}", project
602 end
622 end
603 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
623 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
604 save
624 save
605 end
625 end
606 end
626 end
607 end
627 end
608
628
609
629
610 # Copies +project+ and returns the new instance. This will not save
630 # Copies +project+ and returns the new instance. This will not save
611 # the copy
631 # the copy
612 def self.copy_from(project)
632 def self.copy_from(project)
613 begin
633 begin
614 project = project.is_a?(Project) ? project : Project.find(project)
634 project = project.is_a?(Project) ? project : Project.find(project)
615 if project
635 if project
616 # clear unique attributes
636 # clear unique attributes
617 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
637 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
618 copy = Project.new(attributes)
638 copy = Project.new(attributes)
619 copy.enabled_modules = project.enabled_modules
639 copy.enabled_modules = project.enabled_modules
620 copy.trackers = project.trackers
640 copy.trackers = project.trackers
621 copy.custom_values = project.custom_values.collect {|v| v.clone}
641 copy.custom_values = project.custom_values.collect {|v| v.clone}
622 copy.issue_custom_fields = project.issue_custom_fields
642 copy.issue_custom_fields = project.issue_custom_fields
623 return copy
643 return copy
624 else
644 else
625 return nil
645 return nil
626 end
646 end
627 rescue ActiveRecord::RecordNotFound
647 rescue ActiveRecord::RecordNotFound
628 return nil
648 return nil
629 end
649 end
630 end
650 end
631
651
632 # Yields the given block for each project with its level in the tree
652 # Yields the given block for each project with its level in the tree
633 def self.project_tree(projects, &block)
653 def self.project_tree(projects, &block)
634 ancestors = []
654 ancestors = []
635 projects.sort_by(&:lft).each do |project|
655 projects.sort_by(&:lft).each do |project|
636 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
656 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
637 ancestors.pop
657 ancestors.pop
638 end
658 end
639 yield project, ancestors.size
659 yield project, ancestors.size
640 ancestors << project
660 ancestors << project
641 end
661 end
642 end
662 end
643
663
644 private
664 private
645
665
646 # Copies wiki from +project+
666 # Copies wiki from +project+
647 def copy_wiki(project)
667 def copy_wiki(project)
648 # Check that the source project has a wiki first
668 # Check that the source project has a wiki first
649 unless project.wiki.nil?
669 unless project.wiki.nil?
650 self.wiki ||= Wiki.new
670 self.wiki ||= Wiki.new
651 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
671 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
652 wiki_pages_map = {}
672 wiki_pages_map = {}
653 project.wiki.pages.each do |page|
673 project.wiki.pages.each do |page|
654 # Skip pages without content
674 # Skip pages without content
655 next if page.content.nil?
675 next if page.content.nil?
656 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
676 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
657 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
677 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
658 new_wiki_page.content = new_wiki_content
678 new_wiki_page.content = new_wiki_content
659 wiki.pages << new_wiki_page
679 wiki.pages << new_wiki_page
660 wiki_pages_map[page.id] = new_wiki_page
680 wiki_pages_map[page.id] = new_wiki_page
661 end
681 end
662 wiki.save
682 wiki.save
663 # Reproduce page hierarchy
683 # Reproduce page hierarchy
664 project.wiki.pages.each do |page|
684 project.wiki.pages.each do |page|
665 if page.parent_id && wiki_pages_map[page.id]
685 if page.parent_id && wiki_pages_map[page.id]
666 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
686 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
667 wiki_pages_map[page.id].save
687 wiki_pages_map[page.id].save
668 end
688 end
669 end
689 end
670 end
690 end
671 end
691 end
672
692
673 # Copies versions from +project+
693 # Copies versions from +project+
674 def copy_versions(project)
694 def copy_versions(project)
675 project.versions.each do |version|
695 project.versions.each do |version|
676 new_version = Version.new
696 new_version = Version.new
677 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
697 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
678 self.versions << new_version
698 self.versions << new_version
679 end
699 end
680 end
700 end
681
701
682 # Copies issue categories from +project+
702 # Copies issue categories from +project+
683 def copy_issue_categories(project)
703 def copy_issue_categories(project)
684 project.issue_categories.each do |issue_category|
704 project.issue_categories.each do |issue_category|
685 new_issue_category = IssueCategory.new
705 new_issue_category = IssueCategory.new
686 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
706 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
687 self.issue_categories << new_issue_category
707 self.issue_categories << new_issue_category
688 end
708 end
689 end
709 end
690
710
691 # Copies issues from +project+
711 # Copies issues from +project+
692 # Note: issues assigned to a closed version won't be copied due to validation rules
712 # Note: issues assigned to a closed version won't be copied due to validation rules
693 def copy_issues(project)
713 def copy_issues(project)
694 # Stores the source issue id as a key and the copied issues as the
714 # Stores the source issue id as a key and the copied issues as the
695 # value. Used to map the two togeather for issue relations.
715 # value. Used to map the two togeather for issue relations.
696 issues_map = {}
716 issues_map = {}
697
717
698 # Get issues sorted by root_id, lft so that parent issues
718 # Get issues sorted by root_id, lft so that parent issues
699 # get copied before their children
719 # get copied before their children
700 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
720 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
701 new_issue = Issue.new
721 new_issue = Issue.new
702 new_issue.copy_from(issue)
722 new_issue.copy_from(issue)
703 new_issue.project = self
723 new_issue.project = self
704 # Reassign fixed_versions by name, since names are unique per
724 # Reassign fixed_versions by name, since names are unique per
705 # project and the versions for self are not yet saved
725 # project and the versions for self are not yet saved
706 if issue.fixed_version
726 if issue.fixed_version
707 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
727 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
708 end
728 end
709 # Reassign the category by name, since names are unique per
729 # Reassign the category by name, since names are unique per
710 # project and the categories for self are not yet saved
730 # project and the categories for self are not yet saved
711 if issue.category
731 if issue.category
712 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
732 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
713 end
733 end
714 # Parent issue
734 # Parent issue
715 if issue.parent_id
735 if issue.parent_id
716 if copied_parent = issues_map[issue.parent_id]
736 if copied_parent = issues_map[issue.parent_id]
717 new_issue.parent_issue_id = copied_parent.id
737 new_issue.parent_issue_id = copied_parent.id
718 end
738 end
719 end
739 end
720
740
721 self.issues << new_issue
741 self.issues << new_issue
722 if new_issue.new_record?
742 if new_issue.new_record?
723 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
743 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
724 else
744 else
725 issues_map[issue.id] = new_issue unless new_issue.new_record?
745 issues_map[issue.id] = new_issue unless new_issue.new_record?
726 end
746 end
727 end
747 end
728
748
729 # Relations after in case issues related each other
749 # Relations after in case issues related each other
730 project.issues.each do |issue|
750 project.issues.each do |issue|
731 new_issue = issues_map[issue.id]
751 new_issue = issues_map[issue.id]
732 unless new_issue
752 unless new_issue
733 # Issue was not copied
753 # Issue was not copied
734 next
754 next
735 end
755 end
736
756
737 # Relations
757 # Relations
738 issue.relations_from.each do |source_relation|
758 issue.relations_from.each do |source_relation|
739 new_issue_relation = IssueRelation.new
759 new_issue_relation = IssueRelation.new
740 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
760 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
741 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
761 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
742 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
762 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
743 new_issue_relation.issue_to = source_relation.issue_to
763 new_issue_relation.issue_to = source_relation.issue_to
744 end
764 end
745 new_issue.relations_from << new_issue_relation
765 new_issue.relations_from << new_issue_relation
746 end
766 end
747
767
748 issue.relations_to.each do |source_relation|
768 issue.relations_to.each do |source_relation|
749 new_issue_relation = IssueRelation.new
769 new_issue_relation = IssueRelation.new
750 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
770 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
751 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
771 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
752 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
772 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
753 new_issue_relation.issue_from = source_relation.issue_from
773 new_issue_relation.issue_from = source_relation.issue_from
754 end
774 end
755 new_issue.relations_to << new_issue_relation
775 new_issue.relations_to << new_issue_relation
756 end
776 end
757 end
777 end
758 end
778 end
759
779
760 # Copies members from +project+
780 # Copies members from +project+
761 def copy_members(project)
781 def copy_members(project)
762 # Copy users first, then groups to handle members with inherited and given roles
782 # Copy users first, then groups to handle members with inherited and given roles
763 members_to_copy = []
783 members_to_copy = []
764 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
784 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
765 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
785 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
766
786
767 members_to_copy.each do |member|
787 members_to_copy.each do |member|
768 new_member = Member.new
788 new_member = Member.new
769 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
789 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
770 # only copy non inherited roles
790 # only copy non inherited roles
771 # inherited roles will be added when copying the group membership
791 # inherited roles will be added when copying the group membership
772 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
792 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
773 next if role_ids.empty?
793 next if role_ids.empty?
774 new_member.role_ids = role_ids
794 new_member.role_ids = role_ids
775 new_member.project = self
795 new_member.project = self
776 self.members << new_member
796 self.members << new_member
777 end
797 end
778 end
798 end
779
799
780 # Copies queries from +project+
800 # Copies queries from +project+
781 def copy_queries(project)
801 def copy_queries(project)
782 project.queries.each do |query|
802 project.queries.each do |query|
783 new_query = Query.new
803 new_query = Query.new
784 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
804 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
785 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
805 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
786 new_query.project = self
806 new_query.project = self
787 self.queries << new_query
807 self.queries << new_query
788 end
808 end
789 end
809 end
790
810
791 # Copies boards from +project+
811 # Copies boards from +project+
792 def copy_boards(project)
812 def copy_boards(project)
793 project.boards.each do |board|
813 project.boards.each do |board|
794 new_board = Board.new
814 new_board = Board.new
795 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
815 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
796 new_board.project = self
816 new_board.project = self
797 self.boards << new_board
817 self.boards << new_board
798 end
818 end
799 end
819 end
800
820
801 def allowed_permissions
821 def allowed_permissions
802 @allowed_permissions ||= begin
822 @allowed_permissions ||= begin
803 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
823 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
804 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
824 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
805 end
825 end
806 end
826 end
807
827
808 def allowed_actions
828 def allowed_actions
809 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
829 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
810 end
830 end
811
831
812 # Returns all the active Systemwide and project specific activities
832 # Returns all the active Systemwide and project specific activities
813 def active_activities
833 def active_activities
814 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
834 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
815
835
816 if overridden_activity_ids.empty?
836 if overridden_activity_ids.empty?
817 return TimeEntryActivity.shared.active
837 return TimeEntryActivity.shared.active
818 else
838 else
819 return system_activities_and_project_overrides
839 return system_activities_and_project_overrides
820 end
840 end
821 end
841 end
822
842
823 # Returns all the Systemwide and project specific activities
843 # Returns all the Systemwide and project specific activities
824 # (inactive and active)
844 # (inactive and active)
825 def all_activities
845 def all_activities
826 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
846 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
827
847
828 if overridden_activity_ids.empty?
848 if overridden_activity_ids.empty?
829 return TimeEntryActivity.shared
849 return TimeEntryActivity.shared
830 else
850 else
831 return system_activities_and_project_overrides(true)
851 return system_activities_and_project_overrides(true)
832 end
852 end
833 end
853 end
834
854
835 # Returns the systemwide active activities merged with the project specific overrides
855 # Returns the systemwide active activities merged with the project specific overrides
836 def system_activities_and_project_overrides(include_inactive=false)
856 def system_activities_and_project_overrides(include_inactive=false)
837 if include_inactive
857 if include_inactive
838 return TimeEntryActivity.shared.
858 return TimeEntryActivity.shared.
839 find(:all,
859 find(:all,
840 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
860 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
841 self.time_entry_activities
861 self.time_entry_activities
842 else
862 else
843 return TimeEntryActivity.shared.active.
863 return TimeEntryActivity.shared.active.
844 find(:all,
864 find(:all,
845 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
865 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
846 self.time_entry_activities.active
866 self.time_entry_activities.active
847 end
867 end
848 end
868 end
849
869
850 # Archives subprojects recursively
870 # Archives subprojects recursively
851 def archive!
871 def archive!
852 children.each do |subproject|
872 children.each do |subproject|
853 subproject.send :archive!
873 subproject.send :archive!
854 end
874 end
855 update_attribute :status, STATUS_ARCHIVED
875 update_attribute :status, STATUS_ARCHIVED
856 end
876 end
857 end
877 end
@@ -1,1090 +1,1138
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 context "enabled_modules" do
601 setup do
602 @project = Project.find(1)
603 end
604
605 should "define module by names and preserve ids" do
606 # Remove one module
607 modules = @project.enabled_modules.slice(0..-2)
608 assert modules.any?
609 assert_difference 'EnabledModule.count', -1 do
610 @project.enabled_module_names = modules.collect(&:name)
611 end
612 @project.reload
613 # Ids should be preserved
614 assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort
615 end
616
617 should "enable a module" do
618 @project.enabled_module_names = []
619 @project.reload
620 assert_equal [], @project.enabled_module_names
621 #with string
622 @project.enable_module!("issue_tracking")
623 assert_equal ["issue_tracking"], @project.enabled_module_names
624 #with symbol
625 @project.enable_module!(:gantt)
626 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
627 #don't add a module twice
628 @project.enable_module!("issue_tracking")
629 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
630 end
631
632 should "disable a module" do
633 #with string
634 assert @project.enabled_module_names.include?("issue_tracking")
635 @project.disable_module!("issue_tracking")
636 assert ! @project.reload.enabled_module_names.include?("issue_tracking")
637 #with symbol
638 assert @project.enabled_module_names.include?("gantt")
639 @project.disable_module!(:gantt)
640 assert ! @project.reload.enabled_module_names.include?("gantt")
641 #with EnabledModule object
642 first_module = @project.enabled_modules.first
643 @project.disable_module!(first_module)
644 assert ! @project.reload.enabled_module_names.include?(first_module.name)
645 end
646 end
647
600 def test_enabled_module_names_should_not_recreate_enabled_modules
648 def test_enabled_module_names_should_not_recreate_enabled_modules
601 project = Project.find(1)
649 project = Project.find(1)
602 # Remove one module
650 # Remove one module
603 modules = project.enabled_modules.slice(0..-2)
651 modules = project.enabled_modules.slice(0..-2)
604 assert modules.any?
652 assert modules.any?
605 assert_difference 'EnabledModule.count', -1 do
653 assert_difference 'EnabledModule.count', -1 do
606 project.enabled_module_names = modules.collect(&:name)
654 project.enabled_module_names = modules.collect(&:name)
607 end
655 end
608 project.reload
656 project.reload
609 # Ids should be preserved
657 # Ids should be preserved
610 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
658 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
611 end
659 end
612
660
613 def test_copy_from_existing_project
661 def test_copy_from_existing_project
614 source_project = Project.find(1)
662 source_project = Project.find(1)
615 copied_project = Project.copy_from(1)
663 copied_project = Project.copy_from(1)
616
664
617 assert copied_project
665 assert copied_project
618 # Cleared attributes
666 # Cleared attributes
619 assert copied_project.id.blank?
667 assert copied_project.id.blank?
620 assert copied_project.name.blank?
668 assert copied_project.name.blank?
621 assert copied_project.identifier.blank?
669 assert copied_project.identifier.blank?
622
670
623 # Duplicated attributes
671 # Duplicated attributes
624 assert_equal source_project.description, copied_project.description
672 assert_equal source_project.description, copied_project.description
625 assert_equal source_project.enabled_modules, copied_project.enabled_modules
673 assert_equal source_project.enabled_modules, copied_project.enabled_modules
626 assert_equal source_project.trackers, copied_project.trackers
674 assert_equal source_project.trackers, copied_project.trackers
627
675
628 # Default attributes
676 # Default attributes
629 assert_equal 1, copied_project.status
677 assert_equal 1, copied_project.status
630 end
678 end
631
679
632 def test_activities_should_use_the_system_activities
680 def test_activities_should_use_the_system_activities
633 project = Project.find(1)
681 project = Project.find(1)
634 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
682 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
635 end
683 end
636
684
637
685
638 def test_activities_should_use_the_project_specific_activities
686 def test_activities_should_use_the_project_specific_activities
639 project = Project.find(1)
687 project = Project.find(1)
640 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
688 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
641 assert overridden_activity.save!
689 assert overridden_activity.save!
642
690
643 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
691 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
644 end
692 end
645
693
646 def test_activities_should_not_include_the_inactive_project_specific_activities
694 def test_activities_should_not_include_the_inactive_project_specific_activities
647 project = Project.find(1)
695 project = Project.find(1)
648 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
696 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
649 assert overridden_activity.save!
697 assert overridden_activity.save!
650
698
651 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
699 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
652 end
700 end
653
701
654 def test_activities_should_not_include_project_specific_activities_from_other_projects
702 def test_activities_should_not_include_project_specific_activities_from_other_projects
655 project = Project.find(1)
703 project = Project.find(1)
656 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
704 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
657 assert overridden_activity.save!
705 assert overridden_activity.save!
658
706
659 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
707 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
660 end
708 end
661
709
662 def test_activities_should_handle_nils
710 def test_activities_should_handle_nils
663 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
711 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
664 TimeEntryActivity.delete_all
712 TimeEntryActivity.delete_all
665
713
666 # No activities
714 # No activities
667 project = Project.find(1)
715 project = Project.find(1)
668 assert project.activities.empty?
716 assert project.activities.empty?
669
717
670 # No system, one overridden
718 # No system, one overridden
671 assert overridden_activity.save!
719 assert overridden_activity.save!
672 project.reload
720 project.reload
673 assert_equal [overridden_activity], project.activities
721 assert_equal [overridden_activity], project.activities
674 end
722 end
675
723
676 def test_activities_should_override_system_activities_with_project_activities
724 def test_activities_should_override_system_activities_with_project_activities
677 project = Project.find(1)
725 project = Project.find(1)
678 parent_activity = TimeEntryActivity.find(:first)
726 parent_activity = TimeEntryActivity.find(:first)
679 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
727 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
680 assert overridden_activity.save!
728 assert overridden_activity.save!
681
729
682 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
730 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"
731 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
684 end
732 end
685
733
686 def test_activities_should_include_inactive_activities_if_specified
734 def test_activities_should_include_inactive_activities_if_specified
687 project = Project.find(1)
735 project = Project.find(1)
688 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
736 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
689 assert overridden_activity.save!
737 assert overridden_activity.save!
690
738
691 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
739 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
692 end
740 end
693
741
694 test 'activities should not include active System activities if the project has an override that is inactive' do
742 test 'activities should not include active System activities if the project has an override that is inactive' do
695 project = Project.find(1)
743 project = Project.find(1)
696 system_activity = TimeEntryActivity.find_by_name('Design')
744 system_activity = TimeEntryActivity.find_by_name('Design')
697 assert system_activity.active?
745 assert system_activity.active?
698 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
746 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
699 assert overridden_activity.save!
747 assert overridden_activity.save!
700
748
701 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
749 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"
750 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
703 end
751 end
704
752
705 def test_close_completed_versions
753 def test_close_completed_versions
706 Version.update_all("status = 'open'")
754 Version.update_all("status = 'open'")
707 project = Project.find(1)
755 project = Project.find(1)
708 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
756 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'}
757 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
710 project.close_completed_versions
758 project.close_completed_versions
711 project.reload
759 project.reload
712 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
760 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
713 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
761 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
714 end
762 end
715
763
716 context "Project#copy" do
764 context "Project#copy" do
717 setup do
765 setup do
718 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
766 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
719 Project.destroy_all :identifier => "copy-test"
767 Project.destroy_all :identifier => "copy-test"
720 @source_project = Project.find(2)
768 @source_project = Project.find(2)
721 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
769 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
722 @project.trackers = @source_project.trackers
770 @project.trackers = @source_project.trackers
723 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
771 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
724 end
772 end
725
773
726 should "copy issues" do
774 should "copy issues" do
727 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
775 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
728 :subject => "copy issue status",
776 :subject => "copy issue status",
729 :tracker_id => 1,
777 :tracker_id => 1,
730 :assigned_to_id => 2,
778 :assigned_to_id => 2,
731 :project_id => @source_project.id)
779 :project_id => @source_project.id)
732 assert @project.valid?
780 assert @project.valid?
733 assert @project.issues.empty?
781 assert @project.issues.empty?
734 assert @project.copy(@source_project)
782 assert @project.copy(@source_project)
735
783
736 assert_equal @source_project.issues.size, @project.issues.size
784 assert_equal @source_project.issues.size, @project.issues.size
737 @project.issues.each do |issue|
785 @project.issues.each do |issue|
738 assert issue.valid?
786 assert issue.valid?
739 assert ! issue.assigned_to.blank?
787 assert ! issue.assigned_to.blank?
740 assert_equal @project, issue.project
788 assert_equal @project, issue.project
741 end
789 end
742
790
743 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
791 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
744 assert copied_issue
792 assert copied_issue
745 assert copied_issue.status
793 assert copied_issue.status
746 assert_equal "Closed", copied_issue.status.name
794 assert_equal "Closed", copied_issue.status.name
747 end
795 end
748
796
749 should "change the new issues to use the copied version" do
797 should "change the new issues to use the copied version" do
750 User.current = User.find(1)
798 User.current = User.find(1)
751 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
799 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
752 @source_project.versions << assigned_version
800 @source_project.versions << assigned_version
753 assert_equal 3, @source_project.versions.size
801 assert_equal 3, @source_project.versions.size
754 Issue.generate_for_project!(@source_project,
802 Issue.generate_for_project!(@source_project,
755 :fixed_version_id => assigned_version.id,
803 :fixed_version_id => assigned_version.id,
756 :subject => "change the new issues to use the copied version",
804 :subject => "change the new issues to use the copied version",
757 :tracker_id => 1,
805 :tracker_id => 1,
758 :project_id => @source_project.id)
806 :project_id => @source_project.id)
759
807
760 assert @project.copy(@source_project)
808 assert @project.copy(@source_project)
761 @project.reload
809 @project.reload
762 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
810 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
763
811
764 assert copied_issue
812 assert copied_issue
765 assert copied_issue.fixed_version
813 assert copied_issue.fixed_version
766 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
814 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
815 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
768 end
816 end
769
817
770 should "copy issue relations" do
818 should "copy issue relations" do
771 Setting.cross_project_issue_relations = '1'
819 Setting.cross_project_issue_relations = '1'
772
820
773 second_issue = Issue.generate!(:status_id => 5,
821 second_issue = Issue.generate!(:status_id => 5,
774 :subject => "copy issue relation",
822 :subject => "copy issue relation",
775 :tracker_id => 1,
823 :tracker_id => 1,
776 :assigned_to_id => 2,
824 :assigned_to_id => 2,
777 :project_id => @source_project.id)
825 :project_id => @source_project.id)
778 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
826 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
779 :issue_to => second_issue,
827 :issue_to => second_issue,
780 :relation_type => "relates")
828 :relation_type => "relates")
781 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
829 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
782 :issue_to => second_issue,
830 :issue_to => second_issue,
783 :relation_type => "duplicates")
831 :relation_type => "duplicates")
784
832
785 assert @project.copy(@source_project)
833 assert @project.copy(@source_project)
786 assert_equal @source_project.issues.count, @project.issues.count
834 assert_equal @source_project.issues.count, @project.issues.count
787 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
835 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")
836 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
789
837
790 # First issue with a relation on project
838 # First issue with a relation on project
791 assert_equal 1, copied_issue.relations.size, "Relation not copied"
839 assert_equal 1, copied_issue.relations.size, "Relation not copied"
792 copied_relation = copied_issue.relations.first
840 copied_relation = copied_issue.relations.first
793 assert_equal "relates", copied_relation.relation_type
841 assert_equal "relates", copied_relation.relation_type
794 assert_equal copied_second_issue.id, copied_relation.issue_to_id
842 assert_equal copied_second_issue.id, copied_relation.issue_to_id
795 assert_not_equal source_relation.id, copied_relation.id
843 assert_not_equal source_relation.id, copied_relation.id
796
844
797 # Second issue with a cross project relation
845 # Second issue with a cross project relation
798 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
846 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
847 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
800 assert_equal "duplicates", copied_relation.relation_type
848 assert_equal "duplicates", copied_relation.relation_type
801 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
849 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
850 assert_not_equal source_relation_cross_project.id, copied_relation.id
803 end
851 end
804
852
805 should "copy memberships" do
853 should "copy memberships" do
806 assert @project.valid?
854 assert @project.valid?
807 assert @project.members.empty?
855 assert @project.members.empty?
808 assert @project.copy(@source_project)
856 assert @project.copy(@source_project)
809
857
810 assert_equal @source_project.memberships.size, @project.memberships.size
858 assert_equal @source_project.memberships.size, @project.memberships.size
811 @project.memberships.each do |membership|
859 @project.memberships.each do |membership|
812 assert membership
860 assert membership
813 assert_equal @project, membership.project
861 assert_equal @project, membership.project
814 end
862 end
815 end
863 end
816
864
817 should "copy memberships with groups and additional roles" do
865 should "copy memberships with groups and additional roles" do
818 group = Group.create!(:lastname => "Copy group")
866 group = Group.create!(:lastname => "Copy group")
819 user = User.find(7)
867 user = User.find(7)
820 group.users << user
868 group.users << user
821 # group role
869 # group role
822 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
870 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)
871 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
824 # additional role
872 # additional role
825 member.role_ids = [1]
873 member.role_ids = [1]
826
874
827 assert @project.copy(@source_project)
875 assert @project.copy(@source_project)
828 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
876 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
829 assert_not_nil member
877 assert_not_nil member
830 assert_equal [1, 2], member.role_ids.sort
878 assert_equal [1, 2], member.role_ids.sort
831 end
879 end
832
880
833 should "copy project specific queries" do
881 should "copy project specific queries" do
834 assert @project.valid?
882 assert @project.valid?
835 assert @project.queries.empty?
883 assert @project.queries.empty?
836 assert @project.copy(@source_project)
884 assert @project.copy(@source_project)
837
885
838 assert_equal @source_project.queries.size, @project.queries.size
886 assert_equal @source_project.queries.size, @project.queries.size
839 @project.queries.each do |query|
887 @project.queries.each do |query|
840 assert query
888 assert query
841 assert_equal @project, query.project
889 assert_equal @project, query.project
842 end
890 end
843 end
891 end
844
892
845 should "copy versions" do
893 should "copy versions" do
846 @source_project.versions << Version.generate!
894 @source_project.versions << Version.generate!
847 @source_project.versions << Version.generate!
895 @source_project.versions << Version.generate!
848
896
849 assert @project.versions.empty?
897 assert @project.versions.empty?
850 assert @project.copy(@source_project)
898 assert @project.copy(@source_project)
851
899
852 assert_equal @source_project.versions.size, @project.versions.size
900 assert_equal @source_project.versions.size, @project.versions.size
853 @project.versions.each do |version|
901 @project.versions.each do |version|
854 assert version
902 assert version
855 assert_equal @project, version.project
903 assert_equal @project, version.project
856 end
904 end
857 end
905 end
858
906
859 should "copy wiki" do
907 should "copy wiki" do
860 assert_difference 'Wiki.count' do
908 assert_difference 'Wiki.count' do
861 assert @project.copy(@source_project)
909 assert @project.copy(@source_project)
862 end
910 end
863
911
864 assert @project.wiki
912 assert @project.wiki
865 assert_not_equal @source_project.wiki, @project.wiki
913 assert_not_equal @source_project.wiki, @project.wiki
866 assert_equal "Start page", @project.wiki.start_page
914 assert_equal "Start page", @project.wiki.start_page
867 end
915 end
868
916
869 should "copy wiki pages and content with hierarchy" do
917 should "copy wiki pages and content with hierarchy" do
870 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
918 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
871 assert @project.copy(@source_project)
919 assert @project.copy(@source_project)
872 end
920 end
873
921
874 assert @project.wiki
922 assert @project.wiki
875 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
923 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
876
924
877 @project.wiki.pages.each do |wiki_page|
925 @project.wiki.pages.each do |wiki_page|
878 assert wiki_page.content
926 assert wiki_page.content
879 assert !@source_project.wiki.pages.include?(wiki_page)
927 assert !@source_project.wiki.pages.include?(wiki_page)
880 end
928 end
881
929
882 parent = @project.wiki.find_page('Parent_page')
930 parent = @project.wiki.find_page('Parent_page')
883 child1 = @project.wiki.find_page('Child_page_1')
931 child1 = @project.wiki.find_page('Child_page_1')
884 child2 = @project.wiki.find_page('Child_page_2')
932 child2 = @project.wiki.find_page('Child_page_2')
885 assert_equal parent, child1.parent
933 assert_equal parent, child1.parent
886 assert_equal parent, child2.parent
934 assert_equal parent, child2.parent
887 end
935 end
888
936
889 should "copy issue categories" do
937 should "copy issue categories" do
890 assert @project.copy(@source_project)
938 assert @project.copy(@source_project)
891
939
892 assert_equal 2, @project.issue_categories.size
940 assert_equal 2, @project.issue_categories.size
893 @project.issue_categories.each do |issue_category|
941 @project.issue_categories.each do |issue_category|
894 assert !@source_project.issue_categories.include?(issue_category)
942 assert !@source_project.issue_categories.include?(issue_category)
895 end
943 end
896 end
944 end
897
945
898 should "copy boards" do
946 should "copy boards" do
899 assert @project.copy(@source_project)
947 assert @project.copy(@source_project)
900
948
901 assert_equal 1, @project.boards.size
949 assert_equal 1, @project.boards.size
902 @project.boards.each do |board|
950 @project.boards.each do |board|
903 assert !@source_project.boards.include?(board)
951 assert !@source_project.boards.include?(board)
904 end
952 end
905 end
953 end
906
954
907 should "change the new issues to use the copied issue categories" do
955 should "change the new issues to use the copied issue categories" do
908 issue = Issue.find(4)
956 issue = Issue.find(4)
909 issue.update_attribute(:category_id, 3)
957 issue.update_attribute(:category_id, 3)
910
958
911 assert @project.copy(@source_project)
959 assert @project.copy(@source_project)
912
960
913 @project.issues.each do |issue|
961 @project.issues.each do |issue|
914 assert issue.category
962 assert issue.category
915 assert_equal "Stock management", issue.category.name # Same name
963 assert_equal "Stock management", issue.category.name # Same name
916 assert_not_equal IssueCategory.find(3), issue.category # Different record
964 assert_not_equal IssueCategory.find(3), issue.category # Different record
917 end
965 end
918 end
966 end
919
967
920 should "limit copy with :only option" do
968 should "limit copy with :only option" do
921 assert @project.members.empty?
969 assert @project.members.empty?
922 assert @project.issue_categories.empty?
970 assert @project.issue_categories.empty?
923 assert @source_project.issues.any?
971 assert @source_project.issues.any?
924
972
925 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
973 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
926
974
927 assert @project.members.any?
975 assert @project.members.any?
928 assert @project.issue_categories.any?
976 assert @project.issue_categories.any?
929 assert @project.issues.empty?
977 assert @project.issues.empty?
930 end
978 end
931
979
932 end
980 end
933
981
934 context "#start_date" do
982 context "#start_date" do
935 setup do
983 setup do
936 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
984 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
937 @project = Project.generate!(:identifier => 'test0')
985 @project = Project.generate!(:identifier => 'test0')
938 @project.trackers << Tracker.generate!
986 @project.trackers << Tracker.generate!
939 end
987 end
940
988
941 should "be nil if there are no issues on the project" do
989 should "be nil if there are no issues on the project" do
942 assert_nil @project.start_date
990 assert_nil @project.start_date
943 end
991 end
944
992
945 should "be tested when issues have no start date"
993 should "be tested when issues have no start date"
946
994
947 should "be the earliest start date of it's issues" do
995 should "be the earliest start date of it's issues" do
948 early = 7.days.ago.to_date
996 early = 7.days.ago.to_date
949 Issue.generate_for_project!(@project, :start_date => Date.today)
997 Issue.generate_for_project!(@project, :start_date => Date.today)
950 Issue.generate_for_project!(@project, :start_date => early)
998 Issue.generate_for_project!(@project, :start_date => early)
951
999
952 assert_equal early, @project.start_date
1000 assert_equal early, @project.start_date
953 end
1001 end
954
1002
955 end
1003 end
956
1004
957 context "#due_date" do
1005 context "#due_date" do
958 setup do
1006 setup do
959 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1007 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
960 @project = Project.generate!(:identifier => 'test0')
1008 @project = Project.generate!(:identifier => 'test0')
961 @project.trackers << Tracker.generate!
1009 @project.trackers << Tracker.generate!
962 end
1010 end
963
1011
964 should "be nil if there are no issues on the project" do
1012 should "be nil if there are no issues on the project" do
965 assert_nil @project.due_date
1013 assert_nil @project.due_date
966 end
1014 end
967
1015
968 should "be tested when issues have no due date"
1016 should "be tested when issues have no due date"
969
1017
970 should "be the latest due date of it's issues" do
1018 should "be the latest due date of it's issues" do
971 future = 7.days.from_now.to_date
1019 future = 7.days.from_now.to_date
972 Issue.generate_for_project!(@project, :due_date => future)
1020 Issue.generate_for_project!(@project, :due_date => future)
973 Issue.generate_for_project!(@project, :due_date => Date.today)
1021 Issue.generate_for_project!(@project, :due_date => Date.today)
974
1022
975 assert_equal future, @project.due_date
1023 assert_equal future, @project.due_date
976 end
1024 end
977
1025
978 should "be the latest due date of it's versions" do
1026 should "be the latest due date of it's versions" do
979 future = 7.days.from_now.to_date
1027 future = 7.days.from_now.to_date
980 @project.versions << Version.generate!(:effective_date => future)
1028 @project.versions << Version.generate!(:effective_date => future)
981 @project.versions << Version.generate!(:effective_date => Date.today)
1029 @project.versions << Version.generate!(:effective_date => Date.today)
982
1030
983
1031
984 assert_equal future, @project.due_date
1032 assert_equal future, @project.due_date
985
1033
986 end
1034 end
987
1035
988 should "pick the latest date from it's issues and versions" do
1036 should "pick the latest date from it's issues and versions" do
989 future = 7.days.from_now.to_date
1037 future = 7.days.from_now.to_date
990 far_future = 14.days.from_now.to_date
1038 far_future = 14.days.from_now.to_date
991 Issue.generate_for_project!(@project, :due_date => far_future)
1039 Issue.generate_for_project!(@project, :due_date => far_future)
992 @project.versions << Version.generate!(:effective_date => future)
1040 @project.versions << Version.generate!(:effective_date => future)
993
1041
994 assert_equal far_future, @project.due_date
1042 assert_equal far_future, @project.due_date
995 end
1043 end
996
1044
997 end
1045 end
998
1046
999 context "Project#completed_percent" do
1047 context "Project#completed_percent" do
1000 setup do
1048 setup do
1001 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1049 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1002 @project = Project.generate!(:identifier => 'test0')
1050 @project = Project.generate!(:identifier => 'test0')
1003 @project.trackers << Tracker.generate!
1051 @project.trackers << Tracker.generate!
1004 end
1052 end
1005
1053
1006 context "no versions" do
1054 context "no versions" do
1007 should "be 100" do
1055 should "be 100" do
1008 assert_equal 100, @project.completed_percent
1056 assert_equal 100, @project.completed_percent
1009 end
1057 end
1010 end
1058 end
1011
1059
1012 context "with versions" do
1060 context "with versions" do
1013 should "return 0 if the versions have no issues" do
1061 should "return 0 if the versions have no issues" do
1014 Version.generate!(:project => @project)
1062 Version.generate!(:project => @project)
1015 Version.generate!(:project => @project)
1063 Version.generate!(:project => @project)
1016
1064
1017 assert_equal 0, @project.completed_percent
1065 assert_equal 0, @project.completed_percent
1018 end
1066 end
1019
1067
1020 should "return 100 if the version has only closed issues" do
1068 should "return 100 if the version has only closed issues" do
1021 v1 = Version.generate!(:project => @project)
1069 v1 = Version.generate!(:project => @project)
1022 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
1070 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
1023 v2 = Version.generate!(:project => @project)
1071 v2 = Version.generate!(:project => @project)
1024 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
1072 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
1025
1073
1026 assert_equal 100, @project.completed_percent
1074 assert_equal 100, @project.completed_percent
1027 end
1075 end
1028
1076
1029 should "return the averaged completed percent of the versions (not weighted)" do
1077 should "return the averaged completed percent of the versions (not weighted)" do
1030 v1 = Version.generate!(:project => @project)
1078 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)
1079 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)
1080 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)
1081 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
1034
1082
1035 assert_equal 50, @project.completed_percent
1083 assert_equal 50, @project.completed_percent
1036 end
1084 end
1037
1085
1038 end
1086 end
1039 end
1087 end
1040
1088
1041 context "#notified_users" do
1089 context "#notified_users" do
1042 setup do
1090 setup do
1043 @project = Project.generate!
1091 @project = Project.generate!
1044 @role = Role.generate!
1092 @role = Role.generate!
1045
1093
1046 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1094 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1047 Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1095 Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1048
1096
1049 @all_events_user = User.generate!(:mail_notification => 'all')
1097 @all_events_user = User.generate!(:mail_notification => 'all')
1050 Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user)
1098 Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user)
1051
1099
1052 @no_events_user = User.generate!(:mail_notification => 'none')
1100 @no_events_user = User.generate!(:mail_notification => 'none')
1053 Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user)
1101 Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user)
1054
1102
1055 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1103 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1056 Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1104 Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1057
1105
1058 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1106 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1059 Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1107 Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1060
1108
1061 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1109 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1062 Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1110 Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1063 end
1111 end
1064
1112
1065 should "include members with a mail notification" do
1113 should "include members with a mail notification" do
1066 assert @project.notified_users.include?(@user_with_membership_notification)
1114 assert @project.notified_users.include?(@user_with_membership_notification)
1067 end
1115 end
1068
1116
1069 should "include users with the 'all' notification option" do
1117 should "include users with the 'all' notification option" do
1070 assert @project.notified_users.include?(@all_events_user)
1118 assert @project.notified_users.include?(@all_events_user)
1071 end
1119 end
1072
1120
1073 should "not include users with the 'none' notification option" do
1121 should "not include users with the 'none' notification option" do
1074 assert !@project.notified_users.include?(@no_events_user)
1122 assert !@project.notified_users.include?(@no_events_user)
1075 end
1123 end
1076
1124
1077 should "not include users with the 'only_my_events' notification option" do
1125 should "not include users with the 'only_my_events' notification option" do
1078 assert !@project.notified_users.include?(@only_my_events_user)
1126 assert !@project.notified_users.include?(@only_my_events_user)
1079 end
1127 end
1080
1128
1081 should "not include users with the 'only_assigned' notification option" do
1129 should "not include users with the 'only_assigned' notification option" do
1082 assert !@project.notified_users.include?(@only_assigned_user)
1130 assert !@project.notified_users.include?(@only_assigned_user)
1083 end
1131 end
1084
1132
1085 should "not include users with the 'only_owner' notification option" do
1133 should "not include users with the 'only_owner' notification option" do
1086 assert !@project.notified_users.include?(@only_owned_user)
1134 assert !@project.notified_users.include?(@only_owned_user)
1087 end
1135 end
1088 end
1136 end
1089
1137
1090 end
1138 end
General Comments 0
You need to be logged in to leave comments. Login now