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