comparison app/models/.svn/text-base/project.rb.svn-base @ 511:107d36338b70 live

Merge from branch "cannam"
author Chris Cannam
date Thu, 14 Jul 2011 10:43:07 +0100
parents 0c939c159af4
children
comparison
equal deleted inserted replaced
451:a9f6345cb43d 511:107d36338b70
1 # redMine - project management software 1 # Redmine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 # 3 #
4 # This program is free software; you can redistribute it and/or 4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License 5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2 6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version. 7 # of the License, or (at your option) any later version.
14 # You should have received a copy of the GNU General Public License 14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software 15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 class Project < ActiveRecord::Base 18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
20
19 # Project statuses 21 # Project statuses
20 STATUS_ACTIVE = 1 22 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9 23 STATUS_ARCHIVED = 9
22 24
23 # Maximum length for project identifiers 25 # Maximum length for project identifiers
39 has_many :issue_changes, :through => :issues, :source => :journals 41 has_many :issue_changes, :through => :issues, :source => :journals
40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" 42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
41 has_many :time_entries, :dependent => :delete_all 43 has_many :time_entries, :dependent => :delete_all
42 has_many :queries, :dependent => :delete_all 44 has_many :queries, :dependent => :delete_all
43 has_many :documents, :dependent => :destroy 45 has_many :documents, :dependent => :destroy
44 has_many :news, :dependent => :delete_all, :include => :author 46 has_many :news, :dependent => :destroy, :include => :author
45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" 47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
46 has_many :boards, :dependent => :destroy, :order => "position ASC" 48 has_many :boards, :dependent => :destroy, :order => "position ASC"
47 has_one :repository, :dependent => :destroy 49 has_one :repository, :dependent => :destroy
48 has_many :changesets, :through => :repository 50 has_many :changesets, :through => :repository
49 has_one :wiki, :dependent => :destroy 51 has_one :wiki, :dependent => :destroy
52 :class_name => 'IssueCustomField', 54 :class_name => 'IssueCustomField',
53 :order => "#{CustomField.table_name}.position", 55 :order => "#{CustomField.table_name}.position",
54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", 56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
55 :association_foreign_key => 'custom_field_id' 57 :association_foreign_key => 'custom_field_id'
56 58
57 acts_as_nested_set :order => 'name' 59 acts_as_nested_set :order => 'name', :dependent => :destroy
58 acts_as_attachable :view_permission => :view_files, 60 acts_as_attachable :view_permission => :view_files,
59 :delete_permission => :manage_files 61 :delete_permission => :manage_files
60 62
61 acts_as_customizable 63 acts_as_customizable
62 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil 64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, 65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}}, 66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
65 :author => nil 67 :author => nil
66 68
67 attr_protected :status, :enabled_module_names 69 attr_protected :status
68 70
69 validates_presence_of :name, :identifier 71 validates_presence_of :name, :identifier
70 validates_uniqueness_of :identifier 72 validates_uniqueness_of :identifier
71 validates_associated :repository, :wiki 73 validates_associated :repository, :wiki
72 validates_length_of :name, :maximum => 255 74 validates_length_of :name, :maximum => 255
75 # donwcase letters, digits, dashes but not digits only 77 # donwcase letters, digits, dashes but not digits only
76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? } 78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
77 # reserved words 79 # reserved words
78 validates_exclusion_of :identifier, :in => %w( new ) 80 validates_exclusion_of :identifier, :in => %w( new )
79 81
80 before_destroy :delete_all_members, :destroy_children 82 before_destroy :delete_all_members
81 83
82 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] } } 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] } }
83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} 85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
84 named_scope :all_public, { :conditions => { :is_public => true } } 86 named_scope :all_public, { :conditions => { :is_public => true } }
85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } } 87 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
88
89 def initialize(attributes = nil)
90 super
91
92 initialized = (attributes || {}).stringify_keys
93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
94 self.identifier = Project.next_identifier
95 end
96 if !initialized.key?('is_public')
97 self.is_public = Setting.default_projects_public?
98 end
99 if !initialized.key?('enabled_module_names')
100 self.enabled_module_names = Setting.default_projects_modules
101 end
102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
103 self.trackers = Tracker.all
104 end
105 end
86 106
87 def identifier=(identifier) 107 def identifier=(identifier)
88 super unless identifier_frozen? 108 super unless identifier_frozen?
89 end 109 end
90 110
93 end 113 end
94 114
95 # returns latest created projects 115 # returns latest created projects
96 # non public projects will be returned only if user is a member of those 116 # non public projects will be returned only if user is a member of those
97 def self.latest(user=nil, count=5) 117 def self.latest(user=nil, count=5)
98 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC") 118 visible(user).find(:all, :limit => count, :order => "created_on DESC")
99 end 119 end
100 120
101 # Returns a SQL :conditions string used to find all active projects for the specified user. 121 # Returns true if the project is visible to +user+ or to the current user.
122 def visible?(user=User.current)
123 user.allowed_to?(:view_project, self)
124 end
125
126 def self.visible_by(user=nil)
127 ActiveSupport::Deprecation.warn "Project.visible_by is deprecated and will be removed in Redmine 1.3.0. Use Project.visible_condition instead."
128 visible_condition(user || User.current)
129 end
130
131 # Returns a SQL conditions string used to find all projects visible by the specified user.
102 # 132 #
103 # Examples: 133 # Examples:
104 # Projects.visible_by(admin) => "projects.status = 1" 134 # Project.visible_condition(admin) => "projects.status = 1"
105 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1" 135 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
106 def self.visible_by(user=nil) 136 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
107 user ||= User.current 137 def self.visible_condition(user, options={})
108 if user && user.admin? 138 allowed_to_condition(user, :view_project, options)
109 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" 139 end
110 elsif user && user.memberships.any? 140
111 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(',')}))" 141 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
112 else 142 #
113 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}" 143 # Valid options:
114 end 144 # * :project => limit the condition to project
115 end 145 # * :with_subprojects => limit the condition to project and its subprojects
116 146 # * :member => limit the condition to the user projects
117 def self.allowed_to_condition(user, permission, options={}) 147 def self.allowed_to_condition(user, permission, options={})
118 statements = []
119 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" 148 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
120 if perm = Redmine::AccessControl.permission(permission) 149 if perm = Redmine::AccessControl.permission(permission)
121 unless perm.project_module.nil? 150 unless perm.project_module.nil?
122 # If the permission belongs to a project module, make sure the module is enabled 151 # If the permission belongs to a project module, make sure the module is enabled
123 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" 152 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
126 if options[:project] 155 if options[:project]
127 project_statement = "#{Project.table_name}.id = #{options[:project].id}" 156 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
128 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects] 157 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
129 base_statement = "(#{project_statement}) AND (#{base_statement})" 158 base_statement = "(#{project_statement}) AND (#{base_statement})"
130 end 159 end
160
131 if user.admin? 161 if user.admin?
132 # no restriction 162 base_statement
133 else 163 else
134 statements << "1=0" 164 statement_by_role = {}
165 unless options[:member]
166 role = user.logged? ? Role.non_member : Role.anonymous
167 if role.allowed_to?(permission)
168 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
169 end
170 end
135 if user.logged? 171 if user.logged?
136 if Role.non_member.allowed_to?(permission) && !options[:member] 172 user.projects_by_role.each do |role, projects|
137 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" 173 if role.allowed_to?(permission)
138 end 174 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
139 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id} 175 end
140 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any? 176 end
177 end
178 if statement_by_role.empty?
179 "1=0"
141 else 180 else
142 if Role.anonymous.allowed_to?(permission) && !options[:member] 181 if block_given?
143 # anonymous user allowed on public project 182 statement_by_role.each do |role, statement|
144 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" 183 if s = yield(role, user)
145 end 184 statement_by_role[role] = "(#{statement} AND (#{s}))"
146 end 185 end
147 end 186 end
148 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))" 187 end
188 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
189 end
190 end
149 end 191 end
150 192
151 # Returns the Systemwide and project specific activities 193 # Returns the Systemwide and project specific activities
152 def activities(include_inactive=false) 194 def activities(include_inactive=false)
153 if include_inactive 195 if include_inactive
325 end 367 end
326 368
327 # Returns an array of the trackers used by the project and its active sub projects 369 # Returns an array of the trackers used by the project and its active sub projects
328 def rolled_up_trackers 370 def rolled_up_trackers
329 @rolled_up_trackers ||= 371 @rolled_up_trackers ||=
330 Tracker.find(:all, :include => :projects, 372 Tracker.find(:all, :joins => :projects,
331 :select => "DISTINCT #{Tracker.table_name}.*", 373 :select => "DISTINCT #{Tracker.table_name}.*",
332 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt], 374 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
333 :order => "#{Tracker.table_name}.position") 375 :order => "#{Tracker.table_name}.position")
334 end 376 end
335 377
351 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt]) 393 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
352 end 394 end
353 395
354 # Returns a scope of the Versions used by the project 396 # Returns a scope of the Versions used by the project
355 def shared_versions 397 def shared_versions
356 @shared_versions ||= 398 @shared_versions ||= begin
399 r = root? ? self : root
357 Version.scoped(:include => :project, 400 Version.scoped(:include => :project,
358 :conditions => "#{Project.table_name}.id = #{id}" + 401 :conditions => "#{Project.table_name}.id = #{id}" +
359 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" + 402 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
360 " #{Version.table_name}.sharing = 'system'" + 403 " #{Version.table_name}.sharing = 'system'" +
361 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" + 404 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
362 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" + 405 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
363 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" + 406 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
364 "))") 407 "))")
408 end
365 end 409 end
366 410
367 # Returns a hash of project users grouped by role 411 # Returns a hash of project users grouped by role
368 def users_by_role 412 def users_by_role
369 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m| 413 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
401 # Returns an array of all custom fields enabled for project issues 445 # Returns an array of all custom fields enabled for project issues
402 # (explictly associated custom fields and custom fields enabled for all projects) 446 # (explictly associated custom fields and custom fields enabled for all projects)
403 def all_issue_custom_fields 447 def all_issue_custom_fields
404 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort 448 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
405 end 449 end
450
451 # Returns an array of all custom fields enabled for project time entries
452 # (explictly associated custom fields and custom fields enabled for all projects)
453 def all_time_entry_custom_fields
454 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
455 end
406 456
407 def project 457 def project
408 self 458 self
409 end 459 end
410 460
429 s 479 s
430 end 480 end
431 481
432 # The earliest start date of a project, based on it's issues and versions 482 # The earliest start date of a project, based on it's issues and versions
433 def start_date 483 def start_date
434 if module_enabled?(:issue_tracking) 484 [
435 [ 485 issues.minimum('start_date'),
436 issues.minimum('start_date'), 486 shared_versions.collect(&:effective_date),
437 shared_versions.collect(&:effective_date), 487 shared_versions.collect(&:start_date)
438 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')} 488 ].flatten.compact.min
439 ].flatten.compact.min
440 end
441 end 489 end
442 490
443 # The latest due date of an issue or version 491 # The latest due date of an issue or version
444 def due_date 492 def due_date
445 if module_enabled?(:issue_tracking) 493 [
446 [ 494 issues.maximum('due_date'),
447 issues.maximum('due_date'), 495 shared_versions.collect(&:effective_date),
448 shared_versions.collect(&:effective_date), 496 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
449 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')} 497 ].flatten.compact.max
450 ].flatten.compact.max
451 end
452 end 498 end
453 499
454 def overdue? 500 def overdue?
455 active? && !due_date.nil? && (due_date < Date.today) 501 active? && !due_date.nil? && (due_date < Date.today)
456 end 502 end
490 enabled_modules.detect {|m| m.name == module_name} 536 enabled_modules.detect {|m| m.name == module_name}
491 end 537 end
492 538
493 def enabled_module_names=(module_names) 539 def enabled_module_names=(module_names)
494 if module_names && module_names.is_a?(Array) 540 if module_names && module_names.is_a?(Array)
495 module_names = module_names.collect(&:to_s) 541 module_names = module_names.collect(&:to_s).reject(&:blank?)
496 # remove disabled modules 542 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
497 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
498 # add new modules
499 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
500 else 543 else
501 enabled_modules.clear 544 enabled_modules.clear
502 end 545 end
503 end 546 end
504 547
548 # Returns an array of the enabled modules names
549 def enabled_module_names
550 enabled_modules.collect(&:name)
551 end
552
553 # Enable a specific module
554 #
555 # Examples:
556 # project.enable_module!(:issue_tracking)
557 # project.enable_module!("issue_tracking")
558 def enable_module!(name)
559 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
560 end
561
562 # Disable a module if it exists
563 #
564 # Examples:
565 # project.disable_module!(:issue_tracking)
566 # project.disable_module!("issue_tracking")
567 # project.disable_module!(project.enabled_modules.first)
568 def disable_module!(target)
569 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
570 target.destroy unless target.blank?
571 end
572
573 safe_attributes 'name',
574 'description',
575 'homepage',
576 'is_public',
577 'identifier',
578 'custom_field_values',
579 'custom_fields',
580 'tracker_ids',
581 'issue_custom_field_ids'
582
583 safe_attributes 'enabled_module_names',
584 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
585
505 # Returns an array of projects that are in this project's hierarchy 586 # Returns an array of projects that are in this project's hierarchy
506 # 587 #
507 # Example: parents, children, siblings 588 # Example: parents, children, siblings
508 def hierarchy 589 def hierarchy
509 parents = project.self_and_ancestors || [] 590 parents = project.self_and_ancestors || []
585 end 666 end
586 end 667 end
587 668
588 private 669 private
589 670
590 # Destroys children before destroying self
591 def destroy_children
592 children.each do |child|
593 child.destroy
594 end
595 end
596
597 # Copies wiki from +project+ 671 # Copies wiki from +project+
598 def copy_wiki(project) 672 def copy_wiki(project)
599 # Check that the source project has a wiki first 673 # Check that the source project has a wiki first
600 unless project.wiki.nil? 674 unless project.wiki.nil?
601 self.wiki ||= Wiki.new 675 self.wiki ||= Wiki.new
638 self.issue_categories << new_issue_category 712 self.issue_categories << new_issue_category
639 end 713 end
640 end 714 end
641 715
642 # Copies issues from +project+ 716 # Copies issues from +project+
717 # Note: issues assigned to a closed version won't be copied due to validation rules
643 def copy_issues(project) 718 def copy_issues(project)
644 # Stores the source issue id as a key and the copied issues as the 719 # Stores the source issue id as a key and the copied issues as the
645 # value. Used to map the two togeather for issue relations. 720 # value. Used to map the two togeather for issue relations.
646 issues_map = {} 721 issues_map = {}
647 722
667 new_issue.parent_issue_id = copied_parent.id 742 new_issue.parent_issue_id = copied_parent.id
668 end 743 end
669 end 744 end
670 745
671 self.issues << new_issue 746 self.issues << new_issue
672 issues_map[issue.id] = new_issue 747 if new_issue.new_record?
748 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
749 else
750 issues_map[issue.id] = new_issue unless new_issue.new_record?
751 end
673 end 752 end
674 753
675 # Relations after in case issues related each other 754 # Relations after in case issues related each other
676 project.issues.each do |issue| 755 project.issues.each do |issue|
677 new_issue = issues_map[issue.id] 756 new_issue = issues_map[issue.id]
757 unless new_issue
758 # Issue was not copied
759 next
760 end
678 761
679 # Relations 762 # Relations
680 issue.relations_from.each do |source_relation| 763 issue.relations_from.each do |source_relation|
681 new_issue_relation = IssueRelation.new 764 new_issue_relation = IssueRelation.new
682 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id") 765 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
699 end 782 end
700 end 783 end
701 784
702 # Copies members from +project+ 785 # Copies members from +project+
703 def copy_members(project) 786 def copy_members(project)
704 project.memberships.each do |member| 787 # Copy users first, then groups to handle members with inherited and given roles
788 members_to_copy = []
789 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
790 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
791
792 members_to_copy.each do |member|
705 new_member = Member.new 793 new_member = Member.new
706 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on") 794 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
707 # only copy non inherited roles 795 # only copy non inherited roles
708 # inherited roles will be added when copying the group membership 796 # inherited roles will be added when copying the group membership
709 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id) 797 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)