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