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