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