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