##// END OF EJS Templates
Project#activities should check all overridden activities, not just active ones....
Eric Davis -
r3125:9fb40b1a2f70
parent child
Show More
@@ -1,672 +1,672
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_subprojects))
250 250 @allowed_parents = @allowed_parents - self_and_descendants
251 251 if User.current.allowed_to?(:add_project, nil, :global => true)
252 252 @allowed_parents << nil
253 253 end
254 254 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
255 255 @allowed_parents << parent
256 256 end
257 257 @allowed_parents
258 258 end
259 259
260 260 # Sets the parent of the project with authorization check
261 261 def set_allowed_parent!(p)
262 262 unless p.nil? || p.is_a?(Project)
263 263 if p.to_s.blank?
264 264 p = nil
265 265 else
266 266 p = Project.find_by_id(p)
267 267 return false unless p
268 268 end
269 269 end
270 270 if p.nil?
271 271 if !new_record? && allowed_parents.empty?
272 272 return false
273 273 end
274 274 elsif !allowed_parents.include?(p)
275 275 return false
276 276 end
277 277 set_parent!(p)
278 278 end
279 279
280 280 # Sets the parent of the project
281 281 # Argument can be either a Project, a String, a Fixnum or nil
282 282 def set_parent!(p)
283 283 unless p.nil? || p.is_a?(Project)
284 284 if p.to_s.blank?
285 285 p = nil
286 286 else
287 287 p = Project.find_by_id(p)
288 288 return false unless p
289 289 end
290 290 end
291 291 if p == parent && !p.nil?
292 292 # Nothing to do
293 293 true
294 294 elsif p.nil? || (p.active? && move_possible?(p))
295 295 # Insert the project so that target's children or root projects stay alphabetically sorted
296 296 sibs = (p.nil? ? self.class.roots : p.children)
297 297 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
298 298 if to_be_inserted_before
299 299 move_to_left_of(to_be_inserted_before)
300 300 elsif p.nil?
301 301 if sibs.empty?
302 302 # move_to_root adds the project in first (ie. left) position
303 303 move_to_root
304 304 else
305 305 move_to_right_of(sibs.last) unless self == sibs.last
306 306 end
307 307 else
308 308 # move_to_child_of adds the project in last (ie.right) position
309 309 move_to_child_of(p)
310 310 end
311 311 Issue.update_versions_from_hierarchy_change(self)
312 312 true
313 313 else
314 314 # Can not move to the given target
315 315 false
316 316 end
317 317 end
318 318
319 319 # Returns an array of the trackers used by the project and its active sub projects
320 320 def rolled_up_trackers
321 321 @rolled_up_trackers ||=
322 322 Tracker.find(:all, :include => :projects,
323 323 :select => "DISTINCT #{Tracker.table_name}.*",
324 324 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
325 325 :order => "#{Tracker.table_name}.position")
326 326 end
327 327
328 328 # Closes open and locked project versions that are completed
329 329 def close_completed_versions
330 330 Version.transaction do
331 331 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
332 332 if version.completed?
333 333 version.update_attribute(:status, 'closed')
334 334 end
335 335 end
336 336 end
337 337 end
338 338
339 339 # Returns a scope of the Versions used by the project
340 340 def shared_versions
341 341 @shared_versions ||=
342 342 Version.scoped(:include => :project,
343 343 :conditions => "#{Project.table_name}.id = #{id}" +
344 344 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
345 345 " #{Version.table_name}.sharing = 'system'" +
346 346 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
347 347 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
348 348 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
349 349 "))")
350 350 end
351 351
352 352 # Returns a hash of project users grouped by role
353 353 def users_by_role
354 354 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
355 355 m.roles.each do |r|
356 356 h[r] ||= []
357 357 h[r] << m.user
358 358 end
359 359 h
360 360 end
361 361 end
362 362
363 363 # Deletes all project's members
364 364 def delete_all_members
365 365 me, mr = Member.table_name, MemberRole.table_name
366 366 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
367 367 Member.delete_all(['project_id = ?', id])
368 368 end
369 369
370 370 # Users issues can be assigned to
371 371 def assignable_users
372 372 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
373 373 end
374 374
375 375 # Returns the mail adresses of users that should be always notified on project events
376 376 def recipients
377 377 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
378 378 end
379 379
380 380 # Returns the users that should be notified on project events
381 381 def notified_users
382 382 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
383 383 end
384 384
385 385 # Returns an array of all custom fields enabled for project issues
386 386 # (explictly associated custom fields and custom fields enabled for all projects)
387 387 def all_issue_custom_fields
388 388 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
389 389 end
390 390
391 391 def project
392 392 self
393 393 end
394 394
395 395 def <=>(project)
396 396 name.downcase <=> project.name.downcase
397 397 end
398 398
399 399 def to_s
400 400 name
401 401 end
402 402
403 403 # Returns a short description of the projects (first lines)
404 404 def short_description(length = 255)
405 405 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
406 406 end
407 407
408 408 # Return true if this project is allowed to do the specified action.
409 409 # action can be:
410 410 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
411 411 # * a permission Symbol (eg. :edit_project)
412 412 def allows_to?(action)
413 413 if action.is_a? Hash
414 414 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
415 415 else
416 416 allowed_permissions.include? action
417 417 end
418 418 end
419 419
420 420 def module_enabled?(module_name)
421 421 module_name = module_name.to_s
422 422 enabled_modules.detect {|m| m.name == module_name}
423 423 end
424 424
425 425 def enabled_module_names=(module_names)
426 426 if module_names && module_names.is_a?(Array)
427 427 module_names = module_names.collect(&:to_s)
428 428 # remove disabled modules
429 429 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
430 430 # add new modules
431 431 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
432 432 else
433 433 enabled_modules.clear
434 434 end
435 435 end
436 436
437 437 # Returns an auto-generated project identifier based on the last identifier used
438 438 def self.next_identifier
439 439 p = Project.find(:first, :order => 'created_on DESC')
440 440 p.nil? ? nil : p.identifier.to_s.succ
441 441 end
442 442
443 443 # Copies and saves the Project instance based on the +project+.
444 444 # Duplicates the source project's:
445 445 # * Wiki
446 446 # * Versions
447 447 # * Categories
448 448 # * Issues
449 449 # * Members
450 450 # * Queries
451 451 #
452 452 # Accepts an +options+ argument to specify what to copy
453 453 #
454 454 # Examples:
455 455 # project.copy(1) # => copies everything
456 456 # project.copy(1, :only => 'members') # => copies members only
457 457 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
458 458 def copy(project, options={})
459 459 project = project.is_a?(Project) ? project : Project.find(project)
460 460
461 461 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
462 462 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
463 463
464 464 Project.transaction do
465 465 if save
466 466 reload
467 467 to_be_copied.each do |name|
468 468 send "copy_#{name}", project
469 469 end
470 470 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
471 471 save
472 472 end
473 473 end
474 474 end
475 475
476 476
477 477 # Copies +project+ and returns the new instance. This will not save
478 478 # the copy
479 479 def self.copy_from(project)
480 480 begin
481 481 project = project.is_a?(Project) ? project : Project.find(project)
482 482 if project
483 483 # clear unique attributes
484 484 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
485 485 copy = Project.new(attributes)
486 486 copy.enabled_modules = project.enabled_modules
487 487 copy.trackers = project.trackers
488 488 copy.custom_values = project.custom_values.collect {|v| v.clone}
489 489 copy.issue_custom_fields = project.issue_custom_fields
490 490 return copy
491 491 else
492 492 return nil
493 493 end
494 494 rescue ActiveRecord::RecordNotFound
495 495 return nil
496 496 end
497 497 end
498 498
499 499 private
500 500
501 501 # Copies wiki from +project+
502 502 def copy_wiki(project)
503 503 # Check that the source project has a wiki first
504 504 unless project.wiki.nil?
505 505 self.wiki ||= Wiki.new
506 506 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
507 507 project.wiki.pages.each do |page|
508 508 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
509 509 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
510 510 new_wiki_page.content = new_wiki_content
511 511 wiki.pages << new_wiki_page
512 512 end
513 513 end
514 514 end
515 515
516 516 # Copies versions from +project+
517 517 def copy_versions(project)
518 518 project.versions.each do |version|
519 519 new_version = Version.new
520 520 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
521 521 self.versions << new_version
522 522 end
523 523 end
524 524
525 525 # Copies issue categories from +project+
526 526 def copy_issue_categories(project)
527 527 project.issue_categories.each do |issue_category|
528 528 new_issue_category = IssueCategory.new
529 529 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
530 530 self.issue_categories << new_issue_category
531 531 end
532 532 end
533 533
534 534 # Copies issues from +project+
535 535 def copy_issues(project)
536 536 # Stores the source issue id as a key and the copied issues as the
537 537 # value. Used to map the two togeather for issue relations.
538 538 issues_map = {}
539 539
540 540 project.issues.each do |issue|
541 541 new_issue = Issue.new
542 542 new_issue.copy_from(issue)
543 543 # Reassign fixed_versions by name, since names are unique per
544 544 # project and the versions for self are not yet saved
545 545 if issue.fixed_version
546 546 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
547 547 end
548 548 # Reassign the category by name, since names are unique per
549 549 # project and the categories for self are not yet saved
550 550 if issue.category
551 551 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
552 552 end
553 553 self.issues << new_issue
554 554 issues_map[issue.id] = new_issue
555 555 end
556 556
557 557 # Relations after in case issues related each other
558 558 project.issues.each do |issue|
559 559 new_issue = issues_map[issue.id]
560 560
561 561 # Relations
562 562 issue.relations_from.each do |source_relation|
563 563 new_issue_relation = IssueRelation.new
564 564 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
565 565 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
566 566 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
567 567 new_issue_relation.issue_to = source_relation.issue_to
568 568 end
569 569 new_issue.relations_from << new_issue_relation
570 570 end
571 571
572 572 issue.relations_to.each do |source_relation|
573 573 new_issue_relation = IssueRelation.new
574 574 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
575 575 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
576 576 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
577 577 new_issue_relation.issue_from = source_relation.issue_from
578 578 end
579 579 new_issue.relations_to << new_issue_relation
580 580 end
581 581 end
582 582 end
583 583
584 584 # Copies members from +project+
585 585 def copy_members(project)
586 586 project.members.each do |member|
587 587 new_member = Member.new
588 588 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
589 589 new_member.role_ids = member.role_ids.dup
590 590 new_member.project = self
591 591 self.members << new_member
592 592 end
593 593 end
594 594
595 595 # Copies queries from +project+
596 596 def copy_queries(project)
597 597 project.queries.each do |query|
598 598 new_query = Query.new
599 599 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
600 600 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
601 601 new_query.project = self
602 602 self.queries << new_query
603 603 end
604 604 end
605 605
606 606 # Copies boards from +project+
607 607 def copy_boards(project)
608 608 project.boards.each do |board|
609 609 new_board = Board.new
610 610 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
611 611 new_board.project = self
612 612 self.boards << new_board
613 613 end
614 614 end
615 615
616 616 def allowed_permissions
617 617 @allowed_permissions ||= begin
618 618 module_names = enabled_modules.collect {|m| m.name}
619 619 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
620 620 end
621 621 end
622 622
623 623 def allowed_actions
624 624 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
625 625 end
626 626
627 627 # Returns all the active Systemwide and project specific activities
628 628 def active_activities
629 overridden_activity_ids = self.time_entry_activities.active.collect(&:parent_id)
630
629 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
630
631 631 if overridden_activity_ids.empty?
632 632 return TimeEntryActivity.shared.active
633 633 else
634 634 return system_activities_and_project_overrides
635 635 end
636 636 end
637 637
638 638 # Returns all the Systemwide and project specific activities
639 639 # (inactive and active)
640 640 def all_activities
641 641 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
642 642
643 643 if overridden_activity_ids.empty?
644 644 return TimeEntryActivity.shared
645 645 else
646 646 return system_activities_and_project_overrides(true)
647 647 end
648 648 end
649 649
650 650 # Returns the systemwide active activities merged with the project specific overrides
651 651 def system_activities_and_project_overrides(include_inactive=false)
652 652 if include_inactive
653 653 return TimeEntryActivity.shared.
654 654 find(:all,
655 655 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
656 656 self.time_entry_activities
657 657 else
658 658 return TimeEntryActivity.shared.active.
659 659 find(:all,
660 :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
660 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
661 661 self.time_entry_activities.active
662 662 end
663 663 end
664 664
665 665 # Archives subprojects recursively
666 666 def archive!
667 667 children.each do |subproject|
668 668 subproject.send :archive!
669 669 end
670 670 update_attribute :status, STATUS_ARCHIVED
671 671 end
672 672 end
@@ -1,730 +1,741
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.compact.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 320
321 321 def test_shared_versions_none_sharing
322 322 p = Project.find(5)
323 323 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
324 324 assert p.shared_versions.include?(v)
325 325 assert !p.children.first.shared_versions.include?(v)
326 326 assert !p.root.shared_versions.include?(v)
327 327 assert !p.siblings.first.shared_versions.include?(v)
328 328 assert !p.root.siblings.first.shared_versions.include?(v)
329 329 end
330 330
331 331 def test_shared_versions_descendants_sharing
332 332 p = Project.find(5)
333 333 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
334 334 assert p.shared_versions.include?(v)
335 335 assert p.children.first.shared_versions.include?(v)
336 336 assert !p.root.shared_versions.include?(v)
337 337 assert !p.siblings.first.shared_versions.include?(v)
338 338 assert !p.root.siblings.first.shared_versions.include?(v)
339 339 end
340 340
341 341 def test_shared_versions_hierarchy_sharing
342 342 p = Project.find(5)
343 343 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
344 344 assert p.shared_versions.include?(v)
345 345 assert p.children.first.shared_versions.include?(v)
346 346 assert p.root.shared_versions.include?(v)
347 347 assert !p.siblings.first.shared_versions.include?(v)
348 348 assert !p.root.siblings.first.shared_versions.include?(v)
349 349 end
350 350
351 351 def test_shared_versions_tree_sharing
352 352 p = Project.find(5)
353 353 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
354 354 assert p.shared_versions.include?(v)
355 355 assert p.children.first.shared_versions.include?(v)
356 356 assert p.root.shared_versions.include?(v)
357 357 assert p.siblings.first.shared_versions.include?(v)
358 358 assert !p.root.siblings.first.shared_versions.include?(v)
359 359 end
360 360
361 361 def test_shared_versions_system_sharing
362 362 p = Project.find(5)
363 363 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
364 364 assert p.shared_versions.include?(v)
365 365 assert p.children.first.shared_versions.include?(v)
366 366 assert p.root.shared_versions.include?(v)
367 367 assert p.siblings.first.shared_versions.include?(v)
368 368 assert p.root.siblings.first.shared_versions.include?(v)
369 369 end
370 370
371 371 def test_shared_versions
372 372 parent = Project.find(1)
373 373 child = parent.children.find(3)
374 374 private_child = parent.children.find(5)
375 375
376 376 assert_equal [1,2,3], parent.version_ids.sort
377 377 assert_equal [4], child.version_ids
378 378 assert_equal [6], private_child.version_ids
379 379 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
380 380
381 381 assert_equal 6, parent.shared_versions.size
382 382 parent.shared_versions.each do |version|
383 383 assert_kind_of Version, version
384 384 end
385 385
386 386 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
387 387 end
388 388
389 389 def test_shared_versions_should_ignore_archived_subprojects
390 390 parent = Project.find(1)
391 391 child = parent.children.find(3)
392 392 child.archive
393 393 parent.reload
394 394
395 395 assert_equal [1,2,3], parent.version_ids.sort
396 396 assert_equal [4], child.version_ids
397 397 assert !parent.shared_versions.collect(&:id).include?(4)
398 398 end
399 399
400 400 def test_shared_versions_visible_to_user
401 401 user = User.find(3)
402 402 parent = Project.find(1)
403 403 child = parent.children.find(5)
404 404
405 405 assert_equal [1,2,3], parent.version_ids.sort
406 406 assert_equal [6], child.version_ids
407 407
408 408 versions = parent.shared_versions.visible(user)
409 409
410 410 assert_equal 4, versions.size
411 411 versions.each do |version|
412 412 assert_kind_of Version, version
413 413 end
414 414
415 415 assert !versions.collect(&:id).include?(6)
416 416 end
417 417
418 418
419 419 def test_next_identifier
420 420 ProjectCustomField.delete_all
421 421 Project.create!(:name => 'last', :identifier => 'p2008040')
422 422 assert_equal 'p2008041', Project.next_identifier
423 423 end
424 424
425 425 def test_next_identifier_first_project
426 426 Project.delete_all
427 427 assert_nil Project.next_identifier
428 428 end
429 429
430 430
431 431 def test_enabled_module_names_should_not_recreate_enabled_modules
432 432 project = Project.find(1)
433 433 # Remove one module
434 434 modules = project.enabled_modules.slice(0..-2)
435 435 assert modules.any?
436 436 assert_difference 'EnabledModule.count', -1 do
437 437 project.enabled_module_names = modules.collect(&:name)
438 438 end
439 439 project.reload
440 440 # Ids should be preserved
441 441 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
442 442 end
443 443
444 444 def test_copy_from_existing_project
445 445 source_project = Project.find(1)
446 446 copied_project = Project.copy_from(1)
447 447
448 448 assert copied_project
449 449 # Cleared attributes
450 450 assert copied_project.id.blank?
451 451 assert copied_project.name.blank?
452 452 assert copied_project.identifier.blank?
453 453
454 454 # Duplicated attributes
455 455 assert_equal source_project.description, copied_project.description
456 456 assert_equal source_project.enabled_modules, copied_project.enabled_modules
457 457 assert_equal source_project.trackers, copied_project.trackers
458 458
459 459 # Default attributes
460 460 assert_equal 1, copied_project.status
461 461 end
462 462
463 463 def test_activities_should_use_the_system_activities
464 464 project = Project.find(1)
465 465 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
466 466 end
467 467
468 468
469 469 def test_activities_should_use_the_project_specific_activities
470 470 project = Project.find(1)
471 471 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
472 472 assert overridden_activity.save!
473 473
474 474 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
475 475 end
476 476
477 477 def test_activities_should_not_include_the_inactive_project_specific_activities
478 478 project = Project.find(1)
479 479 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
480 480 assert overridden_activity.save!
481 481
482 482 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
483 483 end
484 484
485 485 def test_activities_should_not_include_project_specific_activities_from_other_projects
486 486 project = Project.find(1)
487 487 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
488 488 assert overridden_activity.save!
489 489
490 490 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
491 491 end
492 492
493 493 def test_activities_should_handle_nils
494 494 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
495 495 TimeEntryActivity.delete_all
496 496
497 497 # No activities
498 498 project = Project.find(1)
499 499 assert project.activities.empty?
500 500
501 501 # No system, one overridden
502 502 assert overridden_activity.save!
503 503 project.reload
504 504 assert_equal [overridden_activity], project.activities
505 505 end
506 506
507 507 def test_activities_should_override_system_activities_with_project_activities
508 508 project = Project.find(1)
509 509 parent_activity = TimeEntryActivity.find(:first)
510 510 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
511 511 assert overridden_activity.save!
512 512
513 513 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
514 514 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
515 515 end
516 516
517 517 def test_activities_should_include_inactive_activities_if_specified
518 518 project = Project.find(1)
519 519 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
520 520 assert overridden_activity.save!
521 521
522 522 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
523 523 end
524
525 test 'activities should not include active System activities if the project has an override that is inactive' do
526 project = Project.find(1)
527 system_activity = TimeEntryActivity.find_by_name('Design')
528 assert system_activity.active?
529 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
530 assert overridden_activity.save!
531
532 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
533 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
534 end
524 535
525 536 def test_close_completed_versions
526 537 Version.update_all("status = 'open'")
527 538 project = Project.find(1)
528 539 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
529 540 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
530 541 project.close_completed_versions
531 542 project.reload
532 543 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
533 544 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
534 545 end
535 546
536 547 context "Project#copy" do
537 548 setup do
538 549 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
539 550 Project.destroy_all :identifier => "copy-test"
540 551 @source_project = Project.find(2)
541 552 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
542 553 @project.trackers = @source_project.trackers
543 554 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
544 555 end
545 556
546 557 should "copy issues" do
547 558 @source_project.issues << Issue.generate!(:status_id => 5,
548 559 :subject => "copy issue status",
549 560 :tracker_id => 1,
550 561 :assigned_to_id => 2,
551 562 :project_id => @source_project.id)
552 563 assert @project.valid?
553 564 assert @project.issues.empty?
554 565 assert @project.copy(@source_project)
555 566
556 567 assert_equal @source_project.issues.size, @project.issues.size
557 568 @project.issues.each do |issue|
558 569 assert issue.valid?
559 570 assert ! issue.assigned_to.blank?
560 571 assert_equal @project, issue.project
561 572 end
562 573
563 574 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
564 575 assert copied_issue
565 576 assert copied_issue.status
566 577 assert_equal "Closed", copied_issue.status.name
567 578 end
568 579
569 580 should "change the new issues to use the copied version" do
570 581 User.current = User.find(1)
571 582 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
572 583 @source_project.versions << assigned_version
573 584 assert_equal 3, @source_project.versions.size
574 585 Issue.generate_for_project!(@source_project,
575 586 :fixed_version_id => assigned_version.id,
576 587 :subject => "change the new issues to use the copied version",
577 588 :tracker_id => 1,
578 589 :project_id => @source_project.id)
579 590
580 591 assert @project.copy(@source_project)
581 592 @project.reload
582 593 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
583 594
584 595 assert copied_issue
585 596 assert copied_issue.fixed_version
586 597 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
587 598 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
588 599 end
589 600
590 601 should "copy issue relations" do
591 602 Setting.cross_project_issue_relations = '1'
592 603
593 604 second_issue = Issue.generate!(:status_id => 5,
594 605 :subject => "copy issue relation",
595 606 :tracker_id => 1,
596 607 :assigned_to_id => 2,
597 608 :project_id => @source_project.id)
598 609 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
599 610 :issue_to => second_issue,
600 611 :relation_type => "relates")
601 612 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
602 613 :issue_to => second_issue,
603 614 :relation_type => "duplicates")
604 615
605 616 assert @project.copy(@source_project)
606 617 assert_equal @source_project.issues.count, @project.issues.count
607 618 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
608 619 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
609 620
610 621 # First issue with a relation on project
611 622 assert_equal 1, copied_issue.relations.size, "Relation not copied"
612 623 copied_relation = copied_issue.relations.first
613 624 assert_equal "relates", copied_relation.relation_type
614 625 assert_equal copied_second_issue.id, copied_relation.issue_to_id
615 626 assert_not_equal source_relation.id, copied_relation.id
616 627
617 628 # Second issue with a cross project relation
618 629 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
619 630 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
620 631 assert_equal "duplicates", copied_relation.relation_type
621 632 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
622 633 assert_not_equal source_relation_cross_project.id, copied_relation.id
623 634 end
624 635
625 636 should "copy members" do
626 637 assert @project.valid?
627 638 assert @project.members.empty?
628 639 assert @project.copy(@source_project)
629 640
630 641 assert_equal @source_project.members.size, @project.members.size
631 642 @project.members.each do |member|
632 643 assert member
633 644 assert_equal @project, member.project
634 645 end
635 646 end
636 647
637 648 should "copy project specific queries" do
638 649 assert @project.valid?
639 650 assert @project.queries.empty?
640 651 assert @project.copy(@source_project)
641 652
642 653 assert_equal @source_project.queries.size, @project.queries.size
643 654 @project.queries.each do |query|
644 655 assert query
645 656 assert_equal @project, query.project
646 657 end
647 658 end
648 659
649 660 should "copy versions" do
650 661 @source_project.versions << Version.generate!
651 662 @source_project.versions << Version.generate!
652 663
653 664 assert @project.versions.empty?
654 665 assert @project.copy(@source_project)
655 666
656 667 assert_equal @source_project.versions.size, @project.versions.size
657 668 @project.versions.each do |version|
658 669 assert version
659 670 assert_equal @project, version.project
660 671 end
661 672 end
662 673
663 674 should "copy wiki" do
664 675 assert_difference 'Wiki.count' do
665 676 assert @project.copy(@source_project)
666 677 end
667 678
668 679 assert @project.wiki
669 680 assert_not_equal @source_project.wiki, @project.wiki
670 681 assert_equal "Start page", @project.wiki.start_page
671 682 end
672 683
673 684 should "copy wiki pages and content" do
674 685 assert @project.copy(@source_project)
675 686
676 687 assert @project.wiki
677 688 assert_equal 1, @project.wiki.pages.length
678 689
679 690 @project.wiki.pages.each do |wiki_page|
680 691 assert wiki_page.content
681 692 assert !@source_project.wiki.pages.include?(wiki_page)
682 693 end
683 694 end
684 695
685 696 should "copy issue categories" do
686 697 assert @project.copy(@source_project)
687 698
688 699 assert_equal 2, @project.issue_categories.size
689 700 @project.issue_categories.each do |issue_category|
690 701 assert !@source_project.issue_categories.include?(issue_category)
691 702 end
692 703 end
693 704
694 705 should "copy boards" do
695 706 assert @project.copy(@source_project)
696 707
697 708 assert_equal 1, @project.boards.size
698 709 @project.boards.each do |board|
699 710 assert !@source_project.boards.include?(board)
700 711 end
701 712 end
702 713
703 714 should "change the new issues to use the copied issue categories" do
704 715 issue = Issue.find(4)
705 716 issue.update_attribute(:category_id, 3)
706 717
707 718 assert @project.copy(@source_project)
708 719
709 720 @project.issues.each do |issue|
710 721 assert issue.category
711 722 assert_equal "Stock management", issue.category.name # Same name
712 723 assert_not_equal IssueCategory.find(3), issue.category # Different record
713 724 end
714 725 end
715 726
716 727 should "limit copy with :only option" do
717 728 assert @project.members.empty?
718 729 assert @project.issue_categories.empty?
719 730 assert @source_project.issues.any?
720 731
721 732 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
722 733
723 734 assert @project.members.any?
724 735 assert @project.issue_categories.any?
725 736 assert @project.issues.empty?
726 737 end
727 738
728 739 end
729 740
730 741 end
General Comments 0
You need to be logged in to leave comments. Login now