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