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