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