comparison app/models/project.rb @ 514:7eba09d624db live

Merge
author Chris Cannam
date Thu, 14 Jul 2011 10:50:53 +0100
parents 851510f1b535
children 65abc6b39292
comparison
equal deleted inserted replaced
512:b9aebdd7dd40 514:7eba09d624db
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) }}
86 named_scope :visible_roots, lambda { { :conditions => Project.root_visible_by(User.current) } } 88 named_scope :visible_roots, lambda { { :conditions => Project.root_visible_by(User.current) } }
89
90 def initialize(attributes = nil)
91 super
92
93 initialized = (attributes || {}).stringify_keys
94 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
95 self.identifier = Project.next_identifier
96 end
97 if !initialized.key?('is_public')
98 self.is_public = Setting.default_projects_public?
99 end
100 if !initialized.key?('enabled_module_names')
101 self.enabled_module_names = Setting.default_projects_modules
102 end
103 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
104 self.trackers = Tracker.all
105 end
106 end
87 107
88 def identifier=(identifier) 108 def identifier=(identifier)
89 super unless identifier_frozen? 109 super unless identifier_frozen?
90 end 110 end
91 111
94 end 114 end
95 115
96 # returns latest created projects 116 # returns latest created projects
97 # non public projects will be returned only if user is a member of those 117 # non public projects will be returned only if user is a member of those
98 def self.latest(user=nil, count=5) 118 def self.latest(user=nil, count=5)
99 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC") 119 visible(user).find(:all, :limit => count, :order => "created_on DESC")
100 end 120 end
101 121
102 # Returns a SQL :conditions string used to find all active projects for the specified user. 122 # Returns true if the project is visible to +user+ or to the current user.
123 def visible?(user=User.current)
124 user.allowed_to?(:view_project, self)
125 end
126
127 def self.visible_by(user=nil)
128 ActiveSupport::Deprecation.warn "Project.visible_by is deprecated and will be removed in Redmine 1.3.0. Use Project.visible_condition instead."
129 visible_condition(user || User.current)
130 end
131
132 # Returns a SQL conditions string used to find all projects visible by the specified user.
103 # 133 #
104 # Examples: 134 # Examples:
105 # Projects.visible_by(admin) => "projects.status = 1" 135 # Project.visible_condition(admin) => "projects.status = 1"
106 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1" 136 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
107 def self.visible_by(user=nil) 137 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
108 user ||= User.current 138 def self.visible_condition(user, options={})
109 if user && user.admin? 139 allowed_to_condition(user, :view_project, options)
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
111 elsif user && user.memberships.any?
112 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(',')}))"
113 else
114 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
115 end
116 end 140 end
117 141
118 def self.root_visible_by(user=nil) 142 def self.root_visible_by(user=nil)
119 return "#{Project.table_name}.parent_id IS NULL AND " + visible_by(user) 143 return "#{Project.table_name}.parent_id IS NULL AND " + visible_by(user)
120 end 144 end
121 145
146 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
147 #
148 # Valid options:
149 # * :project => limit the condition to project
150 # * :with_subprojects => limit the condition to project and its subprojects
151 # * :member => limit the condition to the user projects
122 def self.allowed_to_condition(user, permission, options={}) 152 def self.allowed_to_condition(user, permission, options={})
123 statements = []
124 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" 153 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
125 if perm = Redmine::AccessControl.permission(permission) 154 if perm = Redmine::AccessControl.permission(permission)
126 unless perm.project_module.nil? 155 unless perm.project_module.nil?
127 # If the permission belongs to a project module, make sure the module is enabled 156 # If the permission belongs to a project module, make sure the module is enabled
128 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" 157 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
131 if options[:project] 160 if options[:project]
132 project_statement = "#{Project.table_name}.id = #{options[:project].id}" 161 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
133 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects] 162 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
134 base_statement = "(#{project_statement}) AND (#{base_statement})" 163 base_statement = "(#{project_statement}) AND (#{base_statement})"
135 end 164 end
165
136 if user.admin? 166 if user.admin?
137 # no restriction 167 base_statement
138 else 168 else
139 statements << "1=0" 169 statement_by_role = {}
170 unless options[:member]
171 role = user.logged? ? Role.non_member : Role.anonymous
172 if role.allowed_to?(permission)
173 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
174 end
175 end
140 if user.logged? 176 if user.logged?
141 if Role.non_member.allowed_to?(permission) && !options[:member] 177 user.projects_by_role.each do |role, projects|
142 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" 178 if role.allowed_to?(permission)
143 end 179 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
144 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id} 180 end
145 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any? 181 end
182 end
183 if statement_by_role.empty?
184 "1=0"
146 else 185 else
147 if Role.anonymous.allowed_to?(permission) && !options[:member] 186 if block_given?
148 # anonymous user allowed on public project 187 statement_by_role.each do |role, statement|
149 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" 188 if s = yield(role, user)
150 end 189 statement_by_role[role] = "(#{statement} AND (#{s}))"
151 end 190 end
152 end 191 end
153 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))" 192 end
193 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
194 end
195 end
154 end 196 end
155 197
156 # Returns the Systemwide and project specific activities 198 # Returns the Systemwide and project specific activities
157 def activities(include_inactive=false) 199 def activities(include_inactive=false)
158 if include_inactive 200 if include_inactive
330 end 372 end
331 373
332 # Returns an array of the trackers used by the project and its active sub projects 374 # Returns an array of the trackers used by the project and its active sub projects
333 def rolled_up_trackers 375 def rolled_up_trackers
334 @rolled_up_trackers ||= 376 @rolled_up_trackers ||=
335 Tracker.find(:all, :include => :projects, 377 Tracker.find(:all, :joins => :projects,
336 :select => "DISTINCT #{Tracker.table_name}.*", 378 :select => "DISTINCT #{Tracker.table_name}.*",
337 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt], 379 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
338 :order => "#{Tracker.table_name}.position") 380 :order => "#{Tracker.table_name}.position")
339 end 381 end
340 382
356 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt]) 398 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
357 end 399 end
358 400
359 # Returns a scope of the Versions used by the project 401 # Returns a scope of the Versions used by the project
360 def shared_versions 402 def shared_versions
361 @shared_versions ||= 403 @shared_versions ||= begin
404 r = root? ? self : root
362 Version.scoped(:include => :project, 405 Version.scoped(:include => :project,
363 :conditions => "#{Project.table_name}.id = #{id}" + 406 :conditions => "#{Project.table_name}.id = #{id}" +
364 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" + 407 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
365 " #{Version.table_name}.sharing = 'system'" + 408 " #{Version.table_name}.sharing = 'system'" +
366 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" + 409 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
367 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" + 410 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
368 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" + 411 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
369 "))") 412 "))")
413 end
370 end 414 end
371 415
372 # Returns a hash of project users grouped by role 416 # Returns a hash of project users grouped by role
373 def users_by_role 417 def users_by_role
374 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m| 418 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
406 # Returns an array of all custom fields enabled for project issues 450 # Returns an array of all custom fields enabled for project issues
407 # (explictly associated custom fields and custom fields enabled for all projects) 451 # (explictly associated custom fields and custom fields enabled for all projects)
408 def all_issue_custom_fields 452 def all_issue_custom_fields
409 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort 453 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
410 end 454 end
455
456 # Returns an array of all custom fields enabled for project time entries
457 # (explictly associated custom fields and custom fields enabled for all projects)
458 def all_time_entry_custom_fields
459 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
460 end
411 461
412 def project 462 def project
413 self 463 self
414 end 464 end
415 465
448 s 498 s
449 end 499 end
450 500
451 # The earliest start date of a project, based on it's issues and versions 501 # The earliest start date of a project, based on it's issues and versions
452 def start_date 502 def start_date
453 if module_enabled?(:issue_tracking) 503 [
454 [ 504 issues.minimum('start_date'),
455 issues.minimum('start_date'), 505 shared_versions.collect(&:effective_date),
456 shared_versions.collect(&:effective_date), 506 shared_versions.collect(&:start_date)
457 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')} 507 ].flatten.compact.min
458 ].flatten.compact.min
459 end
460 end 508 end
461 509
462 # The latest due date of an issue or version 510 # The latest due date of an issue or version
463 def due_date 511 def due_date
464 if module_enabled?(:issue_tracking) 512 [
465 [ 513 issues.maximum('due_date'),
466 issues.maximum('due_date'), 514 shared_versions.collect(&:effective_date),
467 shared_versions.collect(&:effective_date), 515 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
468 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')} 516 ].flatten.compact.max
469 ].flatten.compact.max
470 end
471 end 517 end
472 518
473 def overdue? 519 def overdue?
474 active? && !due_date.nil? && (due_date < Date.today) 520 active? && !due_date.nil? && (due_date < Date.today)
475 end 521 end
509 enabled_modules.detect {|m| m.name == module_name} 555 enabled_modules.detect {|m| m.name == module_name}
510 end 556 end
511 557
512 def enabled_module_names=(module_names) 558 def enabled_module_names=(module_names)
513 if module_names && module_names.is_a?(Array) 559 if module_names && module_names.is_a?(Array)
514 module_names = module_names.collect(&:to_s) 560 module_names = module_names.collect(&:to_s).reject(&:blank?)
515 # remove disabled modules 561 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
516 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
517 # add new modules
518 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
519 else 562 else
520 enabled_modules.clear 563 enabled_modules.clear
521 end 564 end
522 end 565 end
523 566
567 # Returns an array of the enabled modules names
568 def enabled_module_names
569 enabled_modules.collect(&:name)
570 end
571
572 # Enable a specific module
573 #
574 # Examples:
575 # project.enable_module!(:issue_tracking)
576 # project.enable_module!("issue_tracking")
577 def enable_module!(name)
578 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
579 end
580
581 # Disable a module if it exists
582 #
583 # Examples:
584 # project.disable_module!(:issue_tracking)
585 # project.disable_module!("issue_tracking")
586 # project.disable_module!(project.enabled_modules.first)
587 def disable_module!(target)
588 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
589 target.destroy unless target.blank?
590 end
591
592 safe_attributes 'name',
593 'description',
594 'homepage',
595 'is_public',
596 'identifier',
597 'custom_field_values',
598 'custom_fields',
599 'tracker_ids',
600 'issue_custom_field_ids'
601
602 safe_attributes 'enabled_module_names',
603 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
604
524 # Returns an array of projects that are in this project's hierarchy 605 # Returns an array of projects that are in this project's hierarchy
525 # 606 #
526 # Example: parents, children, siblings 607 # Example: parents, children, siblings
527 def hierarchy 608 def hierarchy
528 parents = project.self_and_ancestors || [] 609 parents = project.self_and_ancestors || []
604 end 685 end
605 end 686 end
606 687
607 private 688 private
608 689
609 # Destroys children before destroying self
610 def destroy_children
611 children.each do |child|
612 child.destroy
613 end
614 end
615
616 # Copies wiki from +project+ 690 # Copies wiki from +project+
617 def copy_wiki(project) 691 def copy_wiki(project)
618 # Check that the source project has a wiki first 692 # Check that the source project has a wiki first
619 unless project.wiki.nil? 693 unless project.wiki.nil?
620 self.wiki ||= Wiki.new 694 self.wiki ||= Wiki.new
657 self.issue_categories << new_issue_category 731 self.issue_categories << new_issue_category
658 end 732 end
659 end 733 end
660 734
661 # Copies issues from +project+ 735 # Copies issues from +project+
736 # Note: issues assigned to a closed version won't be copied due to validation rules
662 def copy_issues(project) 737 def copy_issues(project)
663 # Stores the source issue id as a key and the copied issues as the 738 # Stores the source issue id as a key and the copied issues as the
664 # value. Used to map the two togeather for issue relations. 739 # value. Used to map the two togeather for issue relations.
665 issues_map = {} 740 issues_map = {}
666 741
686 new_issue.parent_issue_id = copied_parent.id 761 new_issue.parent_issue_id = copied_parent.id
687 end 762 end
688 end 763 end
689 764
690 self.issues << new_issue 765 self.issues << new_issue
691 issues_map[issue.id] = new_issue 766 if new_issue.new_record?
767 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
768 else
769 issues_map[issue.id] = new_issue unless new_issue.new_record?
770 end
692 end 771 end
693 772
694 # Relations after in case issues related each other 773 # Relations after in case issues related each other
695 project.issues.each do |issue| 774 project.issues.each do |issue|
696 new_issue = issues_map[issue.id] 775 new_issue = issues_map[issue.id]
776 unless new_issue
777 # Issue was not copied
778 next
779 end
697 780
698 # Relations 781 # Relations
699 issue.relations_from.each do |source_relation| 782 issue.relations_from.each do |source_relation|
700 new_issue_relation = IssueRelation.new 783 new_issue_relation = IssueRelation.new
701 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id") 784 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
718 end 801 end
719 end 802 end
720 803
721 # Copies members from +project+ 804 # Copies members from +project+
722 def copy_members(project) 805 def copy_members(project)
723 project.memberships.each do |member| 806 # Copy users first, then groups to handle members with inherited and given roles
807 members_to_copy = []
808 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
809 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
810
811 members_to_copy.each do |member|
724 new_member = Member.new 812 new_member = Member.new
725 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on") 813 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
726 # only copy non inherited roles 814 # only copy non inherited roles
727 # inherited roles will be added when copying the group membership 815 # inherited roles will be added when copying the group membership
728 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id) 816 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)