##// END OF EJS Templates
Improved Project#copy to copy more data from the source Project. #3367...
Eric Davis -
r2820:a150689be928
parent child
Show More
@@ -0,0 +1,10
1 class Version < ActiveRecord::Base
2 generator_for :name, :method => :next_name
3
4 def self.next_name
5 @last_name ||= 'Version 1.0.0'
6 @last_name.succ!
7 @last_name
8 end
9
10 end
@@ -1,415 +1,449
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 # Project statuses
20 20 STATUS_ACTIVE = 1
21 21 STATUS_ARCHIVED = 9
22 22
23 23 has_many :members, :include => :user, :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 24 has_many :member_principals, :class_name => 'Member',
25 25 :include => :principal,
26 26 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
27 27 has_many :users, :through => :members
28 28 has_many :principals, :through => :member_principals, :source => :principal
29 29
30 30 has_many :enabled_modules, :dependent => :delete_all
31 31 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
32 32 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
33 33 has_many :issue_changes, :through => :issues, :source => :journals
34 34 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
35 35 has_many :time_entries, :dependent => :delete_all
36 36 has_many :queries, :dependent => :delete_all
37 37 has_many :documents, :dependent => :destroy
38 38 has_many :news, :dependent => :delete_all, :include => :author
39 39 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
40 40 has_many :boards, :dependent => :destroy, :order => "position ASC"
41 41 has_one :repository, :dependent => :destroy
42 42 has_many :changesets, :through => :repository
43 43 has_one :wiki, :dependent => :destroy
44 44 # Custom field for the project issues
45 45 has_and_belongs_to_many :issue_custom_fields,
46 46 :class_name => 'IssueCustomField',
47 47 :order => "#{CustomField.table_name}.position",
48 48 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
49 49 :association_foreign_key => 'custom_field_id'
50 50
51 51 acts_as_nested_set :order => 'name', :dependent => :destroy
52 52 acts_as_attachable :view_permission => :view_files,
53 53 :delete_permission => :manage_files
54 54
55 55 acts_as_customizable
56 56 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
57 57 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
58 58 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
59 59 :author => nil
60 60
61 61 attr_protected :status, :enabled_module_names
62 62
63 63 validates_presence_of :name, :identifier
64 64 validates_uniqueness_of :name, :identifier
65 65 validates_associated :repository, :wiki
66 66 validates_length_of :name, :maximum => 30
67 67 validates_length_of :homepage, :maximum => 255
68 68 validates_length_of :identifier, :in => 1..20
69 69 # donwcase letters, digits, dashes but not digits only
70 70 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
71 71 # reserved words
72 72 validates_exclusion_of :identifier, :in => %w( new )
73 73
74 74 before_destroy :delete_all_members
75 75
76 76 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] } }
77 77 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
78 78 named_scope :public, { :conditions => { :is_public => true } }
79 79 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
80 80
81 81 def identifier=(identifier)
82 82 super unless identifier_frozen?
83 83 end
84 84
85 85 def identifier_frozen?
86 86 errors[:identifier].nil? && !(new_record? || identifier.blank?)
87 87 end
88 88
89 89 def issues_with_subprojects(include_subprojects=false)
90 90 conditions = nil
91 91 if include_subprojects
92 92 ids = [id] + descendants.collect(&:id)
93 93 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
94 94 end
95 95 conditions ||= ["#{Project.table_name}.id = ?", id]
96 96 # Quick and dirty fix for Rails 2 compatibility
97 97 Issue.send(:with_scope, :find => { :conditions => conditions }) do
98 98 Version.send(:with_scope, :find => { :conditions => conditions }) do
99 99 yield
100 100 end
101 101 end
102 102 end
103 103
104 104 # returns latest created projects
105 105 # non public projects will be returned only if user is a member of those
106 106 def self.latest(user=nil, count=5)
107 107 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
108 108 end
109 109
110 110 # Returns a SQL :conditions string used to find all active projects for the specified user.
111 111 #
112 112 # Examples:
113 113 # Projects.visible_by(admin) => "projects.status = 1"
114 114 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
115 115 def self.visible_by(user=nil)
116 116 user ||= User.current
117 117 if user && user.admin?
118 118 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
119 119 elsif user && user.memberships.any?
120 120 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(',')}))"
121 121 else
122 122 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
123 123 end
124 124 end
125 125
126 126 def self.allowed_to_condition(user, permission, options={})
127 127 statements = []
128 128 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
129 129 if perm = Redmine::AccessControl.permission(permission)
130 130 unless perm.project_module.nil?
131 131 # If the permission belongs to a project module, make sure the module is enabled
132 132 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
133 133 end
134 134 end
135 135 if options[:project]
136 136 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
137 137 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
138 138 base_statement = "(#{project_statement}) AND (#{base_statement})"
139 139 end
140 140 if user.admin?
141 141 # no restriction
142 142 else
143 143 statements << "1=0"
144 144 if user.logged?
145 145 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
146 146 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
147 147 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
148 148 elsif Role.anonymous.allowed_to?(permission)
149 149 # anonymous user allowed on public project
150 150 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
151 151 else
152 152 # anonymous user is not authorized
153 153 end
154 154 end
155 155 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
156 156 end
157 157
158 158 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
159 159 #
160 160 # Examples:
161 161 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
162 162 # project.project_condition(false) => "projects.id = 1"
163 163 def project_condition(with_subprojects)
164 164 cond = "#{Project.table_name}.id = #{id}"
165 165 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
166 166 cond
167 167 end
168 168
169 169 def self.find(*args)
170 170 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
171 171 project = find_by_identifier(*args)
172 172 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
173 173 project
174 174 else
175 175 super
176 176 end
177 177 end
178 178
179 179 def to_param
180 180 # id is used for projects with a numeric identifier (compatibility)
181 181 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
182 182 end
183 183
184 184 def active?
185 185 self.status == STATUS_ACTIVE
186 186 end
187 187
188 188 # Archives the project and its descendants recursively
189 189 def archive
190 190 # Archive subprojects if any
191 191 children.each do |subproject|
192 192 subproject.archive
193 193 end
194 194 update_attribute :status, STATUS_ARCHIVED
195 195 end
196 196
197 197 # Unarchives the project
198 198 # All its ancestors must be active
199 199 def unarchive
200 200 return false if ancestors.detect {|a| !a.active?}
201 201 update_attribute :status, STATUS_ACTIVE
202 202 end
203 203
204 204 # Returns an array of projects the project can be moved to
205 205 def possible_parents
206 206 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
207 207 end
208 208
209 209 # Sets the parent of the project
210 210 # Argument can be either a Project, a String, a Fixnum or nil
211 211 def set_parent!(p)
212 212 unless p.nil? || p.is_a?(Project)
213 213 if p.to_s.blank?
214 214 p = nil
215 215 else
216 216 p = Project.find_by_id(p)
217 217 return false unless p
218 218 end
219 219 end
220 220 if p == parent && !p.nil?
221 221 # Nothing to do
222 222 true
223 223 elsif p.nil? || (p.active? && move_possible?(p))
224 224 # Insert the project so that target's children or root projects stay alphabetically sorted
225 225 sibs = (p.nil? ? self.class.roots : p.children)
226 226 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
227 227 if to_be_inserted_before
228 228 move_to_left_of(to_be_inserted_before)
229 229 elsif p.nil?
230 230 if sibs.empty?
231 231 # move_to_root adds the project in first (ie. left) position
232 232 move_to_root
233 233 else
234 234 move_to_right_of(sibs.last) unless self == sibs.last
235 235 end
236 236 else
237 237 # move_to_child_of adds the project in last (ie.right) position
238 238 move_to_child_of(p)
239 239 end
240 240 true
241 241 else
242 242 # Can not move to the given target
243 243 false
244 244 end
245 245 end
246 246
247 247 # Returns an array of the trackers used by the project and its active sub projects
248 248 def rolled_up_trackers
249 249 @rolled_up_trackers ||=
250 250 Tracker.find(:all, :include => :projects,
251 251 :select => "DISTINCT #{Tracker.table_name}.*",
252 252 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
253 253 :order => "#{Tracker.table_name}.position")
254 254 end
255 255
256 256 # Returns a hash of project users grouped by role
257 257 def users_by_role
258 258 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
259 259 m.roles.each do |r|
260 260 h[r] ||= []
261 261 h[r] << m.user
262 262 end
263 263 h
264 264 end
265 265 end
266 266
267 267 # Deletes all project's members
268 268 def delete_all_members
269 269 me, mr = Member.table_name, MemberRole.table_name
270 270 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
271 271 Member.delete_all(['project_id = ?', id])
272 272 end
273 273
274 274 # Users issues can be assigned to
275 275 def assignable_users
276 276 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
277 277 end
278 278
279 279 # Returns the mail adresses of users that should be always notified on project events
280 280 def recipients
281 281 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
282 282 end
283 283
284 284 # Returns an array of all custom fields enabled for project issues
285 285 # (explictly associated custom fields and custom fields enabled for all projects)
286 286 def all_issue_custom_fields
287 287 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
288 288 end
289 289
290 290 def project
291 291 self
292 292 end
293 293
294 294 def <=>(project)
295 295 name.downcase <=> project.name.downcase
296 296 end
297 297
298 298 def to_s
299 299 name
300 300 end
301 301
302 302 # Returns a short description of the projects (first lines)
303 303 def short_description(length = 255)
304 304 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
305 305 end
306 306
307 307 # Return true if this project is allowed to do the specified action.
308 308 # action can be:
309 309 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
310 310 # * a permission Symbol (eg. :edit_project)
311 311 def allows_to?(action)
312 312 if action.is_a? Hash
313 313 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
314 314 else
315 315 allowed_permissions.include? action
316 316 end
317 317 end
318 318
319 319 def module_enabled?(module_name)
320 320 module_name = module_name.to_s
321 321 enabled_modules.detect {|m| m.name == module_name}
322 322 end
323 323
324 324 def enabled_module_names=(module_names)
325 325 if module_names && module_names.is_a?(Array)
326 326 module_names = module_names.collect(&:to_s)
327 327 # remove disabled modules
328 328 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
329 329 # add new modules
330 330 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
331 331 else
332 332 enabled_modules.clear
333 333 end
334 334 end
335 335
336 336 # Returns an auto-generated project identifier based on the last identifier used
337 337 def self.next_identifier
338 338 p = Project.find(:first, :order => 'created_on DESC')
339 339 p.nil? ? nil : p.identifier.to_s.succ
340 340 end
341 341
342 342 # Copies and saves the Project instance based on the +project+.
343 343 # Will duplicate the source project's:
344 344 # * Issues
345 345 # * Members
346 346 # * Queries
347 347 def copy(project)
348 348 project = project.is_a?(Project) ? project : Project.find(project)
349 349
350 350 Project.transaction do
351 # Wikis
352 self.wiki = Wiki.new(project.wiki.attributes.dup.except("project_id"))
353 project.wiki.pages.each do |page|
354 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("page_id"))
355 new_wiki_page = WikiPage.new(page.attributes.dup.except("wiki_id"))
356 new_wiki_page.content = new_wiki_content
357
358 self.wiki.pages << new_wiki_page
359 end
360
361 # Versions
362 project.versions.each do |version|
363 new_version = Version.new
364 new_version.attributes = version.attributes.dup.except("project_id")
365 self.versions << new_version
366 end
367
368 project.issue_categories.each do |issue_category|
369 new_issue_category = IssueCategory.new
370 new_issue_category.attributes = issue_category.attributes.dup.except("project_id")
371 self.issue_categories << new_issue_category
372 end
373
351 374 # Issues
352 375 project.issues.each do |issue|
353 376 new_issue = Issue.new
354 377 new_issue.copy_from(issue)
378 # Reassign fixed_versions by name, since names are unique per
379 # project and the versions for self are not yet saved
380 if issue.fixed_version
381 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
382 end
383 # Reassign the category by name, since names are unique per
384 # project and the categories for self are not yet saved
385 if issue.category
386 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
387 end
388
355 389 self.issues << new_issue
356 390 end
357 391
358 392 # Members
359 393 project.members.each do |member|
360 394 new_member = Member.new
361 395 new_member.attributes = member.attributes.dup.except("project_id")
362 396 new_member.role_ids = member.role_ids.dup
363 397 new_member.project = self
364 398 self.members << new_member
365 399 end
366 400
367 401 # Queries
368 402 project.queries.each do |query|
369 403 new_query = Query.new
370 404 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
371 405 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
372 406 new_query.project = self
373 407 self.queries << new_query
374 408 end
375 409
376 410 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
377 411 self.save
378 412 end
379 413 end
380 414
381 415
382 416 # Copies +project+ and returns the new instance. This will not save
383 417 # the copy
384 418 def self.copy_from(project)
385 419 begin
386 420 project = project.is_a?(Project) ? project : Project.find(project)
387 421 if project
388 422 # clear unique attributes
389 423 attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
390 424 copy = Project.new(attributes)
391 425 copy.enabled_modules = project.enabled_modules
392 426 copy.trackers = project.trackers
393 427 copy.custom_values = project.custom_values.collect {|v| v.clone}
394 428 copy.issue_custom_fields = project.issue_custom_fields
395 429 return copy
396 430 else
397 431 return nil
398 432 end
399 433 rescue ActiveRecord::RecordNotFound
400 434 return nil
401 435 end
402 436 end
403 437
404 438 private
405 439 def allowed_permissions
406 440 @allowed_permissions ||= begin
407 441 module_names = enabled_modules.collect {|m| m.name}
408 442 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
409 443 end
410 444 end
411 445
412 446 def allowed_actions
413 447 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
414 448 end
415 449 end
@@ -1,30 +1,30
1 class User < ActiveRecord::Base
1 class User < Principal
2 2 generator_for :login, :method => :next_email
3 3 generator_for :mail, :method => :next_email
4 4 generator_for :firstname, :method => :next_firstname
5 5 generator_for :lastname, :method => :next_lastname
6 6
7 7 def self.next_login
8 8 @gen_login ||= 'user1'
9 9 @gen_login.succ!
10 10 @gen_login
11 11 end
12 12
13 13 def self.next_email
14 14 @last_email ||= 'user1'
15 15 @last_email.succ!
16 16 "#{@last_email}@example.com"
17 17 end
18 18
19 19 def self.next_firstname
20 20 @last_firstname ||= 'Bob'
21 21 @last_firstname.succ!
22 22 @last_firstname
23 23 end
24 24
25 25 def self.next_lastname
26 26 @last_lastname ||= 'Doe'
27 27 @last_lastname.succ!
28 28 @last_lastname
29 29 end
30 30 end
@@ -1,74 +1,87
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 ENV["RAILS_ENV"] ||= "test"
19 19 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
20 20 require 'test_help'
21 21 require File.expand_path(File.dirname(__FILE__) + '/helper_testcase')
22 22 require File.join(RAILS_ROOT,'test', 'mocks', 'open_id_authentication_mock.rb')
23 23
24 # TODO: The gem or official version of ObjectDaddy doesn't set
25 # protected attributes so they need to be wrapped.
26 def User.generate_with_protected!(attributes={})
27 user = User.spawn(attributes) do |user|
28 user.login = User.next_login
29 attributes.each do |attr,v|
30 user.send("#{attr}=", v)
31 end
32 end
33 user.save!
34 user
35 end
36
24 37 class ActiveSupport::TestCase
25 38 # Transactional fixtures accelerate your tests by wrapping each test method
26 39 # in a transaction that's rolled back on completion. This ensures that the
27 40 # test database remains unchanged so your fixtures don't have to be reloaded
28 41 # between every test method. Fewer database queries means faster tests.
29 42 #
30 43 # Read Mike Clark's excellent walkthrough at
31 44 # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
32 45 #
33 46 # Every Active Record database supports transactions except MyISAM tables
34 47 # in MySQL. Turn off transactional fixtures in this case; however, if you
35 48 # don't care one way or the other, switching from MyISAM to InnoDB tables
36 49 # is recommended.
37 50 self.use_transactional_fixtures = true
38 51
39 52 # Instantiated fixtures are slow, but give you @david where otherwise you
40 53 # would need people(:david). If you don't want to migrate your existing
41 54 # test cases which use the @david style and don't mind the speed hit (each
42 55 # instantiated fixtures translates to a database query per test method),
43 56 # then set this back to true.
44 57 self.use_instantiated_fixtures = false
45 58
46 59 # Add more helper methods to be used by all tests here...
47 60
48 61 def log_user(login, password)
49 62 get "/login"
50 63 assert_equal nil, session[:user_id]
51 64 assert_response :success
52 65 assert_template "account/login"
53 66 post "/login", :username => login, :password => password
54 67 assert_equal login, User.find(session[:user_id]).login
55 68 end
56 69
57 70 def uploaded_test_file(name, mime)
58 71 ActionController::TestUploadedFile.new(ActiveSupport::TestCase.fixture_path + "/files/#{name}", mime)
59 72 end
60 73
61 74 # Use a temporary directory for attachment related tests
62 75 def set_tmp_attachments_directory
63 76 Dir.mkdir "#{RAILS_ROOT}/tmp/test" unless File.directory?("#{RAILS_ROOT}/tmp/test")
64 77 Dir.mkdir "#{RAILS_ROOT}/tmp/test/attachments" unless File.directory?("#{RAILS_ROOT}/tmp/test/attachments")
65 78 Attachment.storage_path = "#{RAILS_ROOT}/tmp/test/attachments"
66 79 end
67 80
68 81 def with_settings(options, &block)
69 82 saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].dup; h}
70 83 options.each {|k, v| Setting[k] = v}
71 84 yield
72 85 saved_settings.each {|k, v| Setting[k] = v}
73 86 end
74 87 end
@@ -1,342 +1,422
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class ProjectTest < ActiveSupport::TestCase
21 21 fixtures :projects, :enabled_modules,
22 22 :issues, :issue_statuses, :journals, :journal_details,
23 23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
24 24 :queries
25 25
26 26 def setup
27 27 @ecookbook = Project.find(1)
28 28 @ecookbook_sub1 = Project.find(3)
29 29 end
30 30
31 31 def test_truth
32 32 assert_kind_of Project, @ecookbook
33 33 assert_equal "eCookbook", @ecookbook.name
34 34 end
35 35
36 36 def test_update
37 37 assert_equal "eCookbook", @ecookbook.name
38 38 @ecookbook.name = "eCook"
39 39 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
40 40 @ecookbook.reload
41 41 assert_equal "eCook", @ecookbook.name
42 42 end
43 43
44 44 def test_validate
45 45 @ecookbook.name = ""
46 46 assert !@ecookbook.save
47 47 assert_equal 1, @ecookbook.errors.count
48 48 assert_equal I18n.translate('activerecord.errors.messages.blank'), @ecookbook.errors.on(:name)
49 49 end
50 50
51 51 def test_validate_identifier
52 52 to_test = {"abc" => true,
53 53 "ab12" => true,
54 54 "ab-12" => true,
55 55 "12" => false,
56 56 "new" => false}
57 57
58 58 to_test.each do |identifier, valid|
59 59 p = Project.new
60 60 p.identifier = identifier
61 61 p.valid?
62 62 assert_equal valid, p.errors.on('identifier').nil?
63 63 end
64 64 end
65 65
66 66 def test_members_should_be_active_users
67 67 Project.all.each do |project|
68 68 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
69 69 end
70 70 end
71 71
72 72 def test_users_should_be_active_users
73 73 Project.all.each do |project|
74 74 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
75 75 end
76 76 end
77 77
78 78 def test_archive
79 79 user = @ecookbook.members.first.user
80 80 @ecookbook.archive
81 81 @ecookbook.reload
82 82
83 83 assert !@ecookbook.active?
84 84 assert !user.projects.include?(@ecookbook)
85 85 # Subproject are also archived
86 86 assert !@ecookbook.children.empty?
87 87 assert @ecookbook.descendants.active.empty?
88 88 end
89 89
90 90 def test_unarchive
91 91 user = @ecookbook.members.first.user
92 92 @ecookbook.archive
93 93 # A subproject of an archived project can not be unarchived
94 94 assert !@ecookbook_sub1.unarchive
95 95
96 96 # Unarchive project
97 97 assert @ecookbook.unarchive
98 98 @ecookbook.reload
99 99 assert @ecookbook.active?
100 100 assert user.projects.include?(@ecookbook)
101 101 # Subproject can now be unarchived
102 102 @ecookbook_sub1.reload
103 103 assert @ecookbook_sub1.unarchive
104 104 end
105 105
106 106 def test_destroy
107 107 # 2 active members
108 108 assert_equal 2, @ecookbook.members.size
109 109 # and 1 is locked
110 110 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
111 111 # some boards
112 112 assert @ecookbook.boards.any?
113 113
114 114 @ecookbook.destroy
115 115 # make sure that the project non longer exists
116 116 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
117 117 # make sure related data was removed
118 118 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
119 119 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
120 120 end
121 121
122 122 def test_move_an_orphan_project_to_a_root_project
123 123 sub = Project.find(2)
124 124 sub.set_parent! @ecookbook
125 125 assert_equal @ecookbook.id, sub.parent.id
126 126 @ecookbook.reload
127 127 assert_equal 4, @ecookbook.children.size
128 128 end
129 129
130 130 def test_move_an_orphan_project_to_a_subproject
131 131 sub = Project.find(2)
132 132 assert sub.set_parent!(@ecookbook_sub1)
133 133 end
134 134
135 135 def test_move_a_root_project_to_a_project
136 136 sub = @ecookbook
137 137 assert sub.set_parent!(Project.find(2))
138 138 end
139 139
140 140 def test_should_not_move_a_project_to_its_children
141 141 sub = @ecookbook
142 142 assert !(sub.set_parent!(Project.find(3)))
143 143 end
144 144
145 145 def test_set_parent_should_add_roots_in_alphabetical_order
146 146 ProjectCustomField.delete_all
147 147 Project.delete_all
148 148 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
149 149 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
150 150 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
151 151 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
152 152
153 153 assert_equal 4, Project.count
154 154 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
155 155 end
156 156
157 157 def test_set_parent_should_add_children_in_alphabetical_order
158 158 ProjectCustomField.delete_all
159 159 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
160 160 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
161 161 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
162 162 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
163 163 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
164 164
165 165 parent.reload
166 166 assert_equal 4, parent.children.size
167 167 assert_equal parent.children.sort_by(&:name), parent.children
168 168 end
169 169
170 170 def test_rebuild_should_sort_children_alphabetically
171 171 ProjectCustomField.delete_all
172 172 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
173 173 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
174 174 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
175 175 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
176 176 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
177 177
178 178 Project.update_all("lft = NULL, rgt = NULL")
179 179 Project.rebuild!
180 180
181 181 parent.reload
182 182 assert_equal 4, parent.children.size
183 183 assert_equal parent.children.sort_by(&:name), parent.children
184 184 end
185 185
186 186 def test_parent
187 187 p = Project.find(6).parent
188 188 assert p.is_a?(Project)
189 189 assert_equal 5, p.id
190 190 end
191 191
192 192 def test_ancestors
193 193 a = Project.find(6).ancestors
194 194 assert a.first.is_a?(Project)
195 195 assert_equal [1, 5], a.collect(&:id)
196 196 end
197 197
198 198 def test_root
199 199 r = Project.find(6).root
200 200 assert r.is_a?(Project)
201 201 assert_equal 1, r.id
202 202 end
203 203
204 204 def test_children
205 205 c = Project.find(1).children
206 206 assert c.first.is_a?(Project)
207 207 assert_equal [5, 3, 4], c.collect(&:id)
208 208 end
209 209
210 210 def test_descendants
211 211 d = Project.find(1).descendants
212 212 assert d.first.is_a?(Project)
213 213 assert_equal [5, 6, 3, 4], d.collect(&:id)
214 214 end
215 215
216 216 def test_users_by_role
217 217 users_by_role = Project.find(1).users_by_role
218 218 assert_kind_of Hash, users_by_role
219 219 role = Role.find(1)
220 220 assert_kind_of Array, users_by_role[role]
221 221 assert users_by_role[role].include?(User.find(2))
222 222 end
223 223
224 224 def test_rolled_up_trackers
225 225 parent = Project.find(1)
226 226 parent.trackers = Tracker.find([1,2])
227 227 child = parent.children.find(3)
228 228
229 229 assert_equal [1, 2], parent.tracker_ids
230 230 assert_equal [2, 3], child.trackers.collect(&:id)
231 231
232 232 assert_kind_of Tracker, parent.rolled_up_trackers.first
233 233 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
234 234
235 235 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
236 236 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
237 237 end
238 238
239 239 def test_rolled_up_trackers_should_ignore_archived_subprojects
240 240 parent = Project.find(1)
241 241 parent.trackers = Tracker.find([1,2])
242 242 child = parent.children.find(3)
243 243 child.trackers = Tracker.find([1,3])
244 244 parent.children.each(&:archive)
245 245
246 246 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
247 247 end
248 248
249 249 def test_next_identifier
250 250 ProjectCustomField.delete_all
251 251 Project.create!(:name => 'last', :identifier => 'p2008040')
252 252 assert_equal 'p2008041', Project.next_identifier
253 253 end
254 254
255 255 def test_next_identifier_first_project
256 256 Project.delete_all
257 257 assert_nil Project.next_identifier
258 258 end
259 259
260 260
261 261 def test_enabled_module_names_should_not_recreate_enabled_modules
262 262 project = Project.find(1)
263 263 # Remove one module
264 264 modules = project.enabled_modules.slice(0..-2)
265 265 assert modules.any?
266 266 assert_difference 'EnabledModule.count', -1 do
267 267 project.enabled_module_names = modules.collect(&:name)
268 268 end
269 269 project.reload
270 270 # Ids should be preserved
271 271 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
272 272 end
273 273
274 274 def test_copy_from_existing_project
275 275 source_project = Project.find(1)
276 276 copied_project = Project.copy_from(1)
277 277
278 278 assert copied_project
279 279 # Cleared attributes
280 280 assert copied_project.id.blank?
281 281 assert copied_project.name.blank?
282 282 assert copied_project.identifier.blank?
283 283
284 284 # Duplicated attributes
285 285 assert_equal source_project.description, copied_project.description
286 286 assert_equal source_project.enabled_modules, copied_project.enabled_modules
287 287 assert_equal source_project.trackers, copied_project.trackers
288 288
289 289 # Default attributes
290 290 assert_equal 1, copied_project.status
291 291 end
292 292
293 context "#copy" do
293 context "Project#copy" do
294 294 setup do
295 295 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
296 296 Project.destroy_all :identifier => "copy-test"
297 297 @source_project = Project.find(2)
298 298 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
299 299 @project.trackers = @source_project.trackers
300 300 @project.enabled_modules = @source_project.enabled_modules
301 301 end
302 302
303 303 should "copy issues" do
304 304 assert @project.valid?
305 305 assert @project.issues.empty?
306 306 assert @project.copy(@source_project)
307 307
308 308 assert_equal @source_project.issues.size, @project.issues.size
309 309 @project.issues.each do |issue|
310 310 assert issue.valid?
311 311 assert ! issue.assigned_to.blank?
312 312 assert_equal @project, issue.project
313 313 end
314 314 end
315 315
316 should "change the new issues to use the copied version" do
317 assigned_version = Version.generate!(:name => "Assigned Issues")
318 @source_project.versions << assigned_version
319 assert_equal 1, @source_project.versions.size
320 @source_project.issues << Issue.generate!(:fixed_version_id => assigned_version.id,
321 :subject => "change the new issues to use the copied version",
322 :tracker_id => 1,
323 :project_id => @source_project.id)
324
325 assert @project.copy(@source_project)
326 @project.reload
327 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
328
329 assert copied_issue
330 assert copied_issue.fixed_version
331 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
332 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
333 end
334
316 335 should "copy members" do
317 336 assert @project.valid?
318 337 assert @project.members.empty?
319 338 assert @project.copy(@source_project)
320 339
321 340 assert_equal @source_project.members.size, @project.members.size
322 341 @project.members.each do |member|
323 342 assert member
324 343 assert_equal @project, member.project
325 344 end
326 345 end
327 346
328 347 should "copy project specific queries" do
329 348 assert @project.valid?
330 349 assert @project.queries.empty?
331 350 assert @project.copy(@source_project)
332 351
333 352 assert_equal @source_project.queries.size, @project.queries.size
334 353 @project.queries.each do |query|
335 354 assert query
336 355 assert_equal @project, query.project
337 356 end
338 357 end
339 358
359 should "copy versions" do
360 @source_project.versions << Version.generate!
361 @source_project.versions << Version.generate!
362
363 assert @project.versions.empty?
364 assert @project.copy(@source_project)
365
366 assert_equal @source_project.versions.size, @project.versions.size
367 @project.versions.each do |version|
368 assert version
369 assert_equal @project, version.project
370 end
371 end
372
373 should "copy wiki" do
374 assert @project.copy(@source_project)
375
376 assert @project.wiki
377 assert_not_equal @source_project.wiki, @project.wiki
378 assert_equal "Start page", @project.wiki.start_page
379 end
380
381 should "copy wiki pages and content" do
382 assert @project.copy(@source_project)
383
384 assert @project.wiki
385 assert_equal 1, @project.wiki.pages.length
386
387 @project.wiki.pages.each do |wiki_page|
388 assert wiki_page.content
389 assert !@source_project.wiki.pages.include?(wiki_page)
390 end
391 end
392
393 should "copy custom fields"
394
395 should "copy issue categories" do
396 assert @project.copy(@source_project)
397
398 assert_equal 2, @project.issue_categories.size
399 @project.issue_categories.each do |issue_category|
400 assert !@source_project.issue_categories.include?(issue_category)
401 end
402 end
403
404 should "change the new issues to use the copied issue categories" do
405 issue = Issue.find(4)
406 issue.update_attribute(:category_id, 3)
407
408 assert @project.copy(@source_project)
409
410 @project.issues.each do |issue|
411 assert issue.category
412 assert_equal "Stock management", issue.category.name # Same name
413 assert_not_equal IssueCategory.find(3), issue.category # Different record
414 end
415 end
416
417 should "copy issue relations"
418 should "link issue relations if cross project issue relations are valid"
419
340 420 end
341 421
342 422 end
General Comments 0
You need to be logged in to leave comments. Login now