##// END OF EJS Templates
Allow project forums copy....
Jean-Philippe Lang -
r2862:6fedbf60d5a9
parent child
Show More
@@ -1,573 +1,583
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 # Specific overidden Activities
23 # Specific overidden Activities
24 has_many :time_entry_activities do
24 has_many :time_entry_activities do
25 def active
25 def active
26 find(:all, :conditions => {:active => true})
26 find(:all, :conditions => {:active => true})
27 end
27 end
28 end
28 end
29 has_many :members, :include => :user, :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
29 has_many :members, :include => :user, :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 has_many :member_principals, :class_name => 'Member',
30 has_many :member_principals, :class_name => 'Member',
31 :include => :principal,
31 :include => :principal,
32 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
32 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
33 has_many :users, :through => :members
33 has_many :users, :through => :members
34 has_many :principals, :through => :member_principals, :source => :principal
34 has_many :principals, :through => :member_principals, :source => :principal
35
35
36 has_many :enabled_modules, :dependent => :delete_all
36 has_many :enabled_modules, :dependent => :delete_all
37 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
37 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
38 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
38 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
39 has_many :issue_changes, :through => :issues, :source => :journals
39 has_many :issue_changes, :through => :issues, :source => :journals
40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
41 has_many :time_entries, :dependent => :delete_all
41 has_many :time_entries, :dependent => :delete_all
42 has_many :queries, :dependent => :delete_all
42 has_many :queries, :dependent => :delete_all
43 has_many :documents, :dependent => :destroy
43 has_many :documents, :dependent => :destroy
44 has_many :news, :dependent => :delete_all, :include => :author
44 has_many :news, :dependent => :delete_all, :include => :author
45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
46 has_many :boards, :dependent => :destroy, :order => "position ASC"
46 has_many :boards, :dependent => :destroy, :order => "position ASC"
47 has_one :repository, :dependent => :destroy
47 has_one :repository, :dependent => :destroy
48 has_many :changesets, :through => :repository
48 has_many :changesets, :through => :repository
49 has_one :wiki, :dependent => :destroy
49 has_one :wiki, :dependent => :destroy
50 # Custom field for the project issues
50 # Custom field for the project issues
51 has_and_belongs_to_many :issue_custom_fields,
51 has_and_belongs_to_many :issue_custom_fields,
52 :class_name => 'IssueCustomField',
52 :class_name => 'IssueCustomField',
53 :order => "#{CustomField.table_name}.position",
53 :order => "#{CustomField.table_name}.position",
54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
55 :association_foreign_key => 'custom_field_id'
55 :association_foreign_key => 'custom_field_id'
56
56
57 acts_as_nested_set :order => 'name', :dependent => :destroy
57 acts_as_nested_set :order => 'name', :dependent => :destroy
58 acts_as_attachable :view_permission => :view_files,
58 acts_as_attachable :view_permission => :view_files,
59 :delete_permission => :manage_files
59 :delete_permission => :manage_files
60
60
61 acts_as_customizable
61 acts_as_customizable
62 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
62 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
65 :author => nil
65 :author => nil
66
66
67 attr_protected :status, :enabled_module_names
67 attr_protected :status, :enabled_module_names
68
68
69 validates_presence_of :name, :identifier
69 validates_presence_of :name, :identifier
70 validates_uniqueness_of :name, :identifier
70 validates_uniqueness_of :name, :identifier
71 validates_associated :repository, :wiki
71 validates_associated :repository, :wiki
72 validates_length_of :name, :maximum => 30
72 validates_length_of :name, :maximum => 30
73 validates_length_of :homepage, :maximum => 255
73 validates_length_of :homepage, :maximum => 255
74 validates_length_of :identifier, :in => 1..20
74 validates_length_of :identifier, :in => 1..20
75 # donwcase letters, digits, dashes but not digits only
75 # donwcase letters, digits, dashes but not digits only
76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
77 # reserved words
77 # reserved words
78 validates_exclusion_of :identifier, :in => %w( new )
78 validates_exclusion_of :identifier, :in => %w( new )
79
79
80 before_destroy :delete_all_members
80 before_destroy :delete_all_members
81
81
82 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] } }
82 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] } }
83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
84 named_scope :all_public, { :conditions => { :is_public => true } }
84 named_scope :all_public, { :conditions => { :is_public => true } }
85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
86
86
87 def identifier=(identifier)
87 def identifier=(identifier)
88 super unless identifier_frozen?
88 super unless identifier_frozen?
89 end
89 end
90
90
91 def identifier_frozen?
91 def identifier_frozen?
92 errors[:identifier].nil? && !(new_record? || identifier.blank?)
92 errors[:identifier].nil? && !(new_record? || identifier.blank?)
93 end
93 end
94
94
95 def issues_with_subprojects(include_subprojects=false)
95 def issues_with_subprojects(include_subprojects=false)
96 conditions = nil
96 conditions = nil
97 if include_subprojects
97 if include_subprojects
98 ids = [id] + descendants.collect(&:id)
98 ids = [id] + descendants.collect(&:id)
99 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
99 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
100 end
100 end
101 conditions ||= ["#{Project.table_name}.id = ?", id]
101 conditions ||= ["#{Project.table_name}.id = ?", id]
102 # Quick and dirty fix for Rails 2 compatibility
102 # Quick and dirty fix for Rails 2 compatibility
103 Issue.send(:with_scope, :find => { :conditions => conditions }) do
103 Issue.send(:with_scope, :find => { :conditions => conditions }) do
104 Version.send(:with_scope, :find => { :conditions => conditions }) do
104 Version.send(:with_scope, :find => { :conditions => conditions }) do
105 yield
105 yield
106 end
106 end
107 end
107 end
108 end
108 end
109
109
110 # returns latest created projects
110 # returns latest created projects
111 # non public projects will be returned only if user is a member of those
111 # non public projects will be returned only if user is a member of those
112 def self.latest(user=nil, count=5)
112 def self.latest(user=nil, count=5)
113 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
113 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
114 end
114 end
115
115
116 # Returns a SQL :conditions string used to find all active projects for the specified user.
116 # Returns a SQL :conditions string used to find all active projects for the specified user.
117 #
117 #
118 # Examples:
118 # Examples:
119 # Projects.visible_by(admin) => "projects.status = 1"
119 # Projects.visible_by(admin) => "projects.status = 1"
120 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
120 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
121 def self.visible_by(user=nil)
121 def self.visible_by(user=nil)
122 user ||= User.current
122 user ||= User.current
123 if user && user.admin?
123 if user && user.admin?
124 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
124 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
125 elsif user && user.memberships.any?
125 elsif user && user.memberships.any?
126 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(',')}))"
126 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(',')}))"
127 else
127 else
128 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
128 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
129 end
129 end
130 end
130 end
131
131
132 def self.allowed_to_condition(user, permission, options={})
132 def self.allowed_to_condition(user, permission, options={})
133 statements = []
133 statements = []
134 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
134 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
135 if perm = Redmine::AccessControl.permission(permission)
135 if perm = Redmine::AccessControl.permission(permission)
136 unless perm.project_module.nil?
136 unless perm.project_module.nil?
137 # If the permission belongs to a project module, make sure the module is enabled
137 # If the permission belongs to a project module, make sure the module is enabled
138 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
138 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
139 end
139 end
140 end
140 end
141 if options[:project]
141 if options[:project]
142 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
142 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
143 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
143 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
144 base_statement = "(#{project_statement}) AND (#{base_statement})"
144 base_statement = "(#{project_statement}) AND (#{base_statement})"
145 end
145 end
146 if user.admin?
146 if user.admin?
147 # no restriction
147 # no restriction
148 else
148 else
149 statements << "1=0"
149 statements << "1=0"
150 if user.logged?
150 if user.logged?
151 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
151 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
152 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
152 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
153 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
153 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
154 elsif Role.anonymous.allowed_to?(permission)
154 elsif Role.anonymous.allowed_to?(permission)
155 # anonymous user allowed on public project
155 # anonymous user allowed on public project
156 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
156 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
157 else
157 else
158 # anonymous user is not authorized
158 # anonymous user is not authorized
159 end
159 end
160 end
160 end
161 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
161 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
162 end
162 end
163
163
164 # Returns the Systemwide and project specific activities
164 # Returns the Systemwide and project specific activities
165 def activities(include_inactive=false)
165 def activities(include_inactive=false)
166 if include_inactive
166 if include_inactive
167 return all_activities
167 return all_activities
168 else
168 else
169 return active_activities
169 return active_activities
170 end
170 end
171 end
171 end
172
172
173 # Will create a new Project specific Activity or update an existing one
173 # Will create a new Project specific Activity or update an existing one
174 #
174 #
175 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
175 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
176 # does not successfully save.
176 # does not successfully save.
177 def update_or_create_time_entry_activity(id, activity_hash)
177 def update_or_create_time_entry_activity(id, activity_hash)
178 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
178 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
179 self.create_time_entry_activity_if_needed(activity_hash)
179 self.create_time_entry_activity_if_needed(activity_hash)
180 else
180 else
181 activity = project.time_entry_activities.find_by_id(id.to_i)
181 activity = project.time_entry_activities.find_by_id(id.to_i)
182 activity.update_attributes(activity_hash) if activity
182 activity.update_attributes(activity_hash) if activity
183 end
183 end
184 end
184 end
185
185
186 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
186 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
187 #
187 #
188 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
188 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
189 # does not successfully save.
189 # does not successfully save.
190 def create_time_entry_activity_if_needed(activity)
190 def create_time_entry_activity_if_needed(activity)
191 if activity['parent_id']
191 if activity['parent_id']
192
192
193 parent_activity = TimeEntryActivity.find(activity['parent_id'])
193 parent_activity = TimeEntryActivity.find(activity['parent_id'])
194 activity['name'] = parent_activity.name
194 activity['name'] = parent_activity.name
195 activity['position'] = parent_activity.position
195 activity['position'] = parent_activity.position
196
196
197 if Enumeration.overridding_change?(activity, parent_activity)
197 if Enumeration.overridding_change?(activity, parent_activity)
198 project_activity = self.time_entry_activities.create(activity)
198 project_activity = self.time_entry_activities.create(activity)
199
199
200 if project_activity.new_record?
200 if project_activity.new_record?
201 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
201 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
202 else
202 else
203 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
203 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
204 end
204 end
205 end
205 end
206 end
206 end
207 end
207 end
208
208
209 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
209 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
210 #
210 #
211 # Examples:
211 # Examples:
212 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
212 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
213 # project.project_condition(false) => "projects.id = 1"
213 # project.project_condition(false) => "projects.id = 1"
214 def project_condition(with_subprojects)
214 def project_condition(with_subprojects)
215 cond = "#{Project.table_name}.id = #{id}"
215 cond = "#{Project.table_name}.id = #{id}"
216 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
216 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
217 cond
217 cond
218 end
218 end
219
219
220 def self.find(*args)
220 def self.find(*args)
221 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
221 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
222 project = find_by_identifier(*args)
222 project = find_by_identifier(*args)
223 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
223 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
224 project
224 project
225 else
225 else
226 super
226 super
227 end
227 end
228 end
228 end
229
229
230 def to_param
230 def to_param
231 # id is used for projects with a numeric identifier (compatibility)
231 # id is used for projects with a numeric identifier (compatibility)
232 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
232 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
233 end
233 end
234
234
235 def active?
235 def active?
236 self.status == STATUS_ACTIVE
236 self.status == STATUS_ACTIVE
237 end
237 end
238
238
239 # Archives the project and its descendants recursively
239 # Archives the project and its descendants recursively
240 def archive
240 def archive
241 # Archive subprojects if any
241 # Archive subprojects if any
242 children.each do |subproject|
242 children.each do |subproject|
243 subproject.archive
243 subproject.archive
244 end
244 end
245 update_attribute :status, STATUS_ARCHIVED
245 update_attribute :status, STATUS_ARCHIVED
246 end
246 end
247
247
248 # Unarchives the project
248 # Unarchives the project
249 # All its ancestors must be active
249 # All its ancestors must be active
250 def unarchive
250 def unarchive
251 return false if ancestors.detect {|a| !a.active?}
251 return false if ancestors.detect {|a| !a.active?}
252 update_attribute :status, STATUS_ACTIVE
252 update_attribute :status, STATUS_ACTIVE
253 end
253 end
254
254
255 # Returns an array of projects the project can be moved to
255 # Returns an array of projects the project can be moved to
256 def possible_parents
256 def possible_parents
257 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
257 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
258 end
258 end
259
259
260 # Sets the parent of the project
260 # Sets the parent of the project
261 # Argument can be either a Project, a String, a Fixnum or nil
261 # Argument can be either a Project, a String, a Fixnum or nil
262 def set_parent!(p)
262 def set_parent!(p)
263 unless p.nil? || p.is_a?(Project)
263 unless p.nil? || p.is_a?(Project)
264 if p.to_s.blank?
264 if p.to_s.blank?
265 p = nil
265 p = nil
266 else
266 else
267 p = Project.find_by_id(p)
267 p = Project.find_by_id(p)
268 return false unless p
268 return false unless p
269 end
269 end
270 end
270 end
271 if p == parent && !p.nil?
271 if p == parent && !p.nil?
272 # Nothing to do
272 # Nothing to do
273 true
273 true
274 elsif p.nil? || (p.active? && move_possible?(p))
274 elsif p.nil? || (p.active? && move_possible?(p))
275 # Insert the project so that target's children or root projects stay alphabetically sorted
275 # Insert the project so that target's children or root projects stay alphabetically sorted
276 sibs = (p.nil? ? self.class.roots : p.children)
276 sibs = (p.nil? ? self.class.roots : p.children)
277 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
277 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
278 if to_be_inserted_before
278 if to_be_inserted_before
279 move_to_left_of(to_be_inserted_before)
279 move_to_left_of(to_be_inserted_before)
280 elsif p.nil?
280 elsif p.nil?
281 if sibs.empty?
281 if sibs.empty?
282 # move_to_root adds the project in first (ie. left) position
282 # move_to_root adds the project in first (ie. left) position
283 move_to_root
283 move_to_root
284 else
284 else
285 move_to_right_of(sibs.last) unless self == sibs.last
285 move_to_right_of(sibs.last) unless self == sibs.last
286 end
286 end
287 else
287 else
288 # move_to_child_of adds the project in last (ie.right) position
288 # move_to_child_of adds the project in last (ie.right) position
289 move_to_child_of(p)
289 move_to_child_of(p)
290 end
290 end
291 true
291 true
292 else
292 else
293 # Can not move to the given target
293 # Can not move to the given target
294 false
294 false
295 end
295 end
296 end
296 end
297
297
298 # Returns an array of the trackers used by the project and its active sub projects
298 # Returns an array of the trackers used by the project and its active sub projects
299 def rolled_up_trackers
299 def rolled_up_trackers
300 @rolled_up_trackers ||=
300 @rolled_up_trackers ||=
301 Tracker.find(:all, :include => :projects,
301 Tracker.find(:all, :include => :projects,
302 :select => "DISTINCT #{Tracker.table_name}.*",
302 :select => "DISTINCT #{Tracker.table_name}.*",
303 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
303 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
304 :order => "#{Tracker.table_name}.position")
304 :order => "#{Tracker.table_name}.position")
305 end
305 end
306
306
307 # Returns a hash of project users grouped by role
307 # Returns a hash of project users grouped by role
308 def users_by_role
308 def users_by_role
309 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
309 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
310 m.roles.each do |r|
310 m.roles.each do |r|
311 h[r] ||= []
311 h[r] ||= []
312 h[r] << m.user
312 h[r] << m.user
313 end
313 end
314 h
314 h
315 end
315 end
316 end
316 end
317
317
318 # Deletes all project's members
318 # Deletes all project's members
319 def delete_all_members
319 def delete_all_members
320 me, mr = Member.table_name, MemberRole.table_name
320 me, mr = Member.table_name, MemberRole.table_name
321 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
321 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
322 Member.delete_all(['project_id = ?', id])
322 Member.delete_all(['project_id = ?', id])
323 end
323 end
324
324
325 # Users issues can be assigned to
325 # Users issues can be assigned to
326 def assignable_users
326 def assignable_users
327 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
327 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
328 end
328 end
329
329
330 # Returns the mail adresses of users that should be always notified on project events
330 # Returns the mail adresses of users that should be always notified on project events
331 def recipients
331 def recipients
332 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
332 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
333 end
333 end
334
334
335 # Returns an array of all custom fields enabled for project issues
335 # Returns an array of all custom fields enabled for project issues
336 # (explictly associated custom fields and custom fields enabled for all projects)
336 # (explictly associated custom fields and custom fields enabled for all projects)
337 def all_issue_custom_fields
337 def all_issue_custom_fields
338 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
338 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
339 end
339 end
340
340
341 def project
341 def project
342 self
342 self
343 end
343 end
344
344
345 def <=>(project)
345 def <=>(project)
346 name.downcase <=> project.name.downcase
346 name.downcase <=> project.name.downcase
347 end
347 end
348
348
349 def to_s
349 def to_s
350 name
350 name
351 end
351 end
352
352
353 # Returns a short description of the projects (first lines)
353 # Returns a short description of the projects (first lines)
354 def short_description(length = 255)
354 def short_description(length = 255)
355 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
355 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
356 end
356 end
357
357
358 # Return true if this project is allowed to do the specified action.
358 # Return true if this project is allowed to do the specified action.
359 # action can be:
359 # action can be:
360 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
360 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
361 # * a permission Symbol (eg. :edit_project)
361 # * a permission Symbol (eg. :edit_project)
362 def allows_to?(action)
362 def allows_to?(action)
363 if action.is_a? Hash
363 if action.is_a? Hash
364 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
364 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
365 else
365 else
366 allowed_permissions.include? action
366 allowed_permissions.include? action
367 end
367 end
368 end
368 end
369
369
370 def module_enabled?(module_name)
370 def module_enabled?(module_name)
371 module_name = module_name.to_s
371 module_name = module_name.to_s
372 enabled_modules.detect {|m| m.name == module_name}
372 enabled_modules.detect {|m| m.name == module_name}
373 end
373 end
374
374
375 def enabled_module_names=(module_names)
375 def enabled_module_names=(module_names)
376 if module_names && module_names.is_a?(Array)
376 if module_names && module_names.is_a?(Array)
377 module_names = module_names.collect(&:to_s)
377 module_names = module_names.collect(&:to_s)
378 # remove disabled modules
378 # remove disabled modules
379 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
379 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
380 # add new modules
380 # add new modules
381 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
381 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
382 else
382 else
383 enabled_modules.clear
383 enabled_modules.clear
384 end
384 end
385 end
385 end
386
386
387 # Returns an auto-generated project identifier based on the last identifier used
387 # Returns an auto-generated project identifier based on the last identifier used
388 def self.next_identifier
388 def self.next_identifier
389 p = Project.find(:first, :order => 'created_on DESC')
389 p = Project.find(:first, :order => 'created_on DESC')
390 p.nil? ? nil : p.identifier.to_s.succ
390 p.nil? ? nil : p.identifier.to_s.succ
391 end
391 end
392
392
393 # Copies and saves the Project instance based on the +project+.
393 # Copies and saves the Project instance based on the +project+.
394 # Duplicates the source project's:
394 # Duplicates the source project's:
395 # * Wiki
395 # * Wiki
396 # * Versions
396 # * Versions
397 # * Categories
397 # * Categories
398 # * Issues
398 # * Issues
399 # * Members
399 # * Members
400 # * Queries
400 # * Queries
401 #
401 #
402 # Accepts an +options+ argument to specify what to copy
402 # Accepts an +options+ argument to specify what to copy
403 #
403 #
404 # Examples:
404 # Examples:
405 # project.copy(1) # => copies everything
405 # project.copy(1) # => copies everything
406 # project.copy(1, :only => 'members') # => copies members only
406 # project.copy(1, :only => 'members') # => copies members only
407 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
407 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
408 def copy(project, options={})
408 def copy(project, options={})
409 project = project.is_a?(Project) ? project : Project.find(project)
409 project = project.is_a?(Project) ? project : Project.find(project)
410
410
411 to_be_copied = %w(wiki versions issue_categories issues members queries)
411 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
412 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
412 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
413
413
414 Project.transaction do
414 Project.transaction do
415 if save
415 if save
416 reload
416 reload
417 to_be_copied.each do |name|
417 to_be_copied.each do |name|
418 send "copy_#{name}", project
418 send "copy_#{name}", project
419 end
419 end
420 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
420 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
421 save
421 save
422 end
422 end
423 end
423 end
424 end
424 end
425
425
426
426
427 # Copies +project+ and returns the new instance. This will not save
427 # Copies +project+ and returns the new instance. This will not save
428 # the copy
428 # the copy
429 def self.copy_from(project)
429 def self.copy_from(project)
430 begin
430 begin
431 project = project.is_a?(Project) ? project : Project.find(project)
431 project = project.is_a?(Project) ? project : Project.find(project)
432 if project
432 if project
433 # clear unique attributes
433 # clear unique attributes
434 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
434 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
435 copy = Project.new(attributes)
435 copy = Project.new(attributes)
436 copy.enabled_modules = project.enabled_modules
436 copy.enabled_modules = project.enabled_modules
437 copy.trackers = project.trackers
437 copy.trackers = project.trackers
438 copy.custom_values = project.custom_values.collect {|v| v.clone}
438 copy.custom_values = project.custom_values.collect {|v| v.clone}
439 copy.issue_custom_fields = project.issue_custom_fields
439 copy.issue_custom_fields = project.issue_custom_fields
440 return copy
440 return copy
441 else
441 else
442 return nil
442 return nil
443 end
443 end
444 rescue ActiveRecord::RecordNotFound
444 rescue ActiveRecord::RecordNotFound
445 return nil
445 return nil
446 end
446 end
447 end
447 end
448
448
449 private
449 private
450
450
451 # Copies wiki from +project+
451 # Copies wiki from +project+
452 def copy_wiki(project)
452 def copy_wiki(project)
453 # Check that the source project has a wiki first
453 # Check that the source project has a wiki first
454 unless project.wiki.nil?
454 unless project.wiki.nil?
455 self.wiki ||= Wiki.new
455 self.wiki ||= Wiki.new
456 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
456 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
457 project.wiki.pages.each do |page|
457 project.wiki.pages.each do |page|
458 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
458 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
459 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
459 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
460 new_wiki_page.content = new_wiki_content
460 new_wiki_page.content = new_wiki_content
461 wiki.pages << new_wiki_page
461 wiki.pages << new_wiki_page
462 end
462 end
463 end
463 end
464 end
464 end
465
465
466 # Copies versions from +project+
466 # Copies versions from +project+
467 def copy_versions(project)
467 def copy_versions(project)
468 project.versions.each do |version|
468 project.versions.each do |version|
469 new_version = Version.new
469 new_version = Version.new
470 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
470 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
471 self.versions << new_version
471 self.versions << new_version
472 end
472 end
473 end
473 end
474
474
475 # Copies issue categories from +project+
475 # Copies issue categories from +project+
476 def copy_issue_categories(project)
476 def copy_issue_categories(project)
477 project.issue_categories.each do |issue_category|
477 project.issue_categories.each do |issue_category|
478 new_issue_category = IssueCategory.new
478 new_issue_category = IssueCategory.new
479 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
479 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
480 self.issue_categories << new_issue_category
480 self.issue_categories << new_issue_category
481 end
481 end
482 end
482 end
483
483
484 # Copies issues from +project+
484 # Copies issues from +project+
485 def copy_issues(project)
485 def copy_issues(project)
486 project.issues.each do |issue|
486 project.issues.each do |issue|
487 new_issue = Issue.new
487 new_issue = Issue.new
488 new_issue.copy_from(issue)
488 new_issue.copy_from(issue)
489 # Reassign fixed_versions by name, since names are unique per
489 # Reassign fixed_versions by name, since names are unique per
490 # project and the versions for self are not yet saved
490 # project and the versions for self are not yet saved
491 if issue.fixed_version
491 if issue.fixed_version
492 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
492 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
493 end
493 end
494 # Reassign the category by name, since names are unique per
494 # Reassign the category by name, since names are unique per
495 # project and the categories for self are not yet saved
495 # project and the categories for self are not yet saved
496 if issue.category
496 if issue.category
497 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
497 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
498 end
498 end
499 self.issues << new_issue
499 self.issues << new_issue
500 end
500 end
501 end
501 end
502
502
503 # Copies members from +project+
503 # Copies members from +project+
504 def copy_members(project)
504 def copy_members(project)
505 project.members.each do |member|
505 project.members.each do |member|
506 new_member = Member.new
506 new_member = Member.new
507 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
507 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
508 new_member.role_ids = member.role_ids.dup
508 new_member.role_ids = member.role_ids.dup
509 new_member.project = self
509 new_member.project = self
510 self.members << new_member
510 self.members << new_member
511 end
511 end
512 end
512 end
513
513
514 # Copies queries from +project+
514 # Copies queries from +project+
515 def copy_queries(project)
515 def copy_queries(project)
516 project.queries.each do |query|
516 project.queries.each do |query|
517 new_query = Query.new
517 new_query = Query.new
518 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
518 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
519 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
519 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
520 new_query.project = self
520 new_query.project = self
521 self.queries << new_query
521 self.queries << new_query
522 end
522 end
523 end
523 end
524
525 # Copies boards from +project+
526 def copy_boards(project)
527 project.boards.each do |board|
528 new_board = Board.new
529 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
530 new_board.project = self
531 self.boards << new_board
532 end
533 end
524
534
525 def allowed_permissions
535 def allowed_permissions
526 @allowed_permissions ||= begin
536 @allowed_permissions ||= begin
527 module_names = enabled_modules.collect {|m| m.name}
537 module_names = enabled_modules.collect {|m| m.name}
528 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
538 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
529 end
539 end
530 end
540 end
531
541
532 def allowed_actions
542 def allowed_actions
533 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
543 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
534 end
544 end
535
545
536 # Returns all the active Systemwide and project specific activities
546 # Returns all the active Systemwide and project specific activities
537 def active_activities
547 def active_activities
538 overridden_activity_ids = self.time_entry_activities.active.collect(&:parent_id)
548 overridden_activity_ids = self.time_entry_activities.active.collect(&:parent_id)
539
549
540 if overridden_activity_ids.empty?
550 if overridden_activity_ids.empty?
541 return TimeEntryActivity.active
551 return TimeEntryActivity.active
542 else
552 else
543 return system_activities_and_project_overrides
553 return system_activities_and_project_overrides
544 end
554 end
545 end
555 end
546
556
547 # Returns all the Systemwide and project specific activities
557 # Returns all the Systemwide and project specific activities
548 # (inactive and active)
558 # (inactive and active)
549 def all_activities
559 def all_activities
550 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
560 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
551
561
552 if overridden_activity_ids.empty?
562 if overridden_activity_ids.empty?
553 return TimeEntryActivity.all
563 return TimeEntryActivity.all
554 else
564 else
555 return system_activities_and_project_overrides(true)
565 return system_activities_and_project_overrides(true)
556 end
566 end
557 end
567 end
558
568
559 # Returns the systemwide active activities merged with the project specific overrides
569 # Returns the systemwide active activities merged with the project specific overrides
560 def system_activities_and_project_overrides(include_inactive=false)
570 def system_activities_and_project_overrides(include_inactive=false)
561 if include_inactive
571 if include_inactive
562 return TimeEntryActivity.all.
572 return TimeEntryActivity.all.
563 find(:all,
573 find(:all,
564 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
574 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
565 self.time_entry_activities
575 self.time_entry_activities
566 else
576 else
567 return TimeEntryActivity.active.
577 return TimeEntryActivity.active.
568 find(:all,
578 find(:all,
569 :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
579 :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
570 self.time_entry_activities.active
580 self.time_entry_activities.active
571 end
581 end
572 end
582 end
573 end
583 end
@@ -1,26 +1,27
1 <h2><%=l(:label_project_new)%></h2>
1 <h2><%=l(:label_project_new)%></h2>
2
2
3 <% labelled_tabular_form_for :project, @project, :url => { :action => "copy" } do |f| %>
3 <% labelled_tabular_form_for :project, @project, :url => { :action => "copy" } do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5
5
6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
8 <label class="floating">
8 <label class="floating">
9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
10 <%= l_or_humanize(m, :prefix => "project_module_") %>
10 <%= l_or_humanize(m, :prefix => "project_module_") %>
11 </label>
11 </label>
12 <% end %>
12 <% end %>
13 </fieldset>
13 </fieldset>
14
14
15 <fieldset class="box"><legend><%= l(:button_copy) %></legend>
15 <fieldset class="box"><legend><%= l(:button_copy) %></legend>
16 <label class="block"><%= check_box_tag 'only[]', 'members', true %> <%= l(:label_member_plural) %> (<%= @source_project.members.count %>)</label>
16 <label class="block"><%= check_box_tag 'only[]', 'members', true %> <%= l(:label_member_plural) %> (<%= @source_project.members.count %>)</label>
17 <label class="block"><%= check_box_tag 'only[]', 'versions', true %> <%= l(:label_version_plural) %> (<%= @source_project.versions.count %>)</label>
17 <label class="block"><%= check_box_tag 'only[]', 'versions', true %> <%= l(:label_version_plural) %> (<%= @source_project.versions.count %>)</label>
18 <label class="block"><%= check_box_tag 'only[]', 'issue_categories', true %> <%= l(:label_issue_category_plural) %> (<%= @source_project.issue_categories.count %>)</label>
18 <label class="block"><%= check_box_tag 'only[]', 'issue_categories', true %> <%= l(:label_issue_category_plural) %> (<%= @source_project.issue_categories.count %>)</label>
19 <label class="block"><%= check_box_tag 'only[]', 'issues', true %> <%= l(:label_issue_plural) %> (<%= @source_project.issues.count %>)</label>
19 <label class="block"><%= check_box_tag 'only[]', 'issues', true %> <%= l(:label_issue_plural) %> (<%= @source_project.issues.count %>)</label>
20 <label class="block"><%= check_box_tag 'only[]', 'queries', true %> <%= l(:label_query_plural) %> (<%= @source_project.queries.count %>)</label>
20 <label class="block"><%= check_box_tag 'only[]', 'queries', true %> <%= l(:label_query_plural) %> (<%= @source_project.queries.count %>)</label>
21 <label class="block"><%= check_box_tag 'only[]', 'boards', true %> <%= l(:label_board_plural) %> (<%= @source_project.boards.count %>)</label>
21 <label class="block"><%= check_box_tag 'only[]', 'wiki', true %> <%= l(:label_wiki_page_plural) %> (<%= @source_project.wiki.nil? ? 0 : @source_project.wiki.pages.count %>)</label>
22 <label class="block"><%= check_box_tag 'only[]', 'wiki', true %> <%= l(:label_wiki_page_plural) %> (<%= @source_project.wiki.nil? ? 0 : @source_project.wiki.pages.count %>)</label>
22 <%= hidden_field_tag 'only[]', '' %>
23 <%= hidden_field_tag 'only[]', '' %>
23 </fieldset>
24 </fieldset>
24
25
25 <%= submit_tag l(:button_copy) %>
26 <%= submit_tag l(:button_copy) %>
26 <% end %>
27 <% end %>
@@ -1,19 +1,28
1 ---
1 ---
2 boards_001:
2 boards_001:
3 name: Help
3 name: Help
4 project_id: 1
4 project_id: 1
5 topics_count: 2
5 topics_count: 2
6 id: 1
6 id: 1
7 description: Help board
7 description: Help board
8 position: 1
8 position: 1
9 last_message_id: 6
9 last_message_id: 6
10 messages_count: 6
10 messages_count: 6
11 boards_002:
11 boards_002:
12 name: Discussion
12 name: Discussion
13 project_id: 1
13 project_id: 1
14 topics_count: 0
14 topics_count: 0
15 id: 2
15 id: 2
16 description: Discussion board
16 description: Discussion board
17 position: 2
17 position: 2
18 last_message_id:
18 last_message_id:
19 messages_count: 0
19 messages_count: 0
20 boards_003:
21 name: Discussion
22 project_id: 2
23 topics_count: 0
24 id: 3
25 description: Discussion board
26 position: 1
27 last_message_id:
28 messages_count: 0
@@ -1,521 +1,530
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class ProjectTest < ActiveSupport::TestCase
20 class ProjectTest < ActiveSupport::TestCase
21 fixtures :projects, :enabled_modules,
21 fixtures :projects, :enabled_modules,
22 :issues, :issue_statuses, :journals, :journal_details,
22 :issues, :issue_statuses, :journals, :journal_details,
23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
24 :queries
24 :queries
25
25
26 def setup
26 def setup
27 @ecookbook = Project.find(1)
27 @ecookbook = Project.find(1)
28 @ecookbook_sub1 = Project.find(3)
28 @ecookbook_sub1 = Project.find(3)
29 end
29 end
30
30
31 should_validate_presence_of :name
31 should_validate_presence_of :name
32 should_validate_presence_of :identifier
32 should_validate_presence_of :identifier
33
33
34 should_validate_uniqueness_of :name
34 should_validate_uniqueness_of :name
35 should_validate_uniqueness_of :identifier
35 should_validate_uniqueness_of :identifier
36
36
37 context "associations" do
37 context "associations" do
38 should_have_many :members
38 should_have_many :members
39 should_have_many :users, :through => :members
39 should_have_many :users, :through => :members
40 should_have_many :member_principals
40 should_have_many :member_principals
41 should_have_many :principals, :through => :member_principals
41 should_have_many :principals, :through => :member_principals
42 should_have_many :enabled_modules
42 should_have_many :enabled_modules
43 should_have_many :issues
43 should_have_many :issues
44 should_have_many :issue_changes, :through => :issues
44 should_have_many :issue_changes, :through => :issues
45 should_have_many :versions
45 should_have_many :versions
46 should_have_many :time_entries
46 should_have_many :time_entries
47 should_have_many :queries
47 should_have_many :queries
48 should_have_many :documents
48 should_have_many :documents
49 should_have_many :news
49 should_have_many :news
50 should_have_many :issue_categories
50 should_have_many :issue_categories
51 should_have_many :boards
51 should_have_many :boards
52 should_have_many :changesets, :through => :repository
52 should_have_many :changesets, :through => :repository
53
53
54 should_have_one :repository
54 should_have_one :repository
55 should_have_one :wiki
55 should_have_one :wiki
56
56
57 should_have_and_belong_to_many :trackers
57 should_have_and_belong_to_many :trackers
58 should_have_and_belong_to_many :issue_custom_fields
58 should_have_and_belong_to_many :issue_custom_fields
59 end
59 end
60
60
61 def test_truth
61 def test_truth
62 assert_kind_of Project, @ecookbook
62 assert_kind_of Project, @ecookbook
63 assert_equal "eCookbook", @ecookbook.name
63 assert_equal "eCookbook", @ecookbook.name
64 end
64 end
65
65
66 def test_update
66 def test_update
67 assert_equal "eCookbook", @ecookbook.name
67 assert_equal "eCookbook", @ecookbook.name
68 @ecookbook.name = "eCook"
68 @ecookbook.name = "eCook"
69 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
69 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
70 @ecookbook.reload
70 @ecookbook.reload
71 assert_equal "eCook", @ecookbook.name
71 assert_equal "eCook", @ecookbook.name
72 end
72 end
73
73
74 def test_validate_identifier
74 def test_validate_identifier
75 to_test = {"abc" => true,
75 to_test = {"abc" => true,
76 "ab12" => true,
76 "ab12" => true,
77 "ab-12" => true,
77 "ab-12" => true,
78 "12" => false,
78 "12" => false,
79 "new" => false}
79 "new" => false}
80
80
81 to_test.each do |identifier, valid|
81 to_test.each do |identifier, valid|
82 p = Project.new
82 p = Project.new
83 p.identifier = identifier
83 p.identifier = identifier
84 p.valid?
84 p.valid?
85 assert_equal valid, p.errors.on('identifier').nil?
85 assert_equal valid, p.errors.on('identifier').nil?
86 end
86 end
87 end
87 end
88
88
89 def test_members_should_be_active_users
89 def test_members_should_be_active_users
90 Project.all.each do |project|
90 Project.all.each do |project|
91 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
91 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
92 end
92 end
93 end
93 end
94
94
95 def test_users_should_be_active_users
95 def test_users_should_be_active_users
96 Project.all.each do |project|
96 Project.all.each do |project|
97 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
97 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
98 end
98 end
99 end
99 end
100
100
101 def test_archive
101 def test_archive
102 user = @ecookbook.members.first.user
102 user = @ecookbook.members.first.user
103 @ecookbook.archive
103 @ecookbook.archive
104 @ecookbook.reload
104 @ecookbook.reload
105
105
106 assert !@ecookbook.active?
106 assert !@ecookbook.active?
107 assert !user.projects.include?(@ecookbook)
107 assert !user.projects.include?(@ecookbook)
108 # Subproject are also archived
108 # Subproject are also archived
109 assert !@ecookbook.children.empty?
109 assert !@ecookbook.children.empty?
110 assert @ecookbook.descendants.active.empty?
110 assert @ecookbook.descendants.active.empty?
111 end
111 end
112
112
113 def test_unarchive
113 def test_unarchive
114 user = @ecookbook.members.first.user
114 user = @ecookbook.members.first.user
115 @ecookbook.archive
115 @ecookbook.archive
116 # A subproject of an archived project can not be unarchived
116 # A subproject of an archived project can not be unarchived
117 assert !@ecookbook_sub1.unarchive
117 assert !@ecookbook_sub1.unarchive
118
118
119 # Unarchive project
119 # Unarchive project
120 assert @ecookbook.unarchive
120 assert @ecookbook.unarchive
121 @ecookbook.reload
121 @ecookbook.reload
122 assert @ecookbook.active?
122 assert @ecookbook.active?
123 assert user.projects.include?(@ecookbook)
123 assert user.projects.include?(@ecookbook)
124 # Subproject can now be unarchived
124 # Subproject can now be unarchived
125 @ecookbook_sub1.reload
125 @ecookbook_sub1.reload
126 assert @ecookbook_sub1.unarchive
126 assert @ecookbook_sub1.unarchive
127 end
127 end
128
128
129 def test_destroy
129 def test_destroy
130 # 2 active members
130 # 2 active members
131 assert_equal 2, @ecookbook.members.size
131 assert_equal 2, @ecookbook.members.size
132 # and 1 is locked
132 # and 1 is locked
133 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
133 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
134 # some boards
134 # some boards
135 assert @ecookbook.boards.any?
135 assert @ecookbook.boards.any?
136
136
137 @ecookbook.destroy
137 @ecookbook.destroy
138 # make sure that the project non longer exists
138 # make sure that the project non longer exists
139 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
139 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
140 # make sure related data was removed
140 # make sure related data was removed
141 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
141 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
142 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
142 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
143 end
143 end
144
144
145 def test_move_an_orphan_project_to_a_root_project
145 def test_move_an_orphan_project_to_a_root_project
146 sub = Project.find(2)
146 sub = Project.find(2)
147 sub.set_parent! @ecookbook
147 sub.set_parent! @ecookbook
148 assert_equal @ecookbook.id, sub.parent.id
148 assert_equal @ecookbook.id, sub.parent.id
149 @ecookbook.reload
149 @ecookbook.reload
150 assert_equal 4, @ecookbook.children.size
150 assert_equal 4, @ecookbook.children.size
151 end
151 end
152
152
153 def test_move_an_orphan_project_to_a_subproject
153 def test_move_an_orphan_project_to_a_subproject
154 sub = Project.find(2)
154 sub = Project.find(2)
155 assert sub.set_parent!(@ecookbook_sub1)
155 assert sub.set_parent!(@ecookbook_sub1)
156 end
156 end
157
157
158 def test_move_a_root_project_to_a_project
158 def test_move_a_root_project_to_a_project
159 sub = @ecookbook
159 sub = @ecookbook
160 assert sub.set_parent!(Project.find(2))
160 assert sub.set_parent!(Project.find(2))
161 end
161 end
162
162
163 def test_should_not_move_a_project_to_its_children
163 def test_should_not_move_a_project_to_its_children
164 sub = @ecookbook
164 sub = @ecookbook
165 assert !(sub.set_parent!(Project.find(3)))
165 assert !(sub.set_parent!(Project.find(3)))
166 end
166 end
167
167
168 def test_set_parent_should_add_roots_in_alphabetical_order
168 def test_set_parent_should_add_roots_in_alphabetical_order
169 ProjectCustomField.delete_all
169 ProjectCustomField.delete_all
170 Project.delete_all
170 Project.delete_all
171 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
171 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
172 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
172 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
173 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
173 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
174 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
174 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
175
175
176 assert_equal 4, Project.count
176 assert_equal 4, Project.count
177 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
177 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
178 end
178 end
179
179
180 def test_set_parent_should_add_children_in_alphabetical_order
180 def test_set_parent_should_add_children_in_alphabetical_order
181 ProjectCustomField.delete_all
181 ProjectCustomField.delete_all
182 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
182 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
183 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
183 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
184 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
184 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
185 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
185 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
186 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
186 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
187
187
188 parent.reload
188 parent.reload
189 assert_equal 4, parent.children.size
189 assert_equal 4, parent.children.size
190 assert_equal parent.children.sort_by(&:name), parent.children
190 assert_equal parent.children.sort_by(&:name), parent.children
191 end
191 end
192
192
193 def test_rebuild_should_sort_children_alphabetically
193 def test_rebuild_should_sort_children_alphabetically
194 ProjectCustomField.delete_all
194 ProjectCustomField.delete_all
195 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
195 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
196 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
196 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
197 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
197 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
198 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
198 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
199 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
199 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
200
200
201 Project.update_all("lft = NULL, rgt = NULL")
201 Project.update_all("lft = NULL, rgt = NULL")
202 Project.rebuild!
202 Project.rebuild!
203
203
204 parent.reload
204 parent.reload
205 assert_equal 4, parent.children.size
205 assert_equal 4, parent.children.size
206 assert_equal parent.children.sort_by(&:name), parent.children
206 assert_equal parent.children.sort_by(&:name), parent.children
207 end
207 end
208
208
209 def test_parent
209 def test_parent
210 p = Project.find(6).parent
210 p = Project.find(6).parent
211 assert p.is_a?(Project)
211 assert p.is_a?(Project)
212 assert_equal 5, p.id
212 assert_equal 5, p.id
213 end
213 end
214
214
215 def test_ancestors
215 def test_ancestors
216 a = Project.find(6).ancestors
216 a = Project.find(6).ancestors
217 assert a.first.is_a?(Project)
217 assert a.first.is_a?(Project)
218 assert_equal [1, 5], a.collect(&:id)
218 assert_equal [1, 5], a.collect(&:id)
219 end
219 end
220
220
221 def test_root
221 def test_root
222 r = Project.find(6).root
222 r = Project.find(6).root
223 assert r.is_a?(Project)
223 assert r.is_a?(Project)
224 assert_equal 1, r.id
224 assert_equal 1, r.id
225 end
225 end
226
226
227 def test_children
227 def test_children
228 c = Project.find(1).children
228 c = Project.find(1).children
229 assert c.first.is_a?(Project)
229 assert c.first.is_a?(Project)
230 assert_equal [5, 3, 4], c.collect(&:id)
230 assert_equal [5, 3, 4], c.collect(&:id)
231 end
231 end
232
232
233 def test_descendants
233 def test_descendants
234 d = Project.find(1).descendants
234 d = Project.find(1).descendants
235 assert d.first.is_a?(Project)
235 assert d.first.is_a?(Project)
236 assert_equal [5, 6, 3, 4], d.collect(&:id)
236 assert_equal [5, 6, 3, 4], d.collect(&:id)
237 end
237 end
238
238
239 def test_users_by_role
239 def test_users_by_role
240 users_by_role = Project.find(1).users_by_role
240 users_by_role = Project.find(1).users_by_role
241 assert_kind_of Hash, users_by_role
241 assert_kind_of Hash, users_by_role
242 role = Role.find(1)
242 role = Role.find(1)
243 assert_kind_of Array, users_by_role[role]
243 assert_kind_of Array, users_by_role[role]
244 assert users_by_role[role].include?(User.find(2))
244 assert users_by_role[role].include?(User.find(2))
245 end
245 end
246
246
247 def test_rolled_up_trackers
247 def test_rolled_up_trackers
248 parent = Project.find(1)
248 parent = Project.find(1)
249 parent.trackers = Tracker.find([1,2])
249 parent.trackers = Tracker.find([1,2])
250 child = parent.children.find(3)
250 child = parent.children.find(3)
251
251
252 assert_equal [1, 2], parent.tracker_ids
252 assert_equal [1, 2], parent.tracker_ids
253 assert_equal [2, 3], child.trackers.collect(&:id)
253 assert_equal [2, 3], child.trackers.collect(&:id)
254
254
255 assert_kind_of Tracker, parent.rolled_up_trackers.first
255 assert_kind_of Tracker, parent.rolled_up_trackers.first
256 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
256 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
257
257
258 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
258 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
259 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
259 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
260 end
260 end
261
261
262 def test_rolled_up_trackers_should_ignore_archived_subprojects
262 def test_rolled_up_trackers_should_ignore_archived_subprojects
263 parent = Project.find(1)
263 parent = Project.find(1)
264 parent.trackers = Tracker.find([1,2])
264 parent.trackers = Tracker.find([1,2])
265 child = parent.children.find(3)
265 child = parent.children.find(3)
266 child.trackers = Tracker.find([1,3])
266 child.trackers = Tracker.find([1,3])
267 parent.children.each(&:archive)
267 parent.children.each(&:archive)
268
268
269 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
269 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
270 end
270 end
271
271
272 def test_next_identifier
272 def test_next_identifier
273 ProjectCustomField.delete_all
273 ProjectCustomField.delete_all
274 Project.create!(:name => 'last', :identifier => 'p2008040')
274 Project.create!(:name => 'last', :identifier => 'p2008040')
275 assert_equal 'p2008041', Project.next_identifier
275 assert_equal 'p2008041', Project.next_identifier
276 end
276 end
277
277
278 def test_next_identifier_first_project
278 def test_next_identifier_first_project
279 Project.delete_all
279 Project.delete_all
280 assert_nil Project.next_identifier
280 assert_nil Project.next_identifier
281 end
281 end
282
282
283
283
284 def test_enabled_module_names_should_not_recreate_enabled_modules
284 def test_enabled_module_names_should_not_recreate_enabled_modules
285 project = Project.find(1)
285 project = Project.find(1)
286 # Remove one module
286 # Remove one module
287 modules = project.enabled_modules.slice(0..-2)
287 modules = project.enabled_modules.slice(0..-2)
288 assert modules.any?
288 assert modules.any?
289 assert_difference 'EnabledModule.count', -1 do
289 assert_difference 'EnabledModule.count', -1 do
290 project.enabled_module_names = modules.collect(&:name)
290 project.enabled_module_names = modules.collect(&:name)
291 end
291 end
292 project.reload
292 project.reload
293 # Ids should be preserved
293 # Ids should be preserved
294 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
294 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
295 end
295 end
296
296
297 def test_copy_from_existing_project
297 def test_copy_from_existing_project
298 source_project = Project.find(1)
298 source_project = Project.find(1)
299 copied_project = Project.copy_from(1)
299 copied_project = Project.copy_from(1)
300
300
301 assert copied_project
301 assert copied_project
302 # Cleared attributes
302 # Cleared attributes
303 assert copied_project.id.blank?
303 assert copied_project.id.blank?
304 assert copied_project.name.blank?
304 assert copied_project.name.blank?
305 assert copied_project.identifier.blank?
305 assert copied_project.identifier.blank?
306
306
307 # Duplicated attributes
307 # Duplicated attributes
308 assert_equal source_project.description, copied_project.description
308 assert_equal source_project.description, copied_project.description
309 assert_equal source_project.enabled_modules, copied_project.enabled_modules
309 assert_equal source_project.enabled_modules, copied_project.enabled_modules
310 assert_equal source_project.trackers, copied_project.trackers
310 assert_equal source_project.trackers, copied_project.trackers
311
311
312 # Default attributes
312 # Default attributes
313 assert_equal 1, copied_project.status
313 assert_equal 1, copied_project.status
314 end
314 end
315
315
316 def test_activities_should_use_the_system_activities
316 def test_activities_should_use_the_system_activities
317 project = Project.find(1)
317 project = Project.find(1)
318 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
318 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
319 end
319 end
320
320
321
321
322 def test_activities_should_use_the_project_specific_activities
322 def test_activities_should_use_the_project_specific_activities
323 project = Project.find(1)
323 project = Project.find(1)
324 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
324 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
325 assert overridden_activity.save!
325 assert overridden_activity.save!
326
326
327 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
327 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
328 end
328 end
329
329
330 def test_activities_should_not_include_the_inactive_project_specific_activities
330 def test_activities_should_not_include_the_inactive_project_specific_activities
331 project = Project.find(1)
331 project = Project.find(1)
332 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
332 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
333 assert overridden_activity.save!
333 assert overridden_activity.save!
334
334
335 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
335 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
336 end
336 end
337
337
338 def test_activities_should_not_include_project_specific_activities_from_other_projects
338 def test_activities_should_not_include_project_specific_activities_from_other_projects
339 project = Project.find(1)
339 project = Project.find(1)
340 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
340 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
341 assert overridden_activity.save!
341 assert overridden_activity.save!
342
342
343 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
343 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
344 end
344 end
345
345
346 def test_activities_should_handle_nils
346 def test_activities_should_handle_nils
347 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
347 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
348 TimeEntryActivity.delete_all
348 TimeEntryActivity.delete_all
349
349
350 # No activities
350 # No activities
351 project = Project.find(1)
351 project = Project.find(1)
352 assert project.activities.empty?
352 assert project.activities.empty?
353
353
354 # No system, one overridden
354 # No system, one overridden
355 assert overridden_activity.save!
355 assert overridden_activity.save!
356 project.reload
356 project.reload
357 assert_equal [overridden_activity], project.activities
357 assert_equal [overridden_activity], project.activities
358 end
358 end
359
359
360 def test_activities_should_override_system_activities_with_project_activities
360 def test_activities_should_override_system_activities_with_project_activities
361 project = Project.find(1)
361 project = Project.find(1)
362 parent_activity = TimeEntryActivity.find(:first)
362 parent_activity = TimeEntryActivity.find(:first)
363 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
363 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
364 assert overridden_activity.save!
364 assert overridden_activity.save!
365
365
366 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
366 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
367 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
367 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
368 end
368 end
369
369
370 def test_activities_should_include_inactive_activities_if_specified
370 def test_activities_should_include_inactive_activities_if_specified
371 project = Project.find(1)
371 project = Project.find(1)
372 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
372 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
373 assert overridden_activity.save!
373 assert overridden_activity.save!
374
374
375 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
375 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
376 end
376 end
377
377
378 context "Project#copy" do
378 context "Project#copy" do
379 setup do
379 setup do
380 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
380 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
381 Project.destroy_all :identifier => "copy-test"
381 Project.destroy_all :identifier => "copy-test"
382 @source_project = Project.find(2)
382 @source_project = Project.find(2)
383 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
383 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
384 @project.trackers = @source_project.trackers
384 @project.trackers = @source_project.trackers
385 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
385 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
386 end
386 end
387
387
388 should "copy issues" do
388 should "copy issues" do
389 assert @project.valid?
389 assert @project.valid?
390 assert @project.issues.empty?
390 assert @project.issues.empty?
391 assert @project.copy(@source_project)
391 assert @project.copy(@source_project)
392
392
393 assert_equal @source_project.issues.size, @project.issues.size
393 assert_equal @source_project.issues.size, @project.issues.size
394 @project.issues.each do |issue|
394 @project.issues.each do |issue|
395 assert issue.valid?
395 assert issue.valid?
396 assert ! issue.assigned_to.blank?
396 assert ! issue.assigned_to.blank?
397 assert_equal @project, issue.project
397 assert_equal @project, issue.project
398 end
398 end
399 end
399 end
400
400
401 should "change the new issues to use the copied version" do
401 should "change the new issues to use the copied version" do
402 assigned_version = Version.generate!(:name => "Assigned Issues")
402 assigned_version = Version.generate!(:name => "Assigned Issues")
403 @source_project.versions << assigned_version
403 @source_project.versions << assigned_version
404 assert_equal 1, @source_project.versions.size
404 assert_equal 1, @source_project.versions.size
405 @source_project.issues << Issue.generate!(:fixed_version_id => assigned_version.id,
405 @source_project.issues << Issue.generate!(:fixed_version_id => assigned_version.id,
406 :subject => "change the new issues to use the copied version",
406 :subject => "change the new issues to use the copied version",
407 :tracker_id => 1,
407 :tracker_id => 1,
408 :project_id => @source_project.id)
408 :project_id => @source_project.id)
409
409
410 assert @project.copy(@source_project)
410 assert @project.copy(@source_project)
411 @project.reload
411 @project.reload
412 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
412 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
413
413
414 assert copied_issue
414 assert copied_issue
415 assert copied_issue.fixed_version
415 assert copied_issue.fixed_version
416 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
416 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
417 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
417 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
418 end
418 end
419
419
420 should "copy members" do
420 should "copy members" do
421 assert @project.valid?
421 assert @project.valid?
422 assert @project.members.empty?
422 assert @project.members.empty?
423 assert @project.copy(@source_project)
423 assert @project.copy(@source_project)
424
424
425 assert_equal @source_project.members.size, @project.members.size
425 assert_equal @source_project.members.size, @project.members.size
426 @project.members.each do |member|
426 @project.members.each do |member|
427 assert member
427 assert member
428 assert_equal @project, member.project
428 assert_equal @project, member.project
429 end
429 end
430 end
430 end
431
431
432 should "copy project specific queries" do
432 should "copy project specific queries" do
433 assert @project.valid?
433 assert @project.valid?
434 assert @project.queries.empty?
434 assert @project.queries.empty?
435 assert @project.copy(@source_project)
435 assert @project.copy(@source_project)
436
436
437 assert_equal @source_project.queries.size, @project.queries.size
437 assert_equal @source_project.queries.size, @project.queries.size
438 @project.queries.each do |query|
438 @project.queries.each do |query|
439 assert query
439 assert query
440 assert_equal @project, query.project
440 assert_equal @project, query.project
441 end
441 end
442 end
442 end
443
443
444 should "copy versions" do
444 should "copy versions" do
445 @source_project.versions << Version.generate!
445 @source_project.versions << Version.generate!
446 @source_project.versions << Version.generate!
446 @source_project.versions << Version.generate!
447
447
448 assert @project.versions.empty?
448 assert @project.versions.empty?
449 assert @project.copy(@source_project)
449 assert @project.copy(@source_project)
450
450
451 assert_equal @source_project.versions.size, @project.versions.size
451 assert_equal @source_project.versions.size, @project.versions.size
452 @project.versions.each do |version|
452 @project.versions.each do |version|
453 assert version
453 assert version
454 assert_equal @project, version.project
454 assert_equal @project, version.project
455 end
455 end
456 end
456 end
457
457
458 should "copy wiki" do
458 should "copy wiki" do
459 assert_difference 'Wiki.count' do
459 assert_difference 'Wiki.count' do
460 assert @project.copy(@source_project)
460 assert @project.copy(@source_project)
461 end
461 end
462
462
463 assert @project.wiki
463 assert @project.wiki
464 assert_not_equal @source_project.wiki, @project.wiki
464 assert_not_equal @source_project.wiki, @project.wiki
465 assert_equal "Start page", @project.wiki.start_page
465 assert_equal "Start page", @project.wiki.start_page
466 end
466 end
467
467
468 should "copy wiki pages and content" do
468 should "copy wiki pages and content" do
469 assert @project.copy(@source_project)
469 assert @project.copy(@source_project)
470
470
471 assert @project.wiki
471 assert @project.wiki
472 assert_equal 1, @project.wiki.pages.length
472 assert_equal 1, @project.wiki.pages.length
473
473
474 @project.wiki.pages.each do |wiki_page|
474 @project.wiki.pages.each do |wiki_page|
475 assert wiki_page.content
475 assert wiki_page.content
476 assert !@source_project.wiki.pages.include?(wiki_page)
476 assert !@source_project.wiki.pages.include?(wiki_page)
477 end
477 end
478 end
478 end
479
479
480 should "copy custom fields"
480 should "copy custom fields"
481
481
482 should "copy issue categories" do
482 should "copy issue categories" do
483 assert @project.copy(@source_project)
483 assert @project.copy(@source_project)
484
484
485 assert_equal 2, @project.issue_categories.size
485 assert_equal 2, @project.issue_categories.size
486 @project.issue_categories.each do |issue_category|
486 @project.issue_categories.each do |issue_category|
487 assert !@source_project.issue_categories.include?(issue_category)
487 assert !@source_project.issue_categories.include?(issue_category)
488 end
488 end
489 end
489 end
490
490
491 should "copy boards" do
492 assert @project.copy(@source_project)
493
494 assert_equal 1, @project.boards.size
495 @project.boards.each do |board|
496 assert !@source_project.boards.include?(board)
497 end
498 end
499
491 should "change the new issues to use the copied issue categories" do
500 should "change the new issues to use the copied issue categories" do
492 issue = Issue.find(4)
501 issue = Issue.find(4)
493 issue.update_attribute(:category_id, 3)
502 issue.update_attribute(:category_id, 3)
494
503
495 assert @project.copy(@source_project)
504 assert @project.copy(@source_project)
496
505
497 @project.issues.each do |issue|
506 @project.issues.each do |issue|
498 assert issue.category
507 assert issue.category
499 assert_equal "Stock management", issue.category.name # Same name
508 assert_equal "Stock management", issue.category.name # Same name
500 assert_not_equal IssueCategory.find(3), issue.category # Different record
509 assert_not_equal IssueCategory.find(3), issue.category # Different record
501 end
510 end
502 end
511 end
503
512
504 should "limit copy with :only option" do
513 should "limit copy with :only option" do
505 assert @project.members.empty?
514 assert @project.members.empty?
506 assert @project.issue_categories.empty?
515 assert @project.issue_categories.empty?
507 assert @source_project.issues.any?
516 assert @source_project.issues.any?
508
517
509 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
518 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
510
519
511 assert @project.members.any?
520 assert @project.members.any?
512 assert @project.issue_categories.any?
521 assert @project.issue_categories.any?
513 assert @project.issues.empty?
522 assert @project.issues.empty?
514 end
523 end
515
524
516 should "copy issue relations"
525 should "copy issue relations"
517 should "link issue relations if cross project issue relations are valid"
526 should "link issue relations if cross project issue relations are valid"
518
527
519 end
528 end
520
529
521 end
530 end
General Comments 0
You need to be logged in to leave comments. Login now