##// END OF EJS Templates
Fixes Project#shared_versions for descendants sharing (#465)....
Jean-Philippe Lang -
r3016:5266e328c029
parent child
Show More
@@ -1,637 +1,637
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 # Specific overidden Activities
24 24 has_many :time_entry_activities
25 25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
26 26 has_many :member_principals, :class_name => 'Member',
27 27 :include => :principal,
28 28 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
29 29 has_many :users, :through => :members
30 30 has_many :principals, :through => :member_principals, :source => :principal
31 31
32 32 has_many :enabled_modules, :dependent => :delete_all
33 33 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
34 34 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
35 35 has_many :issue_changes, :through => :issues, :source => :journals
36 36 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
37 37 has_many :time_entries, :dependent => :delete_all
38 38 has_many :queries, :dependent => :delete_all
39 39 has_many :documents, :dependent => :destroy
40 40 has_many :news, :dependent => :delete_all, :include => :author
41 41 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
42 42 has_many :boards, :dependent => :destroy, :order => "position ASC"
43 43 has_one :repository, :dependent => :destroy
44 44 has_many :changesets, :through => :repository
45 45 has_one :wiki, :dependent => :destroy
46 46 # Custom field for the project issues
47 47 has_and_belongs_to_many :issue_custom_fields,
48 48 :class_name => 'IssueCustomField',
49 49 :order => "#{CustomField.table_name}.position",
50 50 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
51 51 :association_foreign_key => 'custom_field_id'
52 52
53 53 acts_as_nested_set :order => 'name', :dependent => :destroy
54 54 acts_as_attachable :view_permission => :view_files,
55 55 :delete_permission => :manage_files
56 56
57 57 acts_as_customizable
58 58 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
59 59 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
60 60 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
61 61 :author => nil
62 62
63 63 attr_protected :status, :enabled_module_names
64 64
65 65 validates_presence_of :name, :identifier
66 66 validates_uniqueness_of :name, :identifier
67 67 validates_associated :repository, :wiki
68 68 validates_length_of :name, :maximum => 30
69 69 validates_length_of :homepage, :maximum => 255
70 70 validates_length_of :identifier, :in => 1..20
71 71 # donwcase letters, digits, dashes but not digits only
72 72 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
73 73 # reserved words
74 74 validates_exclusion_of :identifier, :in => %w( new )
75 75
76 76 before_destroy :delete_all_members
77 77
78 78 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] } }
79 79 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
80 80 named_scope :all_public, { :conditions => { :is_public => true } }
81 81 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
82 82
83 83 def identifier=(identifier)
84 84 super unless identifier_frozen?
85 85 end
86 86
87 87 def identifier_frozen?
88 88 errors[:identifier].nil? && !(new_record? || identifier.blank?)
89 89 end
90 90
91 91 # returns latest created projects
92 92 # non public projects will be returned only if user is a member of those
93 93 def self.latest(user=nil, count=5)
94 94 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
95 95 end
96 96
97 97 # Returns a SQL :conditions string used to find all active projects for the specified user.
98 98 #
99 99 # Examples:
100 100 # Projects.visible_by(admin) => "projects.status = 1"
101 101 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
102 102 def self.visible_by(user=nil)
103 103 user ||= User.current
104 104 if user && user.admin?
105 105 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
106 106 elsif user && user.memberships.any?
107 107 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(',')}))"
108 108 else
109 109 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
110 110 end
111 111 end
112 112
113 113 def self.allowed_to_condition(user, permission, options={})
114 114 statements = []
115 115 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
116 116 if perm = Redmine::AccessControl.permission(permission)
117 117 unless perm.project_module.nil?
118 118 # If the permission belongs to a project module, make sure the module is enabled
119 119 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
120 120 end
121 121 end
122 122 if options[:project]
123 123 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
124 124 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
125 125 base_statement = "(#{project_statement}) AND (#{base_statement})"
126 126 end
127 127 if user.admin?
128 128 # no restriction
129 129 else
130 130 statements << "1=0"
131 131 if user.logged?
132 132 if Role.non_member.allowed_to?(permission) && !options[:member]
133 133 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
134 134 end
135 135 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
136 136 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
137 137 else
138 138 if Role.anonymous.allowed_to?(permission) && !options[:member]
139 139 # anonymous user allowed on public project
140 140 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
141 141 end
142 142 end
143 143 end
144 144 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
145 145 end
146 146
147 147 # Returns the Systemwide and project specific activities
148 148 def activities(include_inactive=false)
149 149 if include_inactive
150 150 return all_activities
151 151 else
152 152 return active_activities
153 153 end
154 154 end
155 155
156 156 # Will create a new Project specific Activity or update an existing one
157 157 #
158 158 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
159 159 # does not successfully save.
160 160 def update_or_create_time_entry_activity(id, activity_hash)
161 161 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
162 162 self.create_time_entry_activity_if_needed(activity_hash)
163 163 else
164 164 activity = project.time_entry_activities.find_by_id(id.to_i)
165 165 activity.update_attributes(activity_hash) if activity
166 166 end
167 167 end
168 168
169 169 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
170 170 #
171 171 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
172 172 # does not successfully save.
173 173 def create_time_entry_activity_if_needed(activity)
174 174 if activity['parent_id']
175 175
176 176 parent_activity = TimeEntryActivity.find(activity['parent_id'])
177 177 activity['name'] = parent_activity.name
178 178 activity['position'] = parent_activity.position
179 179
180 180 if Enumeration.overridding_change?(activity, parent_activity)
181 181 project_activity = self.time_entry_activities.create(activity)
182 182
183 183 if project_activity.new_record?
184 184 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
185 185 else
186 186 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
187 187 end
188 188 end
189 189 end
190 190 end
191 191
192 192 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
193 193 #
194 194 # Examples:
195 195 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
196 196 # project.project_condition(false) => "projects.id = 1"
197 197 def project_condition(with_subprojects)
198 198 cond = "#{Project.table_name}.id = #{id}"
199 199 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
200 200 cond
201 201 end
202 202
203 203 def self.find(*args)
204 204 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
205 205 project = find_by_identifier(*args)
206 206 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
207 207 project
208 208 else
209 209 super
210 210 end
211 211 end
212 212
213 213 def to_param
214 214 # id is used for projects with a numeric identifier (compatibility)
215 215 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
216 216 end
217 217
218 218 def active?
219 219 self.status == STATUS_ACTIVE
220 220 end
221 221
222 222 # Archives the project and its descendants
223 223 def archive
224 224 # Check that there is no issue of a non descendant project that is assigned
225 225 # to one of the project or descendant versions
226 226 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
227 227 if v_ids.any? && Issue.find(:first, :include => :project,
228 228 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
229 229 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
230 230 return false
231 231 end
232 232 Project.transaction do
233 233 archive!
234 234 end
235 235 true
236 236 end
237 237
238 238 # Unarchives the project
239 239 # All its ancestors must be active
240 240 def unarchive
241 241 return false if ancestors.detect {|a| !a.active?}
242 242 update_attribute :status, STATUS_ACTIVE
243 243 end
244 244
245 245 # Returns an array of projects the project can be moved to
246 246 # by the current user
247 247 def allowed_parents
248 248 return @allowed_parents if @allowed_parents
249 249 @allowed_parents = (Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_project, :member => true)) - self_and_descendants)
250 250 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
251 251 @allowed_parents << parent
252 252 end
253 253 @allowed_parents
254 254 end
255 255
256 256 # Sets the parent of the project with authorization check
257 257 def set_allowed_parent!(p)
258 258 unless p.nil? || p.is_a?(Project)
259 259 if p.to_s.blank?
260 260 p = nil
261 261 else
262 262 p = Project.find_by_id(p)
263 263 return false unless p
264 264 end
265 265 end
266 266 if p.nil?
267 267 if !new_record? && allowed_parents.empty?
268 268 return false
269 269 end
270 270 elsif !allowed_parents.include?(p)
271 271 return false
272 272 end
273 273 set_parent!(p)
274 274 end
275 275
276 276 # Sets the parent of the project
277 277 # Argument can be either a Project, a String, a Fixnum or nil
278 278 def set_parent!(p)
279 279 unless p.nil? || p.is_a?(Project)
280 280 if p.to_s.blank?
281 281 p = nil
282 282 else
283 283 p = Project.find_by_id(p)
284 284 return false unless p
285 285 end
286 286 end
287 287 if p == parent && !p.nil?
288 288 # Nothing to do
289 289 true
290 290 elsif p.nil? || (p.active? && move_possible?(p))
291 291 # Insert the project so that target's children or root projects stay alphabetically sorted
292 292 sibs = (p.nil? ? self.class.roots : p.children)
293 293 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
294 294 if to_be_inserted_before
295 295 move_to_left_of(to_be_inserted_before)
296 296 elsif p.nil?
297 297 if sibs.empty?
298 298 # move_to_root adds the project in first (ie. left) position
299 299 move_to_root
300 300 else
301 301 move_to_right_of(sibs.last) unless self == sibs.last
302 302 end
303 303 else
304 304 # move_to_child_of adds the project in last (ie.right) position
305 305 move_to_child_of(p)
306 306 end
307 307 Issue.update_fixed_versions_from_project_hierarchy_change
308 308 true
309 309 else
310 310 # Can not move to the given target
311 311 false
312 312 end
313 313 end
314 314
315 315 # Returns an array of the trackers used by the project and its active sub projects
316 316 def rolled_up_trackers
317 317 @rolled_up_trackers ||=
318 318 Tracker.find(:all, :include => :projects,
319 319 :select => "DISTINCT #{Tracker.table_name}.*",
320 320 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
321 321 :order => "#{Tracker.table_name}.position")
322 322 end
323 323
324 324 # Closes open and locked project versions that are completed
325 325 def close_completed_versions
326 326 Version.transaction do
327 327 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
328 328 if version.completed?
329 329 version.update_attribute(:status, 'closed')
330 330 end
331 331 end
332 332 end
333 333 end
334 334
335 335 # Returns a scope of the Versions used by the project
336 336 def shared_versions
337 337 @shared_versions ||=
338 338 Version.scoped(:include => :project,
339 339 :conditions => "#{Project.table_name}.id = #{id}" +
340 340 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
341 341 " #{Version.table_name}.sharing = 'system'" +
342 342 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
343 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
344 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
343 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
344 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
345 345 "))")
346 346 end
347 347
348 348 # Returns a hash of project users grouped by role
349 349 def users_by_role
350 350 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
351 351 m.roles.each do |r|
352 352 h[r] ||= []
353 353 h[r] << m.user
354 354 end
355 355 h
356 356 end
357 357 end
358 358
359 359 # Deletes all project's members
360 360 def delete_all_members
361 361 me, mr = Member.table_name, MemberRole.table_name
362 362 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
363 363 Member.delete_all(['project_id = ?', id])
364 364 end
365 365
366 366 # Users issues can be assigned to
367 367 def assignable_users
368 368 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
369 369 end
370 370
371 371 # Returns the mail adresses of users that should be always notified on project events
372 372 def recipients
373 373 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
374 374 end
375 375
376 376 # Returns the users that should be notified on project events
377 377 def notified_users
378 378 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
379 379 end
380 380
381 381 # Returns an array of all custom fields enabled for project issues
382 382 # (explictly associated custom fields and custom fields enabled for all projects)
383 383 def all_issue_custom_fields
384 384 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
385 385 end
386 386
387 387 def project
388 388 self
389 389 end
390 390
391 391 def <=>(project)
392 392 name.downcase <=> project.name.downcase
393 393 end
394 394
395 395 def to_s
396 396 name
397 397 end
398 398
399 399 # Returns a short description of the projects (first lines)
400 400 def short_description(length = 255)
401 401 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
402 402 end
403 403
404 404 # Return true if this project is allowed to do the specified action.
405 405 # action can be:
406 406 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
407 407 # * a permission Symbol (eg. :edit_project)
408 408 def allows_to?(action)
409 409 if action.is_a? Hash
410 410 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
411 411 else
412 412 allowed_permissions.include? action
413 413 end
414 414 end
415 415
416 416 def module_enabled?(module_name)
417 417 module_name = module_name.to_s
418 418 enabled_modules.detect {|m| m.name == module_name}
419 419 end
420 420
421 421 def enabled_module_names=(module_names)
422 422 if module_names && module_names.is_a?(Array)
423 423 module_names = module_names.collect(&:to_s)
424 424 # remove disabled modules
425 425 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
426 426 # add new modules
427 427 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
428 428 else
429 429 enabled_modules.clear
430 430 end
431 431 end
432 432
433 433 # Returns an auto-generated project identifier based on the last identifier used
434 434 def self.next_identifier
435 435 p = Project.find(:first, :order => 'created_on DESC')
436 436 p.nil? ? nil : p.identifier.to_s.succ
437 437 end
438 438
439 439 # Copies and saves the Project instance based on the +project+.
440 440 # Duplicates the source project's:
441 441 # * Wiki
442 442 # * Versions
443 443 # * Categories
444 444 # * Issues
445 445 # * Members
446 446 # * Queries
447 447 #
448 448 # Accepts an +options+ argument to specify what to copy
449 449 #
450 450 # Examples:
451 451 # project.copy(1) # => copies everything
452 452 # project.copy(1, :only => 'members') # => copies members only
453 453 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
454 454 def copy(project, options={})
455 455 project = project.is_a?(Project) ? project : Project.find(project)
456 456
457 457 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
458 458 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
459 459
460 460 Project.transaction do
461 461 if save
462 462 reload
463 463 to_be_copied.each do |name|
464 464 send "copy_#{name}", project
465 465 end
466 466 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
467 467 save
468 468 end
469 469 end
470 470 end
471 471
472 472
473 473 # Copies +project+ and returns the new instance. This will not save
474 474 # the copy
475 475 def self.copy_from(project)
476 476 begin
477 477 project = project.is_a?(Project) ? project : Project.find(project)
478 478 if project
479 479 # clear unique attributes
480 480 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
481 481 copy = Project.new(attributes)
482 482 copy.enabled_modules = project.enabled_modules
483 483 copy.trackers = project.trackers
484 484 copy.custom_values = project.custom_values.collect {|v| v.clone}
485 485 copy.issue_custom_fields = project.issue_custom_fields
486 486 return copy
487 487 else
488 488 return nil
489 489 end
490 490 rescue ActiveRecord::RecordNotFound
491 491 return nil
492 492 end
493 493 end
494 494
495 495 private
496 496
497 497 # Copies wiki from +project+
498 498 def copy_wiki(project)
499 499 # Check that the source project has a wiki first
500 500 unless project.wiki.nil?
501 501 self.wiki ||= Wiki.new
502 502 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
503 503 project.wiki.pages.each do |page|
504 504 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
505 505 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
506 506 new_wiki_page.content = new_wiki_content
507 507 wiki.pages << new_wiki_page
508 508 end
509 509 end
510 510 end
511 511
512 512 # Copies versions from +project+
513 513 def copy_versions(project)
514 514 project.versions.each do |version|
515 515 new_version = Version.new
516 516 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
517 517 self.versions << new_version
518 518 end
519 519 end
520 520
521 521 # Copies issue categories from +project+
522 522 def copy_issue_categories(project)
523 523 project.issue_categories.each do |issue_category|
524 524 new_issue_category = IssueCategory.new
525 525 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
526 526 self.issue_categories << new_issue_category
527 527 end
528 528 end
529 529
530 530 # Copies issues from +project+
531 531 def copy_issues(project)
532 532 project.issues.each do |issue|
533 533 new_issue = Issue.new
534 534 new_issue.copy_from(issue)
535 535 # Reassign fixed_versions by name, since names are unique per
536 536 # project and the versions for self are not yet saved
537 537 if issue.fixed_version
538 538 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
539 539 end
540 540 # Reassign the category by name, since names are unique per
541 541 # project and the categories for self are not yet saved
542 542 if issue.category
543 543 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
544 544 end
545 545 self.issues << new_issue
546 546 end
547 547 end
548 548
549 549 # Copies members from +project+
550 550 def copy_members(project)
551 551 project.members.each do |member|
552 552 new_member = Member.new
553 553 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
554 554 new_member.role_ids = member.role_ids.dup
555 555 new_member.project = self
556 556 self.members << new_member
557 557 end
558 558 end
559 559
560 560 # Copies queries from +project+
561 561 def copy_queries(project)
562 562 project.queries.each do |query|
563 563 new_query = Query.new
564 564 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
565 565 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
566 566 new_query.project = self
567 567 self.queries << new_query
568 568 end
569 569 end
570 570
571 571 # Copies boards from +project+
572 572 def copy_boards(project)
573 573 project.boards.each do |board|
574 574 new_board = Board.new
575 575 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
576 576 new_board.project = self
577 577 self.boards << new_board
578 578 end
579 579 end
580 580
581 581 def allowed_permissions
582 582 @allowed_permissions ||= begin
583 583 module_names = enabled_modules.collect {|m| m.name}
584 584 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
585 585 end
586 586 end
587 587
588 588 def allowed_actions
589 589 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
590 590 end
591 591
592 592 # Returns all the active Systemwide and project specific activities
593 593 def active_activities
594 594 overridden_activity_ids = self.time_entry_activities.active.collect(&:parent_id)
595 595
596 596 if overridden_activity_ids.empty?
597 597 return TimeEntryActivity.shared.active
598 598 else
599 599 return system_activities_and_project_overrides
600 600 end
601 601 end
602 602
603 603 # Returns all the Systemwide and project specific activities
604 604 # (inactive and active)
605 605 def all_activities
606 606 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
607 607
608 608 if overridden_activity_ids.empty?
609 609 return TimeEntryActivity.shared
610 610 else
611 611 return system_activities_and_project_overrides(true)
612 612 end
613 613 end
614 614
615 615 # Returns the systemwide active activities merged with the project specific overrides
616 616 def system_activities_and_project_overrides(include_inactive=false)
617 617 if include_inactive
618 618 return TimeEntryActivity.shared.
619 619 find(:all,
620 620 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
621 621 self.time_entry_activities
622 622 else
623 623 return TimeEntryActivity.shared.active.
624 624 find(:all,
625 625 :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
626 626 self.time_entry_activities.active
627 627 end
628 628 end
629 629
630 630 # Archives subprojects recursively
631 631 def archive!
632 632 children.each do |subproject|
633 633 subproject.send :archive!
634 634 end
635 635 update_attribute :status, STATUS_ARCHIVED
636 636 end
637 637 end
@@ -1,650 +1,700
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 :all
22 22
23 23 def setup
24 24 @ecookbook = Project.find(1)
25 25 @ecookbook_sub1 = Project.find(3)
26 26 User.current = nil
27 27 end
28 28
29 29 should_validate_presence_of :name
30 30 should_validate_presence_of :identifier
31 31
32 32 should_validate_uniqueness_of :name
33 33 should_validate_uniqueness_of :identifier
34 34
35 35 context "associations" do
36 36 should_have_many :members
37 37 should_have_many :users, :through => :members
38 38 should_have_many :member_principals
39 39 should_have_many :principals, :through => :member_principals
40 40 should_have_many :enabled_modules
41 41 should_have_many :issues
42 42 should_have_many :issue_changes, :through => :issues
43 43 should_have_many :versions
44 44 should_have_many :time_entries
45 45 should_have_many :queries
46 46 should_have_many :documents
47 47 should_have_many :news
48 48 should_have_many :issue_categories
49 49 should_have_many :boards
50 50 should_have_many :changesets, :through => :repository
51 51
52 52 should_have_one :repository
53 53 should_have_one :wiki
54 54
55 55 should_have_and_belong_to_many :trackers
56 56 should_have_and_belong_to_many :issue_custom_fields
57 57 end
58 58
59 59 def test_truth
60 60 assert_kind_of Project, @ecookbook
61 61 assert_equal "eCookbook", @ecookbook.name
62 62 end
63 63
64 64 def test_update
65 65 assert_equal "eCookbook", @ecookbook.name
66 66 @ecookbook.name = "eCook"
67 67 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
68 68 @ecookbook.reload
69 69 assert_equal "eCook", @ecookbook.name
70 70 end
71 71
72 72 def test_validate_identifier
73 73 to_test = {"abc" => true,
74 74 "ab12" => true,
75 75 "ab-12" => true,
76 76 "12" => false,
77 77 "new" => false}
78 78
79 79 to_test.each do |identifier, valid|
80 80 p = Project.new
81 81 p.identifier = identifier
82 82 p.valid?
83 83 assert_equal valid, p.errors.on('identifier').nil?
84 84 end
85 85 end
86 86
87 87 def test_members_should_be_active_users
88 88 Project.all.each do |project|
89 89 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
90 90 end
91 91 end
92 92
93 93 def test_users_should_be_active_users
94 94 Project.all.each do |project|
95 95 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
96 96 end
97 97 end
98 98
99 99 def test_archive
100 100 user = @ecookbook.members.first.user
101 101 @ecookbook.archive
102 102 @ecookbook.reload
103 103
104 104 assert !@ecookbook.active?
105 105 assert !user.projects.include?(@ecookbook)
106 106 # Subproject are also archived
107 107 assert !@ecookbook.children.empty?
108 108 assert @ecookbook.descendants.active.empty?
109 109 end
110 110
111 111 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
112 112 # Assign an issue of a project to a version of a child project
113 113 Issue.find(4).update_attribute :fixed_version_id, 4
114 114
115 115 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
116 116 assert_equal false, @ecookbook.archive
117 117 end
118 118 @ecookbook.reload
119 119 assert @ecookbook.active?
120 120 end
121 121
122 122 def test_unarchive
123 123 user = @ecookbook.members.first.user
124 124 @ecookbook.archive
125 125 # A subproject of an archived project can not be unarchived
126 126 assert !@ecookbook_sub1.unarchive
127 127
128 128 # Unarchive project
129 129 assert @ecookbook.unarchive
130 130 @ecookbook.reload
131 131 assert @ecookbook.active?
132 132 assert user.projects.include?(@ecookbook)
133 133 # Subproject can now be unarchived
134 134 @ecookbook_sub1.reload
135 135 assert @ecookbook_sub1.unarchive
136 136 end
137 137
138 138 def test_destroy
139 139 # 2 active members
140 140 assert_equal 2, @ecookbook.members.size
141 141 # and 1 is locked
142 142 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
143 143 # some boards
144 144 assert @ecookbook.boards.any?
145 145
146 146 @ecookbook.destroy
147 147 # make sure that the project non longer exists
148 148 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
149 149 # make sure related data was removed
150 150 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
151 151 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
152 152 end
153 153
154 154 def test_move_an_orphan_project_to_a_root_project
155 155 sub = Project.find(2)
156 156 sub.set_parent! @ecookbook
157 157 assert_equal @ecookbook.id, sub.parent.id
158 158 @ecookbook.reload
159 159 assert_equal 4, @ecookbook.children.size
160 160 end
161 161
162 162 def test_move_an_orphan_project_to_a_subproject
163 163 sub = Project.find(2)
164 164 assert sub.set_parent!(@ecookbook_sub1)
165 165 end
166 166
167 167 def test_move_a_root_project_to_a_project
168 168 sub = @ecookbook
169 169 assert sub.set_parent!(Project.find(2))
170 170 end
171 171
172 172 def test_should_not_move_a_project_to_its_children
173 173 sub = @ecookbook
174 174 assert !(sub.set_parent!(Project.find(3)))
175 175 end
176 176
177 177 def test_set_parent_should_add_roots_in_alphabetical_order
178 178 ProjectCustomField.delete_all
179 179 Project.delete_all
180 180 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
181 181 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
182 182 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
183 183 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
184 184
185 185 assert_equal 4, Project.count
186 186 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
187 187 end
188 188
189 189 def test_set_parent_should_add_children_in_alphabetical_order
190 190 ProjectCustomField.delete_all
191 191 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
192 192 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
193 193 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
194 194 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
195 195 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
196 196
197 197 parent.reload
198 198 assert_equal 4, parent.children.size
199 199 assert_equal parent.children.sort_by(&:name), parent.children
200 200 end
201 201
202 202 def test_rebuild_should_sort_children_alphabetically
203 203 ProjectCustomField.delete_all
204 204 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
205 205 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
206 206 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
207 207 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
208 208 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
209 209
210 210 Project.update_all("lft = NULL, rgt = NULL")
211 211 Project.rebuild!
212 212
213 213 parent.reload
214 214 assert_equal 4, parent.children.size
215 215 assert_equal parent.children.sort_by(&:name), parent.children
216 216 end
217 217
218 218
219 219 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
220 220 # Parent issue with a hierarchy project's fixed version
221 221 parent_issue = Issue.find(1)
222 222 parent_issue.update_attribute(:fixed_version_id, 4)
223 223 parent_issue.reload
224 224 assert_equal 4, parent_issue.fixed_version_id
225 225
226 226 # Should keep fixed versions for the issues
227 227 issue_with_local_fixed_version = Issue.find(5)
228 228 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
229 229 issue_with_local_fixed_version.reload
230 230 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
231 231
232 232 # Local issue with hierarchy fixed_version
233 233 issue_with_hierarchy_fixed_version = Issue.find(13)
234 234 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
235 235 issue_with_hierarchy_fixed_version.reload
236 236 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
237 237
238 238 # Move project out of the issue's hierarchy
239 239 moved_project = Project.find(3)
240 240 moved_project.set_parent!(Project.find(2))
241 241 parent_issue.reload
242 242 issue_with_local_fixed_version.reload
243 243 issue_with_hierarchy_fixed_version.reload
244 244
245 245 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
246 246 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
247 247 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
248 248 end
249 249
250 250 def test_parent
251 251 p = Project.find(6).parent
252 252 assert p.is_a?(Project)
253 253 assert_equal 5, p.id
254 254 end
255 255
256 256 def test_ancestors
257 257 a = Project.find(6).ancestors
258 258 assert a.first.is_a?(Project)
259 259 assert_equal [1, 5], a.collect(&:id)
260 260 end
261 261
262 262 def test_root
263 263 r = Project.find(6).root
264 264 assert r.is_a?(Project)
265 265 assert_equal 1, r.id
266 266 end
267 267
268 268 def test_children
269 269 c = Project.find(1).children
270 270 assert c.first.is_a?(Project)
271 271 assert_equal [5, 3, 4], c.collect(&:id)
272 272 end
273 273
274 274 def test_descendants
275 275 d = Project.find(1).descendants
276 276 assert d.first.is_a?(Project)
277 277 assert_equal [5, 6, 3, 4], d.collect(&:id)
278 278 end
279 279
280 280 def test_allowed_parents_should_be_empty_for_non_member_user
281 281 Role.non_member.add_permission!(:add_project)
282 282 user = User.find(9)
283 283 assert user.memberships.empty?
284 284 User.current = user
285 285 assert Project.new.allowed_parents.empty?
286 286 end
287 287
288 288 def test_users_by_role
289 289 users_by_role = Project.find(1).users_by_role
290 290 assert_kind_of Hash, users_by_role
291 291 role = Role.find(1)
292 292 assert_kind_of Array, users_by_role[role]
293 293 assert users_by_role[role].include?(User.find(2))
294 294 end
295 295
296 296 def test_rolled_up_trackers
297 297 parent = Project.find(1)
298 298 parent.trackers = Tracker.find([1,2])
299 299 child = parent.children.find(3)
300 300
301 301 assert_equal [1, 2], parent.tracker_ids
302 302 assert_equal [2, 3], child.trackers.collect(&:id)
303 303
304 304 assert_kind_of Tracker, parent.rolled_up_trackers.first
305 305 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
306 306
307 307 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
308 308 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
309 309 end
310 310
311 311 def test_rolled_up_trackers_should_ignore_archived_subprojects
312 312 parent = Project.find(1)
313 313 parent.trackers = Tracker.find([1,2])
314 314 child = parent.children.find(3)
315 315 child.trackers = Tracker.find([1,3])
316 316 parent.children.each(&:archive)
317 317
318 318 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
319 319 end
320
321 def test_shared_versions_none_sharing
322 p = Project.find(5)
323 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
324 assert p.shared_versions.include?(v)
325 assert !p.children.first.shared_versions.include?(v)
326 assert !p.root.shared_versions.include?(v)
327 assert !p.siblings.first.shared_versions.include?(v)
328 assert !p.root.siblings.first.shared_versions.include?(v)
329 end
330
331 def test_shared_versions_descendants_sharing
332 p = Project.find(5)
333 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
334 assert p.shared_versions.include?(v)
335 assert p.children.first.shared_versions.include?(v)
336 assert !p.root.shared_versions.include?(v)
337 assert !p.siblings.first.shared_versions.include?(v)
338 assert !p.root.siblings.first.shared_versions.include?(v)
339 end
340
341 def test_shared_versions_hierarchy_sharing
342 p = Project.find(5)
343 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
344 assert p.shared_versions.include?(v)
345 assert p.children.first.shared_versions.include?(v)
346 assert p.root.shared_versions.include?(v)
347 assert !p.siblings.first.shared_versions.include?(v)
348 assert !p.root.siblings.first.shared_versions.include?(v)
349 end
350
351 def test_shared_versions_tree_sharing
352 p = Project.find(5)
353 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
354 assert p.shared_versions.include?(v)
355 assert p.children.first.shared_versions.include?(v)
356 assert p.root.shared_versions.include?(v)
357 assert p.siblings.first.shared_versions.include?(v)
358 assert !p.root.siblings.first.shared_versions.include?(v)
359 end
360
361 def test_shared_versions_system_sharing
362 p = Project.find(5)
363 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
364 assert p.shared_versions.include?(v)
365 assert p.children.first.shared_versions.include?(v)
366 assert p.root.shared_versions.include?(v)
367 assert p.siblings.first.shared_versions.include?(v)
368 assert p.root.siblings.first.shared_versions.include?(v)
369 end
320 370
321 371 def test_shared_versions
322 372 parent = Project.find(1)
323 373 child = parent.children.find(3)
324 374 private_child = parent.children.find(5)
325 375
326 376 assert_equal [1,2,3], parent.version_ids.sort
327 377 assert_equal [4], child.version_ids
328 378 assert_equal [6], private_child.version_ids
329 379 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
330 380
331 381 assert_equal 6, parent.shared_versions.size
332 382 parent.shared_versions.each do |version|
333 383 assert_kind_of Version, version
334 384 end
335 385
336 386 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
337 387 end
338 388
339 389 def test_shared_versions_should_ignore_archived_subprojects
340 390 parent = Project.find(1)
341 391 child = parent.children.find(3)
342 392 child.archive
343 393 parent.reload
344 394
345 395 assert_equal [1,2,3], parent.version_ids.sort
346 396 assert_equal [4], child.version_ids
347 397 assert !parent.shared_versions.collect(&:id).include?(4)
348 398 end
349 399
350 400 def test_shared_versions_visible_to_user
351 401 user = User.find(3)
352 402 parent = Project.find(1)
353 403 child = parent.children.find(5)
354 404
355 405 assert_equal [1,2,3], parent.version_ids.sort
356 406 assert_equal [6], child.version_ids
357 407
358 408 versions = parent.shared_versions.visible(user)
359 409
360 410 assert_equal 4, versions.size
361 411 versions.each do |version|
362 412 assert_kind_of Version, version
363 413 end
364 414
365 415 assert !versions.collect(&:id).include?(6)
366 416 end
367 417
368 418
369 419 def test_next_identifier
370 420 ProjectCustomField.delete_all
371 421 Project.create!(:name => 'last', :identifier => 'p2008040')
372 422 assert_equal 'p2008041', Project.next_identifier
373 423 end
374 424
375 425 def test_next_identifier_first_project
376 426 Project.delete_all
377 427 assert_nil Project.next_identifier
378 428 end
379 429
380 430
381 431 def test_enabled_module_names_should_not_recreate_enabled_modules
382 432 project = Project.find(1)
383 433 # Remove one module
384 434 modules = project.enabled_modules.slice(0..-2)
385 435 assert modules.any?
386 436 assert_difference 'EnabledModule.count', -1 do
387 437 project.enabled_module_names = modules.collect(&:name)
388 438 end
389 439 project.reload
390 440 # Ids should be preserved
391 441 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
392 442 end
393 443
394 444 def test_copy_from_existing_project
395 445 source_project = Project.find(1)
396 446 copied_project = Project.copy_from(1)
397 447
398 448 assert copied_project
399 449 # Cleared attributes
400 450 assert copied_project.id.blank?
401 451 assert copied_project.name.blank?
402 452 assert copied_project.identifier.blank?
403 453
404 454 # Duplicated attributes
405 455 assert_equal source_project.description, copied_project.description
406 456 assert_equal source_project.enabled_modules, copied_project.enabled_modules
407 457 assert_equal source_project.trackers, copied_project.trackers
408 458
409 459 # Default attributes
410 460 assert_equal 1, copied_project.status
411 461 end
412 462
413 463 def test_activities_should_use_the_system_activities
414 464 project = Project.find(1)
415 465 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
416 466 end
417 467
418 468
419 469 def test_activities_should_use_the_project_specific_activities
420 470 project = Project.find(1)
421 471 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
422 472 assert overridden_activity.save!
423 473
424 474 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
425 475 end
426 476
427 477 def test_activities_should_not_include_the_inactive_project_specific_activities
428 478 project = Project.find(1)
429 479 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
430 480 assert overridden_activity.save!
431 481
432 482 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
433 483 end
434 484
435 485 def test_activities_should_not_include_project_specific_activities_from_other_projects
436 486 project = Project.find(1)
437 487 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
438 488 assert overridden_activity.save!
439 489
440 490 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
441 491 end
442 492
443 493 def test_activities_should_handle_nils
444 494 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
445 495 TimeEntryActivity.delete_all
446 496
447 497 # No activities
448 498 project = Project.find(1)
449 499 assert project.activities.empty?
450 500
451 501 # No system, one overridden
452 502 assert overridden_activity.save!
453 503 project.reload
454 504 assert_equal [overridden_activity], project.activities
455 505 end
456 506
457 507 def test_activities_should_override_system_activities_with_project_activities
458 508 project = Project.find(1)
459 509 parent_activity = TimeEntryActivity.find(:first)
460 510 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
461 511 assert overridden_activity.save!
462 512
463 513 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
464 514 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
465 515 end
466 516
467 517 def test_activities_should_include_inactive_activities_if_specified
468 518 project = Project.find(1)
469 519 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
470 520 assert overridden_activity.save!
471 521
472 522 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
473 523 end
474 524
475 525 def test_close_completed_versions
476 526 Version.update_all("status = 'open'")
477 527 project = Project.find(1)
478 528 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
479 529 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
480 530 project.close_completed_versions
481 531 project.reload
482 532 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
483 533 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
484 534 end
485 535
486 536 context "Project#copy" do
487 537 setup do
488 538 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
489 539 Project.destroy_all :identifier => "copy-test"
490 540 @source_project = Project.find(2)
491 541 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
492 542 @project.trackers = @source_project.trackers
493 543 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
494 544 end
495 545
496 546 should "copy issues" do
497 547 @source_project.issues << Issue.generate!(:status_id => 5,
498 548 :subject => "copy issue status",
499 549 :tracker_id => 1,
500 550 :assigned_to_id => 2,
501 551 :project_id => @source_project.id)
502 552 assert @project.valid?
503 553 assert @project.issues.empty?
504 554 assert @project.copy(@source_project)
505 555
506 556 assert_equal @source_project.issues.size, @project.issues.size
507 557 @project.issues.each do |issue|
508 558 assert issue.valid?
509 559 assert ! issue.assigned_to.blank?
510 560 assert_equal @project, issue.project
511 561 end
512 562
513 563 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
514 564 assert copied_issue
515 565 assert copied_issue.status
516 566 assert_equal "Closed", copied_issue.status.name
517 567 end
518 568
519 569 should "change the new issues to use the copied version" do
520 570 User.current = User.find(1)
521 571 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
522 572 @source_project.versions << assigned_version
523 573 assert_equal 3, @source_project.versions.size
524 574 Issue.generate_for_project!(@source_project,
525 575 :fixed_version_id => assigned_version.id,
526 576 :subject => "change the new issues to use the copied version",
527 577 :tracker_id => 1,
528 578 :project_id => @source_project.id)
529 579
530 580 assert @project.copy(@source_project)
531 581 @project.reload
532 582 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
533 583
534 584 assert copied_issue
535 585 assert copied_issue.fixed_version
536 586 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
537 587 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
538 588 end
539 589
540 590 should "copy members" do
541 591 assert @project.valid?
542 592 assert @project.members.empty?
543 593 assert @project.copy(@source_project)
544 594
545 595 assert_equal @source_project.members.size, @project.members.size
546 596 @project.members.each do |member|
547 597 assert member
548 598 assert_equal @project, member.project
549 599 end
550 600 end
551 601
552 602 should "copy project specific queries" do
553 603 assert @project.valid?
554 604 assert @project.queries.empty?
555 605 assert @project.copy(@source_project)
556 606
557 607 assert_equal @source_project.queries.size, @project.queries.size
558 608 @project.queries.each do |query|
559 609 assert query
560 610 assert_equal @project, query.project
561 611 end
562 612 end
563 613
564 614 should "copy versions" do
565 615 @source_project.versions << Version.generate!
566 616 @source_project.versions << Version.generate!
567 617
568 618 assert @project.versions.empty?
569 619 assert @project.copy(@source_project)
570 620
571 621 assert_equal @source_project.versions.size, @project.versions.size
572 622 @project.versions.each do |version|
573 623 assert version
574 624 assert_equal @project, version.project
575 625 end
576 626 end
577 627
578 628 should "copy wiki" do
579 629 assert_difference 'Wiki.count' do
580 630 assert @project.copy(@source_project)
581 631 end
582 632
583 633 assert @project.wiki
584 634 assert_not_equal @source_project.wiki, @project.wiki
585 635 assert_equal "Start page", @project.wiki.start_page
586 636 end
587 637
588 638 should "copy wiki pages and content" do
589 639 assert @project.copy(@source_project)
590 640
591 641 assert @project.wiki
592 642 assert_equal 1, @project.wiki.pages.length
593 643
594 644 @project.wiki.pages.each do |wiki_page|
595 645 assert wiki_page.content
596 646 assert !@source_project.wiki.pages.include?(wiki_page)
597 647 end
598 648 end
599 649
600 650 should "copy custom fields"
601 651
602 652 should "copy issue categories" do
603 653 assert @project.copy(@source_project)
604 654
605 655 assert_equal 2, @project.issue_categories.size
606 656 @project.issue_categories.each do |issue_category|
607 657 assert !@source_project.issue_categories.include?(issue_category)
608 658 end
609 659 end
610 660
611 661 should "copy boards" do
612 662 assert @project.copy(@source_project)
613 663
614 664 assert_equal 1, @project.boards.size
615 665 @project.boards.each do |board|
616 666 assert !@source_project.boards.include?(board)
617 667 end
618 668 end
619 669
620 670 should "change the new issues to use the copied issue categories" do
621 671 issue = Issue.find(4)
622 672 issue.update_attribute(:category_id, 3)
623 673
624 674 assert @project.copy(@source_project)
625 675
626 676 @project.issues.each do |issue|
627 677 assert issue.category
628 678 assert_equal "Stock management", issue.category.name # Same name
629 679 assert_not_equal IssueCategory.find(3), issue.category # Different record
630 680 end
631 681 end
632 682
633 683 should "limit copy with :only option" do
634 684 assert @project.members.empty?
635 685 assert @project.issue_categories.empty?
636 686 assert @source_project.issues.any?
637 687
638 688 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
639 689
640 690 assert @project.members.any?
641 691 assert @project.issue_categories.any?
642 692 assert @project.issues.empty?
643 693 end
644 694
645 695 should "copy issue relations"
646 696 should "link issue relations if cross project issue relations are valid"
647 697
648 698 end
649 699
650 700 end
General Comments 0
You need to be logged in to leave comments. Login now