##// END OF EJS Templates
Speedup remove_inherited_roles (#22850)....
Jean-Philippe Lang -
r15501:ddf249d7b055
parent child
Show More
@@ -0,0 +1,5
1 class AddIndexOnMemberRolesInheritedFrom < ActiveRecord::Migration
2 def change
3 add_index :member_roles, :inherited_from
4 end
5 end
@@ -1,80 +1,77
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
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 MemberRole < ActiveRecord::Base
18 class MemberRole < ActiveRecord::Base
19 belongs_to :member
19 belongs_to :member
20 belongs_to :role
20 belongs_to :role
21
21
22 after_destroy :remove_member_if_empty
22 after_destroy :remove_member_if_empty
23
23
24 after_create :add_role_to_group_users, :add_role_to_subprojects
24 after_create :add_role_to_group_users, :add_role_to_subprojects
25 after_destroy :remove_inherited_roles
25 after_destroy :remove_inherited_roles
26
26
27 validates_presence_of :role
27 validates_presence_of :role
28 validate :validate_role_member
28 validate :validate_role_member
29 attr_protected :id
29 attr_protected :id
30
30
31 def validate_role_member
31 def validate_role_member
32 errors.add :role_id, :invalid if role && !role.member?
32 errors.add :role_id, :invalid if role && !role.member?
33 end
33 end
34
34
35 def inherited?
35 def inherited?
36 !inherited_from.nil?
36 !inherited_from.nil?
37 end
37 end
38
38
39 # Destroys the MemberRole without destroying its Member if it doesn't have
39 # Destroys the MemberRole without destroying its Member if it doesn't have
40 # any other roles
40 # any other roles
41 def destroy_without_member_removal
41 def destroy_without_member_removal
42 @member_removal = false
42 @member_removal = false
43 destroy
43 destroy
44 end
44 end
45
45
46 private
46 private
47
47
48 def remove_member_if_empty
48 def remove_member_if_empty
49 if @member_removal != false && member.roles.empty?
49 if @member_removal != false && member.roles.empty?
50 member.destroy
50 member.destroy
51 end
51 end
52 end
52 end
53
53
54 def add_role_to_group_users
54 def add_role_to_group_users
55 if member.principal.is_a?(Group) && !inherited?
55 if member.principal.is_a?(Group) && !inherited?
56 member.principal.users.each do |user|
56 member.principal.users.each do |user|
57 user_member = Member.find_or_new(member.project_id, user.id)
57 user_member = Member.find_or_new(member.project_id, user.id)
58 user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
58 user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
59 user_member.save!
59 user_member.save!
60 end
60 end
61 end
61 end
62 end
62 end
63
63
64 def add_role_to_subprojects
64 def add_role_to_subprojects
65 member.project.children.each do |subproject|
65 member.project.children.each do |subproject|
66 if subproject.inherit_members?
66 if subproject.inherit_members?
67 child_member = Member.find_or_new(subproject.id, member.user_id)
67 child_member = Member.find_or_new(subproject.id, member.user_id)
68 child_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
68 child_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
69 child_member.save!
69 child_member.save!
70 end
70 end
71 end
71 end
72 end
72 end
73
73
74 def remove_inherited_roles
74 def remove_inherited_roles
75 MemberRole.where(:inherited_from => id).group_by(&:member).
75 MemberRole.where(:inherited_from => id).destroy_all
76 each do |member, member_roles|
77 member_roles.each(&:destroy)
78 end
79 end
76 end
80 end
77 end
@@ -1,1087 +1,1087
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
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
19 include Redmine::SafeAttributes
20 include Redmine::NestedSet::ProjectNestedSet
20 include Redmine::NestedSet::ProjectNestedSet
21
21
22 # Project statuses
22 # Project statuses
23 STATUS_ACTIVE = 1
23 STATUS_ACTIVE = 1
24 STATUS_CLOSED = 5
24 STATUS_CLOSED = 5
25 STATUS_ARCHIVED = 9
25 STATUS_ARCHIVED = 9
26
26
27 # Maximum length for project identifiers
27 # Maximum length for project identifiers
28 IDENTIFIER_MAX_LENGTH = 100
28 IDENTIFIER_MAX_LENGTH = 100
29
29
30 # Specific overridden Activities
30 # Specific overridden Activities
31 has_many :time_entry_activities
31 has_many :time_entry_activities
32 has_many :memberships, :class_name => 'Member', :inverse_of => :project
32 has_many :memberships, :class_name => 'Member', :inverse_of => :project
33 # Memberships of active users only
33 # Memberships of active users only
34 has_many :members,
34 has_many :members,
35 lambda { joins(:principal).where(:users => {:type => 'User', :status => Principal::STATUS_ACTIVE}) }
35 lambda { joins(:principal).where(:users => {:type => 'User', :status => Principal::STATUS_ACTIVE}) }
36 has_many :enabled_modules, :dependent => :delete_all
36 has_many :enabled_modules, :dependent => :delete_all
37 has_and_belongs_to_many :trackers, lambda {order(:position)}
37 has_and_belongs_to_many :trackers, lambda {order(:position)}
38 has_many :issues, :dependent => :destroy
38 has_many :issues, :dependent => :destroy
39 has_many :issue_changes, :through => :issues, :source => :journals
39 has_many :issue_changes, :through => :issues, :source => :journals
40 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
40 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
41 belongs_to :default_version, :class_name => 'Version'
41 belongs_to :default_version, :class_name => 'Version'
42 has_many :time_entries, :dependent => :destroy
42 has_many :time_entries, :dependent => :destroy
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
44 has_many :documents, :dependent => :destroy
44 has_many :documents, :dependent => :destroy
45 has_many :news, lambda {includes(:author)}, :dependent => :destroy
45 has_many :news, lambda {includes(:author)}, :dependent => :destroy
46 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
46 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
47 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
47 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
48 has_one :repository, lambda {where(["is_default = ?", true])}
48 has_one :repository, lambda {where(["is_default = ?", true])}
49 has_many :repositories, :dependent => :destroy
49 has_many :repositories, :dependent => :destroy
50 has_many :changesets, :through => :repository
50 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy
51 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues
52 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields,
53 has_and_belongs_to_many :issue_custom_fields,
54 lambda {order("#{CustomField.table_name}.position")},
54 lambda {order("#{CustomField.table_name}.position")},
55 :class_name => 'IssueCustomField',
55 :class_name => 'IssueCustomField',
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :association_foreign_key => 'custom_field_id'
57 :association_foreign_key => 'custom_field_id'
58
58
59 acts_as_attachable :view_permission => :view_files,
59 acts_as_attachable :view_permission => :view_files,
60 :edit_permission => :manage_files,
60 :edit_permission => :manage_files,
61 :delete_permission => :manage_files
61 :delete_permission => :manage_files
62
62
63 acts_as_customizable
63 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => "#{Project.table_name}.id", :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => "#{Project.table_name}.id", :permission => nil
65 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}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
67 :author => nil
68
68
69 attr_protected :status
69 attr_protected :status
70
70
71 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier, :if => Proc.new {|p| p.identifier_changed?}
72 validates_uniqueness_of :identifier, :if => Proc.new {|p| p.identifier_changed?}
73 validates_length_of :name, :maximum => 255
73 validates_length_of :name, :maximum => 255
74 validates_length_of :homepage, :maximum => 255
74 validates_length_of :homepage, :maximum => 255
75 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
75 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 # downcase letters, digits, dashes but not digits only
76 # downcase letters, digits, dashes but not digits only
77 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
77 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
78 # reserved words
78 # reserved words
79 validates_exclusion_of :identifier, :in => %w( new )
79 validates_exclusion_of :identifier, :in => %w( new )
80 validate :validate_parent
80 validate :validate_parent
81
81
82 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
82 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
83 after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?}
83 after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?}
84 after_update :update_versions_from_hierarchy_change, :if => Proc.new {|project| project.parent_id_changed?}
84 after_update :update_versions_from_hierarchy_change, :if => Proc.new {|project| project.parent_id_changed?}
85 before_destroy :delete_all_members
85 before_destroy :delete_all_members
86
86
87 scope :has_module, lambda {|mod|
87 scope :has_module, lambda {|mod|
88 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
88 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
89 }
89 }
90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
92 scope :all_public, lambda { where(:is_public => true) }
92 scope :all_public, lambda { where(:is_public => true) }
93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
94 scope :allowed_to, lambda {|*args|
94 scope :allowed_to, lambda {|*args|
95 user = User.current
95 user = User.current
96 permission = nil
96 permission = nil
97 if args.first.is_a?(Symbol)
97 if args.first.is_a?(Symbol)
98 permission = args.shift
98 permission = args.shift
99 else
99 else
100 user = args.shift
100 user = args.shift
101 permission = args.shift
101 permission = args.shift
102 end
102 end
103 where(Project.allowed_to_condition(user, permission, *args))
103 where(Project.allowed_to_condition(user, permission, *args))
104 }
104 }
105 scope :like, lambda {|arg|
105 scope :like, lambda {|arg|
106 if arg.blank?
106 if arg.blank?
107 where(nil)
107 where(nil)
108 else
108 else
109 pattern = "%#{arg.to_s.strip.downcase}%"
109 pattern = "%#{arg.to_s.strip.downcase}%"
110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
111 end
111 end
112 }
112 }
113 scope :sorted, lambda {order(:lft)}
113 scope :sorted, lambda {order(:lft)}
114 scope :having_trackers, lambda {
114 scope :having_trackers, lambda {
115 where("#{Project.table_name}.id IN (SELECT DISTINCT project_id FROM #{table_name_prefix}projects_trackers#{table_name_suffix})")
115 where("#{Project.table_name}.id IN (SELECT DISTINCT project_id FROM #{table_name_prefix}projects_trackers#{table_name_suffix})")
116 }
116 }
117
117
118 def initialize(attributes=nil, *args)
118 def initialize(attributes=nil, *args)
119 super
119 super
120
120
121 initialized = (attributes || {}).stringify_keys
121 initialized = (attributes || {}).stringify_keys
122 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
122 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
123 self.identifier = Project.next_identifier
123 self.identifier = Project.next_identifier
124 end
124 end
125 if !initialized.key?('is_public')
125 if !initialized.key?('is_public')
126 self.is_public = Setting.default_projects_public?
126 self.is_public = Setting.default_projects_public?
127 end
127 end
128 if !initialized.key?('enabled_module_names')
128 if !initialized.key?('enabled_module_names')
129 self.enabled_module_names = Setting.default_projects_modules
129 self.enabled_module_names = Setting.default_projects_modules
130 end
130 end
131 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
131 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
132 default = Setting.default_projects_tracker_ids
132 default = Setting.default_projects_tracker_ids
133 if default.is_a?(Array)
133 if default.is_a?(Array)
134 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
134 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
135 else
135 else
136 self.trackers = Tracker.sorted.to_a
136 self.trackers = Tracker.sorted.to_a
137 end
137 end
138 end
138 end
139 end
139 end
140
140
141 def identifier=(identifier)
141 def identifier=(identifier)
142 super unless identifier_frozen?
142 super unless identifier_frozen?
143 end
143 end
144
144
145 def identifier_frozen?
145 def identifier_frozen?
146 errors[:identifier].blank? && !(new_record? || identifier.blank?)
146 errors[:identifier].blank? && !(new_record? || identifier.blank?)
147 end
147 end
148
148
149 # returns latest created projects
149 # returns latest created projects
150 # non public projects will be returned only if user is a member of those
150 # non public projects will be returned only if user is a member of those
151 def self.latest(user=nil, count=5)
151 def self.latest(user=nil, count=5)
152 visible(user).limit(count).
152 visible(user).limit(count).
153 order(:created_on => :desc).
153 order(:created_on => :desc).
154 where("#{table_name}.created_on >= ?", 30.days.ago).
154 where("#{table_name}.created_on >= ?", 30.days.ago).
155 to_a
155 to_a
156 end
156 end
157
157
158 # Returns true if the project is visible to +user+ or to the current user.
158 # Returns true if the project is visible to +user+ or to the current user.
159 def visible?(user=User.current)
159 def visible?(user=User.current)
160 user.allowed_to?(:view_project, self)
160 user.allowed_to?(:view_project, self)
161 end
161 end
162
162
163 # Returns a SQL conditions string used to find all projects visible by the specified user.
163 # Returns a SQL conditions string used to find all projects visible by the specified user.
164 #
164 #
165 # Examples:
165 # Examples:
166 # Project.visible_condition(admin) => "projects.status = 1"
166 # Project.visible_condition(admin) => "projects.status = 1"
167 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
167 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
168 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
168 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
169 def self.visible_condition(user, options={})
169 def self.visible_condition(user, options={})
170 allowed_to_condition(user, :view_project, options)
170 allowed_to_condition(user, :view_project, options)
171 end
171 end
172
172
173 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
173 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
174 #
174 #
175 # Valid options:
175 # Valid options:
176 # * :project => limit the condition to project
176 # * :project => limit the condition to project
177 # * :with_subprojects => limit the condition to project and its subprojects
177 # * :with_subprojects => limit the condition to project and its subprojects
178 # * :member => limit the condition to the user projects
178 # * :member => limit the condition to the user projects
179 def self.allowed_to_condition(user, permission, options={})
179 def self.allowed_to_condition(user, permission, options={})
180 perm = Redmine::AccessControl.permission(permission)
180 perm = Redmine::AccessControl.permission(permission)
181 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
181 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
182 if perm && perm.project_module
182 if perm && perm.project_module
183 # If the permission belongs to a project module, make sure the module is enabled
183 # If the permission belongs to a project module, make sure the module is enabled
184 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
184 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
185 end
185 end
186 if project = options[:project]
186 if project = options[:project]
187 project_statement = project.project_condition(options[:with_subprojects])
187 project_statement = project.project_condition(options[:with_subprojects])
188 base_statement = "(#{project_statement}) AND (#{base_statement})"
188 base_statement = "(#{project_statement}) AND (#{base_statement})"
189 end
189 end
190
190
191 if user.admin?
191 if user.admin?
192 base_statement
192 base_statement
193 else
193 else
194 statement_by_role = {}
194 statement_by_role = {}
195 unless options[:member]
195 unless options[:member]
196 role = user.builtin_role
196 role = user.builtin_role
197 if role.allowed_to?(permission)
197 if role.allowed_to?(permission)
198 s = "#{Project.table_name}.is_public = #{connection.quoted_true}"
198 s = "#{Project.table_name}.is_public = #{connection.quoted_true}"
199 if user.id
199 if user.id
200 group = role.anonymous? ? Group.anonymous : Group.non_member
200 group = role.anonymous? ? Group.anonymous : Group.non_member
201 principal_ids = [user.id, group.id].compact
201 principal_ids = [user.id, group.id].compact
202 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id IN (#{principal_ids.join(',')})))"
202 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id IN (#{principal_ids.join(',')})))"
203 end
203 end
204 statement_by_role[role] = s
204 statement_by_role[role] = s
205 end
205 end
206 end
206 end
207 user.projects_by_role.each do |role, projects|
207 user.projects_by_role.each do |role, projects|
208 if role.allowed_to?(permission) && projects.any?
208 if role.allowed_to?(permission) && projects.any?
209 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
209 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
210 end
210 end
211 end
211 end
212 if statement_by_role.empty?
212 if statement_by_role.empty?
213 "1=0"
213 "1=0"
214 else
214 else
215 if block_given?
215 if block_given?
216 statement_by_role.each do |role, statement|
216 statement_by_role.each do |role, statement|
217 if s = yield(role, user)
217 if s = yield(role, user)
218 statement_by_role[role] = "(#{statement} AND (#{s}))"
218 statement_by_role[role] = "(#{statement} AND (#{s}))"
219 end
219 end
220 end
220 end
221 end
221 end
222 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
222 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
223 end
223 end
224 end
224 end
225 end
225 end
226
226
227 def override_roles(role)
227 def override_roles(role)
228 @override_members ||= memberships.
228 @override_members ||= memberships.
229 joins(:principal).
229 joins(:principal).
230 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
230 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
231
231
232 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
232 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
233 member = @override_members.detect {|m| m.principal.is_a? group_class}
233 member = @override_members.detect {|m| m.principal.is_a? group_class}
234 member ? member.roles.to_a : [role]
234 member ? member.roles.to_a : [role]
235 end
235 end
236
236
237 def principals
237 def principals
238 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).distinct
238 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).distinct
239 end
239 end
240
240
241 def users
241 def users
242 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).distinct
242 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).distinct
243 end
243 end
244
244
245 # Returns the Systemwide and project specific activities
245 # Returns the Systemwide and project specific activities
246 def activities(include_inactive=false)
246 def activities(include_inactive=false)
247 t = TimeEntryActivity.table_name
247 t = TimeEntryActivity.table_name
248 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
248 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
249
249
250 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
250 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
251 if overridden_activity_ids.any?
251 if overridden_activity_ids.any?
252 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
252 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
253 end
253 end
254 unless include_inactive
254 unless include_inactive
255 scope = scope.active
255 scope = scope.active
256 end
256 end
257 scope
257 scope
258 end
258 end
259
259
260 # Will create a new Project specific Activity or update an existing one
260 # Will create a new Project specific Activity or update an existing one
261 #
261 #
262 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
262 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
263 # does not successfully save.
263 # does not successfully save.
264 def update_or_create_time_entry_activity(id, activity_hash)
264 def update_or_create_time_entry_activity(id, activity_hash)
265 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
265 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
266 self.create_time_entry_activity_if_needed(activity_hash)
266 self.create_time_entry_activity_if_needed(activity_hash)
267 else
267 else
268 activity = project.time_entry_activities.find_by_id(id.to_i)
268 activity = project.time_entry_activities.find_by_id(id.to_i)
269 activity.update_attributes(activity_hash) if activity
269 activity.update_attributes(activity_hash) if activity
270 end
270 end
271 end
271 end
272
272
273 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
273 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
274 #
274 #
275 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
275 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
276 # does not successfully save.
276 # does not successfully save.
277 def create_time_entry_activity_if_needed(activity)
277 def create_time_entry_activity_if_needed(activity)
278 if activity['parent_id']
278 if activity['parent_id']
279 parent_activity = TimeEntryActivity.find(activity['parent_id'])
279 parent_activity = TimeEntryActivity.find(activity['parent_id'])
280 activity['name'] = parent_activity.name
280 activity['name'] = parent_activity.name
281 activity['position'] = parent_activity.position
281 activity['position'] = parent_activity.position
282 if Enumeration.overriding_change?(activity, parent_activity)
282 if Enumeration.overriding_change?(activity, parent_activity)
283 project_activity = self.time_entry_activities.create(activity)
283 project_activity = self.time_entry_activities.create(activity)
284 if project_activity.new_record?
284 if project_activity.new_record?
285 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
285 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
286 else
286 else
287 self.time_entries.
287 self.time_entries.
288 where(:activity_id => parent_activity.id).
288 where(:activity_id => parent_activity.id).
289 update_all(:activity_id => project_activity.id)
289 update_all(:activity_id => project_activity.id)
290 end
290 end
291 end
291 end
292 end
292 end
293 end
293 end
294
294
295 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
295 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
296 #
296 #
297 # Examples:
297 # Examples:
298 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
298 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
299 # project.project_condition(false) => "projects.id = 1"
299 # project.project_condition(false) => "projects.id = 1"
300 def project_condition(with_subprojects)
300 def project_condition(with_subprojects)
301 cond = "#{Project.table_name}.id = #{id}"
301 cond = "#{Project.table_name}.id = #{id}"
302 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
302 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
303 cond
303 cond
304 end
304 end
305
305
306 def self.find(*args)
306 def self.find(*args)
307 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
307 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
308 project = find_by_identifier(*args)
308 project = find_by_identifier(*args)
309 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
309 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
310 project
310 project
311 else
311 else
312 super
312 super
313 end
313 end
314 end
314 end
315
315
316 def self.find_by_param(*args)
316 def self.find_by_param(*args)
317 self.find(*args)
317 self.find(*args)
318 end
318 end
319
319
320 alias :base_reload :reload
320 alias :base_reload :reload
321 def reload(*args)
321 def reload(*args)
322 @principals = nil
322 @principals = nil
323 @users = nil
323 @users = nil
324 @shared_versions = nil
324 @shared_versions = nil
325 @rolled_up_versions = nil
325 @rolled_up_versions = nil
326 @rolled_up_trackers = nil
326 @rolled_up_trackers = nil
327 @all_issue_custom_fields = nil
327 @all_issue_custom_fields = nil
328 @all_time_entry_custom_fields = nil
328 @all_time_entry_custom_fields = nil
329 @to_param = nil
329 @to_param = nil
330 @allowed_parents = nil
330 @allowed_parents = nil
331 @allowed_permissions = nil
331 @allowed_permissions = nil
332 @actions_allowed = nil
332 @actions_allowed = nil
333 @start_date = nil
333 @start_date = nil
334 @due_date = nil
334 @due_date = nil
335 @override_members = nil
335 @override_members = nil
336 @assignable_users = nil
336 @assignable_users = nil
337 base_reload(*args)
337 base_reload(*args)
338 end
338 end
339
339
340 def to_param
340 def to_param
341 if new_record?
341 if new_record?
342 nil
342 nil
343 else
343 else
344 # id is used for projects with a numeric identifier (compatibility)
344 # id is used for projects with a numeric identifier (compatibility)
345 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
345 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
346 end
346 end
347 end
347 end
348
348
349 def active?
349 def active?
350 self.status == STATUS_ACTIVE
350 self.status == STATUS_ACTIVE
351 end
351 end
352
352
353 def archived?
353 def archived?
354 self.status == STATUS_ARCHIVED
354 self.status == STATUS_ARCHIVED
355 end
355 end
356
356
357 # Archives the project and its descendants
357 # Archives the project and its descendants
358 def archive
358 def archive
359 # Check that there is no issue of a non descendant project that is assigned
359 # Check that there is no issue of a non descendant project that is assigned
360 # to one of the project or descendant versions
360 # to one of the project or descendant versions
361 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
361 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
362
362
363 if version_ids.any? &&
363 if version_ids.any? &&
364 Issue.
364 Issue.
365 includes(:project).
365 includes(:project).
366 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
366 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
367 where(:fixed_version_id => version_ids).
367 where(:fixed_version_id => version_ids).
368 exists?
368 exists?
369 return false
369 return false
370 end
370 end
371 Project.transaction do
371 Project.transaction do
372 archive!
372 archive!
373 end
373 end
374 true
374 true
375 end
375 end
376
376
377 # Unarchives the project
377 # Unarchives the project
378 # All its ancestors must be active
378 # All its ancestors must be active
379 def unarchive
379 def unarchive
380 return false if ancestors.detect {|a| !a.active?}
380 return false if ancestors.detect {|a| !a.active?}
381 update_attribute :status, STATUS_ACTIVE
381 update_attribute :status, STATUS_ACTIVE
382 end
382 end
383
383
384 def close
384 def close
385 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
385 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
386 end
386 end
387
387
388 def reopen
388 def reopen
389 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
389 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
390 end
390 end
391
391
392 # Returns an array of projects the project can be moved to
392 # Returns an array of projects the project can be moved to
393 # by the current user
393 # by the current user
394 def allowed_parents(user=User.current)
394 def allowed_parents(user=User.current)
395 return @allowed_parents if @allowed_parents
395 return @allowed_parents if @allowed_parents
396 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
396 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
397 @allowed_parents = @allowed_parents - self_and_descendants
397 @allowed_parents = @allowed_parents - self_and_descendants
398 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
398 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
399 @allowed_parents << nil
399 @allowed_parents << nil
400 end
400 end
401 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
401 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
402 @allowed_parents << parent
402 @allowed_parents << parent
403 end
403 end
404 @allowed_parents
404 @allowed_parents
405 end
405 end
406
406
407 # Sets the parent of the project with authorization check
407 # Sets the parent of the project with authorization check
408 def set_allowed_parent!(p)
408 def set_allowed_parent!(p)
409 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
409 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
410 p = p.id if p.is_a?(Project)
410 p = p.id if p.is_a?(Project)
411 send :safe_attributes, {:project_id => p}
411 send :safe_attributes, {:project_id => p}
412 save
412 save
413 end
413 end
414
414
415 # Sets the parent of the project and saves the project
415 # Sets the parent of the project and saves the project
416 # Argument can be either a Project, a String, a Fixnum or nil
416 # Argument can be either a Project, a String, a Fixnum or nil
417 def set_parent!(p)
417 def set_parent!(p)
418 if p.is_a?(Project)
418 if p.is_a?(Project)
419 self.parent = p
419 self.parent = p
420 else
420 else
421 self.parent_id = p
421 self.parent_id = p
422 end
422 end
423 save
423 save
424 end
424 end
425
425
426 # Returns a scope of the trackers used by the project and its active sub projects
426 # Returns a scope of the trackers used by the project and its active sub projects
427 def rolled_up_trackers(include_subprojects=true)
427 def rolled_up_trackers(include_subprojects=true)
428 if include_subprojects
428 if include_subprojects
429 @rolled_up_trackers ||= rolled_up_trackers_base_scope.
429 @rolled_up_trackers ||= rolled_up_trackers_base_scope.
430 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt)
430 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt)
431 else
431 else
432 rolled_up_trackers_base_scope.
432 rolled_up_trackers_base_scope.
433 where(:projects => {:id => id})
433 where(:projects => {:id => id})
434 end
434 end
435 end
435 end
436
436
437 def rolled_up_trackers_base_scope
437 def rolled_up_trackers_base_scope
438 Tracker.
438 Tracker.
439 joins(projects: :enabled_modules).
439 joins(projects: :enabled_modules).
440 where("#{Project.table_name}.status <> ?", STATUS_ARCHIVED).
440 where("#{Project.table_name}.status <> ?", STATUS_ARCHIVED).
441 where(:enabled_modules => {:name => 'issue_tracking'}).
441 where(:enabled_modules => {:name => 'issue_tracking'}).
442 distinct.
442 distinct.
443 sorted
443 sorted
444 end
444 end
445
445
446 # Closes open and locked project versions that are completed
446 # Closes open and locked project versions that are completed
447 def close_completed_versions
447 def close_completed_versions
448 Version.transaction do
448 Version.transaction do
449 versions.where(:status => %w(open locked)).each do |version|
449 versions.where(:status => %w(open locked)).each do |version|
450 if version.completed?
450 if version.completed?
451 version.update_attribute(:status, 'closed')
451 version.update_attribute(:status, 'closed')
452 end
452 end
453 end
453 end
454 end
454 end
455 end
455 end
456
456
457 # Returns a scope of the Versions on subprojects
457 # Returns a scope of the Versions on subprojects
458 def rolled_up_versions
458 def rolled_up_versions
459 @rolled_up_versions ||=
459 @rolled_up_versions ||=
460 Version.
460 Version.
461 joins(:project).
461 joins(:project).
462 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
462 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
463 end
463 end
464
464
465 # Returns a scope of the Versions used by the project
465 # Returns a scope of the Versions used by the project
466 def shared_versions
466 def shared_versions
467 if new_record?
467 if new_record?
468 Version.
468 Version.
469 joins(:project).
469 joins(:project).
470 preload(:project).
470 preload(:project).
471 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
471 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
472 else
472 else
473 @shared_versions ||= begin
473 @shared_versions ||= begin
474 r = root? ? self : root
474 r = root? ? self : root
475 Version.
475 Version.
476 joins(:project).
476 joins(:project).
477 preload(:project).
477 preload(:project).
478 where("#{Project.table_name}.id = #{id}" +
478 where("#{Project.table_name}.id = #{id}" +
479 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
479 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
480 " #{Version.table_name}.sharing = 'system'" +
480 " #{Version.table_name}.sharing = 'system'" +
481 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
481 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
482 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
482 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
483 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
483 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
484 "))")
484 "))")
485 end
485 end
486 end
486 end
487 end
487 end
488
488
489 # Returns a hash of project users grouped by role
489 # Returns a hash of project users grouped by role
490 def users_by_role
490 def users_by_role
491 members.includes(:user, :roles).inject({}) do |h, m|
491 members.includes(:user, :roles).inject({}) do |h, m|
492 m.roles.each do |r|
492 m.roles.each do |r|
493 h[r] ||= []
493 h[r] ||= []
494 h[r] << m.user
494 h[r] << m.user
495 end
495 end
496 h
496 h
497 end
497 end
498 end
498 end
499
499
500 # Adds user as a project member with the default role
500 # Adds user as a project member with the default role
501 # Used for when a non-admin user creates a project
501 # Used for when a non-admin user creates a project
502 def add_default_member(user)
502 def add_default_member(user)
503 role = self.class.default_member_role
503 role = self.class.default_member_role
504 member = Member.new(:project => self, :principal => user, :roles => [role])
504 member = Member.new(:project => self, :principal => user, :roles => [role])
505 self.members << member
505 self.members << member
506 member
506 member
507 end
507 end
508
508
509 # Default role that is given to non-admin users that
509 # Default role that is given to non-admin users that
510 # create a project
510 # create a project
511 def self.default_member_role
511 def self.default_member_role
512 Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
512 Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
513 end
513 end
514
514
515 # Deletes all project's members
515 # Deletes all project's members
516 def delete_all_members
516 def delete_all_members
517 me, mr = Member.table_name, MemberRole.table_name
517 me, mr = Member.table_name, MemberRole.table_name
518 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
518 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
519 Member.where(:project_id => id).delete_all
519 Member.where(:project_id => id).delete_all
520 end
520 end
521
521
522 # Return a Principal scope of users/groups issues can be assigned to
522 # Return a Principal scope of users/groups issues can be assigned to
523 def assignable_users(tracker=nil)
523 def assignable_users(tracker=nil)
524 return @assignable_users[tracker] if @assignable_users && @assignable_users[tracker]
524 return @assignable_users[tracker] if @assignable_users && @assignable_users[tracker]
525
525
526 types = ['User']
526 types = ['User']
527 types << 'Group' if Setting.issue_group_assignment?
527 types << 'Group' if Setting.issue_group_assignment?
528
528
529 scope = Principal.
529 scope = Principal.
530 active.
530 active.
531 joins(:members => :roles).
531 joins(:members => :roles).
532 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
532 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
533 distinct.
533 distinct.
534 sorted
534 sorted
535
535
536 if tracker
536 if tracker
537 # Rejects users that cannot the view the tracker
537 # Rejects users that cannot the view the tracker
538 roles = Role.where(:assignable => true).select {|role| role.permissions_tracker?(:view_issues, tracker)}
538 roles = Role.where(:assignable => true).select {|role| role.permissions_tracker?(:view_issues, tracker)}
539 scope = scope.where(:roles => {:id => roles.map(&:id)})
539 scope = scope.where(:roles => {:id => roles.map(&:id)})
540 end
540 end
541
541
542 @assignable_users ||= {}
542 @assignable_users ||= {}
543 @assignable_users[tracker] = scope
543 @assignable_users[tracker] = scope
544 end
544 end
545
545
546 # Returns the mail addresses of users that should be always notified on project events
546 # Returns the mail addresses of users that should be always notified on project events
547 def recipients
547 def recipients
548 notified_users.collect {|user| user.mail}
548 notified_users.collect {|user| user.mail}
549 end
549 end
550
550
551 # Returns the users that should be notified on project events
551 # Returns the users that should be notified on project events
552 def notified_users
552 def notified_users
553 # TODO: User part should be extracted to User#notify_about?
553 # TODO: User part should be extracted to User#notify_about?
554 members.preload(:principal).select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
554 members.preload(:principal).select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
555 end
555 end
556
556
557 # Returns a scope of all custom fields enabled for project issues
557 # Returns a scope of all custom fields enabled for project issues
558 # (explicitly associated custom fields and custom fields enabled for all projects)
558 # (explicitly associated custom fields and custom fields enabled for all projects)
559 def all_issue_custom_fields
559 def all_issue_custom_fields
560 if new_record?
560 if new_record?
561 @all_issue_custom_fields ||= IssueCustomField.
561 @all_issue_custom_fields ||= IssueCustomField.
562 sorted.
562 sorted.
563 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
563 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
564 else
564 else
565 @all_issue_custom_fields ||= IssueCustomField.
565 @all_issue_custom_fields ||= IssueCustomField.
566 sorted.
566 sorted.
567 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
567 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
568 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
568 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
569 " WHERE cfp.project_id = ?)", true, id)
569 " WHERE cfp.project_id = ?)", true, id)
570 end
570 end
571 end
571 end
572
572
573 def project
573 def project
574 self
574 self
575 end
575 end
576
576
577 def <=>(project)
577 def <=>(project)
578 name.casecmp(project.name)
578 name.casecmp(project.name)
579 end
579 end
580
580
581 def to_s
581 def to_s
582 name
582 name
583 end
583 end
584
584
585 # Returns a short description of the projects (first lines)
585 # Returns a short description of the projects (first lines)
586 def short_description(length = 255)
586 def short_description(length = 255)
587 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
587 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
588 end
588 end
589
589
590 def css_classes
590 def css_classes
591 s = 'project'
591 s = 'project'
592 s << ' root' if root?
592 s << ' root' if root?
593 s << ' child' if child?
593 s << ' child' if child?
594 s << (leaf? ? ' leaf' : ' parent')
594 s << (leaf? ? ' leaf' : ' parent')
595 unless active?
595 unless active?
596 if archived?
596 if archived?
597 s << ' archived'
597 s << ' archived'
598 else
598 else
599 s << ' closed'
599 s << ' closed'
600 end
600 end
601 end
601 end
602 s
602 s
603 end
603 end
604
604
605 # The earliest start date of a project, based on it's issues and versions
605 # The earliest start date of a project, based on it's issues and versions
606 def start_date
606 def start_date
607 @start_date ||= [
607 @start_date ||= [
608 issues.minimum('start_date'),
608 issues.minimum('start_date'),
609 shared_versions.minimum('effective_date'),
609 shared_versions.minimum('effective_date'),
610 Issue.fixed_version(shared_versions).minimum('start_date')
610 Issue.fixed_version(shared_versions).minimum('start_date')
611 ].compact.min
611 ].compact.min
612 end
612 end
613
613
614 # The latest due date of an issue or version
614 # The latest due date of an issue or version
615 def due_date
615 def due_date
616 @due_date ||= [
616 @due_date ||= [
617 issues.maximum('due_date'),
617 issues.maximum('due_date'),
618 shared_versions.maximum('effective_date'),
618 shared_versions.maximum('effective_date'),
619 Issue.fixed_version(shared_versions).maximum('due_date')
619 Issue.fixed_version(shared_versions).maximum('due_date')
620 ].compact.max
620 ].compact.max
621 end
621 end
622
622
623 def overdue?
623 def overdue?
624 active? && !due_date.nil? && (due_date < User.current.today)
624 active? && !due_date.nil? && (due_date < User.current.today)
625 end
625 end
626
626
627 # Returns the percent completed for this project, based on the
627 # Returns the percent completed for this project, based on the
628 # progress on it's versions.
628 # progress on it's versions.
629 def completed_percent(options={:include_subprojects => false})
629 def completed_percent(options={:include_subprojects => false})
630 if options.delete(:include_subprojects)
630 if options.delete(:include_subprojects)
631 total = self_and_descendants.collect(&:completed_percent).sum
631 total = self_and_descendants.collect(&:completed_percent).sum
632
632
633 total / self_and_descendants.count
633 total / self_and_descendants.count
634 else
634 else
635 if versions.count > 0
635 if versions.count > 0
636 total = versions.collect(&:completed_percent).sum
636 total = versions.collect(&:completed_percent).sum
637
637
638 total / versions.count
638 total / versions.count
639 else
639 else
640 100
640 100
641 end
641 end
642 end
642 end
643 end
643 end
644
644
645 # Return true if this project allows to do the specified action.
645 # Return true if this project allows to do the specified action.
646 # action can be:
646 # action can be:
647 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
647 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
648 # * a permission Symbol (eg. :edit_project)
648 # * a permission Symbol (eg. :edit_project)
649 def allows_to?(action)
649 def allows_to?(action)
650 if archived?
650 if archived?
651 # No action allowed on archived projects
651 # No action allowed on archived projects
652 return false
652 return false
653 end
653 end
654 unless active? || Redmine::AccessControl.read_action?(action)
654 unless active? || Redmine::AccessControl.read_action?(action)
655 # No write action allowed on closed projects
655 # No write action allowed on closed projects
656 return false
656 return false
657 end
657 end
658 # No action allowed on disabled modules
658 # No action allowed on disabled modules
659 if action.is_a? Hash
659 if action.is_a? Hash
660 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
660 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
661 else
661 else
662 allowed_permissions.include? action
662 allowed_permissions.include? action
663 end
663 end
664 end
664 end
665
665
666 # Return the enabled module with the given name
666 # Return the enabled module with the given name
667 # or nil if the module is not enabled for the project
667 # or nil if the module is not enabled for the project
668 def enabled_module(name)
668 def enabled_module(name)
669 name = name.to_s
669 name = name.to_s
670 enabled_modules.detect {|m| m.name == name}
670 enabled_modules.detect {|m| m.name == name}
671 end
671 end
672
672
673 # Return true if the module with the given name is enabled
673 # Return true if the module with the given name is enabled
674 def module_enabled?(name)
674 def module_enabled?(name)
675 enabled_module(name).present?
675 enabled_module(name).present?
676 end
676 end
677
677
678 def enabled_module_names=(module_names)
678 def enabled_module_names=(module_names)
679 if module_names && module_names.is_a?(Array)
679 if module_names && module_names.is_a?(Array)
680 module_names = module_names.collect(&:to_s).reject(&:blank?)
680 module_names = module_names.collect(&:to_s).reject(&:blank?)
681 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
681 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
682 else
682 else
683 enabled_modules.clear
683 enabled_modules.clear
684 end
684 end
685 end
685 end
686
686
687 # Returns an array of the enabled modules names
687 # Returns an array of the enabled modules names
688 def enabled_module_names
688 def enabled_module_names
689 enabled_modules.collect(&:name)
689 enabled_modules.collect(&:name)
690 end
690 end
691
691
692 # Enable a specific module
692 # Enable a specific module
693 #
693 #
694 # Examples:
694 # Examples:
695 # project.enable_module!(:issue_tracking)
695 # project.enable_module!(:issue_tracking)
696 # project.enable_module!("issue_tracking")
696 # project.enable_module!("issue_tracking")
697 def enable_module!(name)
697 def enable_module!(name)
698 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
698 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
699 end
699 end
700
700
701 # Disable a module if it exists
701 # Disable a module if it exists
702 #
702 #
703 # Examples:
703 # Examples:
704 # project.disable_module!(:issue_tracking)
704 # project.disable_module!(:issue_tracking)
705 # project.disable_module!("issue_tracking")
705 # project.disable_module!("issue_tracking")
706 # project.disable_module!(project.enabled_modules.first)
706 # project.disable_module!(project.enabled_modules.first)
707 def disable_module!(target)
707 def disable_module!(target)
708 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
708 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
709 target.destroy unless target.blank?
709 target.destroy unless target.blank?
710 end
710 end
711
711
712 safe_attributes 'name',
712 safe_attributes 'name',
713 'description',
713 'description',
714 'homepage',
714 'homepage',
715 'is_public',
715 'is_public',
716 'identifier',
716 'identifier',
717 'custom_field_values',
717 'custom_field_values',
718 'custom_fields',
718 'custom_fields',
719 'tracker_ids',
719 'tracker_ids',
720 'issue_custom_field_ids',
720 'issue_custom_field_ids',
721 'parent_id',
721 'parent_id',
722 'default_version_id'
722 'default_version_id'
723
723
724 safe_attributes 'enabled_module_names',
724 safe_attributes 'enabled_module_names',
725 :if => lambda {|project, user|
725 :if => lambda {|project, user|
726 if project.new_record?
726 if project.new_record?
727 if user.admin?
727 if user.admin?
728 true
728 true
729 else
729 else
730 default_member_role.has_permission?(:select_project_modules)
730 default_member_role.has_permission?(:select_project_modules)
731 end
731 end
732 else
732 else
733 user.allowed_to?(:select_project_modules, project)
733 user.allowed_to?(:select_project_modules, project)
734 end
734 end
735 }
735 }
736
736
737 safe_attributes 'inherit_members',
737 safe_attributes 'inherit_members',
738 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
738 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
739
739
740 def safe_attributes=(attrs, user=User.current)
740 def safe_attributes=(attrs, user=User.current)
741 return unless attrs.is_a?(Hash)
741 return unless attrs.is_a?(Hash)
742 attrs = attrs.deep_dup
742 attrs = attrs.deep_dup
743
743
744 @unallowed_parent_id = nil
744 @unallowed_parent_id = nil
745 if new_record? || attrs.key?('parent_id')
745 if new_record? || attrs.key?('parent_id')
746 parent_id_param = attrs['parent_id'].to_s
746 parent_id_param = attrs['parent_id'].to_s
747 if new_record? || parent_id_param != parent_id.to_s
747 if new_record? || parent_id_param != parent_id.to_s
748 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
748 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
749 unless allowed_parents(user).include?(p)
749 unless allowed_parents(user).include?(p)
750 attrs.delete('parent_id')
750 attrs.delete('parent_id')
751 @unallowed_parent_id = true
751 @unallowed_parent_id = true
752 end
752 end
753 end
753 end
754 end
754 end
755
755
756 super(attrs, user)
756 super(attrs, user)
757 end
757 end
758
758
759 # Returns an auto-generated project identifier based on the last identifier used
759 # Returns an auto-generated project identifier based on the last identifier used
760 def self.next_identifier
760 def self.next_identifier
761 p = Project.order('id DESC').first
761 p = Project.order('id DESC').first
762 p.nil? ? nil : p.identifier.to_s.succ
762 p.nil? ? nil : p.identifier.to_s.succ
763 end
763 end
764
764
765 # Copies and saves the Project instance based on the +project+.
765 # Copies and saves the Project instance based on the +project+.
766 # Duplicates the source project's:
766 # Duplicates the source project's:
767 # * Wiki
767 # * Wiki
768 # * Versions
768 # * Versions
769 # * Categories
769 # * Categories
770 # * Issues
770 # * Issues
771 # * Members
771 # * Members
772 # * Queries
772 # * Queries
773 #
773 #
774 # Accepts an +options+ argument to specify what to copy
774 # Accepts an +options+ argument to specify what to copy
775 #
775 #
776 # Examples:
776 # Examples:
777 # project.copy(1) # => copies everything
777 # project.copy(1) # => copies everything
778 # project.copy(1, :only => 'members') # => copies members only
778 # project.copy(1, :only => 'members') # => copies members only
779 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
779 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
780 def copy(project, options={})
780 def copy(project, options={})
781 project = project.is_a?(Project) ? project : Project.find(project)
781 project = project.is_a?(Project) ? project : Project.find(project)
782
782
783 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
783 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
784 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
784 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
785
785
786 Project.transaction do
786 Project.transaction do
787 if save
787 if save
788 reload
788 reload
789 to_be_copied.each do |name|
789 to_be_copied.each do |name|
790 send "copy_#{name}", project
790 send "copy_#{name}", project
791 end
791 end
792 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
792 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
793 save
793 save
794 else
794 else
795 false
795 false
796 end
796 end
797 end
797 end
798 end
798 end
799
799
800 def member_principals
800 def member_principals
801 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
801 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
802 memberships.active
802 memberships.active
803 end
803 end
804
804
805 # Returns a new unsaved Project instance with attributes copied from +project+
805 # Returns a new unsaved Project instance with attributes copied from +project+
806 def self.copy_from(project)
806 def self.copy_from(project)
807 project = project.is_a?(Project) ? project : Project.find(project)
807 project = project.is_a?(Project) ? project : Project.find(project)
808 # clear unique attributes
808 # clear unique attributes
809 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
809 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
810 copy = Project.new(attributes)
810 copy = Project.new(attributes)
811 copy.enabled_module_names = project.enabled_module_names
811 copy.enabled_module_names = project.enabled_module_names
812 copy.trackers = project.trackers
812 copy.trackers = project.trackers
813 copy.custom_values = project.custom_values.collect {|v| v.clone}
813 copy.custom_values = project.custom_values.collect {|v| v.clone}
814 copy.issue_custom_fields = project.issue_custom_fields
814 copy.issue_custom_fields = project.issue_custom_fields
815 copy
815 copy
816 end
816 end
817
817
818 # Yields the given block for each project with its level in the tree
818 # Yields the given block for each project with its level in the tree
819 def self.project_tree(projects, options={}, &block)
819 def self.project_tree(projects, options={}, &block)
820 ancestors = []
820 ancestors = []
821 if options[:init_level] && projects.first
821 if options[:init_level] && projects.first
822 ancestors = projects.first.ancestors.to_a
822 ancestors = projects.first.ancestors.to_a
823 end
823 end
824 projects.sort_by(&:lft).each do |project|
824 projects.sort_by(&:lft).each do |project|
825 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
825 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
826 ancestors.pop
826 ancestors.pop
827 end
827 end
828 yield project, ancestors.size
828 yield project, ancestors.size
829 ancestors << project
829 ancestors << project
830 end
830 end
831 end
831 end
832
832
833 private
833 private
834
834
835 def update_inherited_members
835 def update_inherited_members
836 if parent
836 if parent
837 if inherit_members? && !inherit_members_was
837 if inherit_members? && !inherit_members_was
838 remove_inherited_member_roles
838 remove_inherited_member_roles
839 add_inherited_member_roles
839 add_inherited_member_roles
840 elsif !inherit_members? && inherit_members_was
840 elsif !inherit_members? && inherit_members_was
841 remove_inherited_member_roles
841 remove_inherited_member_roles
842 end
842 end
843 end
843 end
844 end
844 end
845
845
846 def remove_inherited_member_roles
846 def remove_inherited_member_roles
847 member_roles = memberships.map(&:member_roles).flatten
847 member_roles = MemberRole.where(:member_id => membership_ids).to_a
848 member_role_ids = member_roles.map(&:id)
848 member_role_ids = member_roles.map(&:id)
849 member_roles.each do |member_role|
849 member_roles.each do |member_role|
850 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
850 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
851 member_role.destroy
851 member_role.destroy
852 end
852 end
853 end
853 end
854 end
854 end
855
855
856 def add_inherited_member_roles
856 def add_inherited_member_roles
857 if inherit_members? && parent
857 if inherit_members? && parent
858 parent.memberships.each do |parent_member|
858 parent.memberships.each do |parent_member|
859 member = Member.find_or_new(self.id, parent_member.user_id)
859 member = Member.find_or_new(self.id, parent_member.user_id)
860 parent_member.member_roles.each do |parent_member_role|
860 parent_member.member_roles.each do |parent_member_role|
861 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
861 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
862 end
862 end
863 member.save!
863 member.save!
864 end
864 end
865 memberships.reset
865 memberships.reset
866 end
866 end
867 end
867 end
868
868
869 def update_versions_from_hierarchy_change
869 def update_versions_from_hierarchy_change
870 Issue.update_versions_from_hierarchy_change(self)
870 Issue.update_versions_from_hierarchy_change(self)
871 end
871 end
872
872
873 def validate_parent
873 def validate_parent
874 if @unallowed_parent_id
874 if @unallowed_parent_id
875 errors.add(:parent_id, :invalid)
875 errors.add(:parent_id, :invalid)
876 elsif parent_id_changed?
876 elsif parent_id_changed?
877 unless parent.nil? || (parent.active? && move_possible?(parent))
877 unless parent.nil? || (parent.active? && move_possible?(parent))
878 errors.add(:parent_id, :invalid)
878 errors.add(:parent_id, :invalid)
879 end
879 end
880 end
880 end
881 end
881 end
882
882
883 # Copies wiki from +project+
883 # Copies wiki from +project+
884 def copy_wiki(project)
884 def copy_wiki(project)
885 # Check that the source project has a wiki first
885 # Check that the source project has a wiki first
886 unless project.wiki.nil?
886 unless project.wiki.nil?
887 wiki = self.wiki || Wiki.new
887 wiki = self.wiki || Wiki.new
888 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
888 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
889 wiki_pages_map = {}
889 wiki_pages_map = {}
890 project.wiki.pages.each do |page|
890 project.wiki.pages.each do |page|
891 # Skip pages without content
891 # Skip pages without content
892 next if page.content.nil?
892 next if page.content.nil?
893 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
893 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
894 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
894 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
895 new_wiki_page.content = new_wiki_content
895 new_wiki_page.content = new_wiki_content
896 wiki.pages << new_wiki_page
896 wiki.pages << new_wiki_page
897 wiki_pages_map[page.id] = new_wiki_page
897 wiki_pages_map[page.id] = new_wiki_page
898 end
898 end
899
899
900 self.wiki = wiki
900 self.wiki = wiki
901 wiki.save
901 wiki.save
902 # Reproduce page hierarchy
902 # Reproduce page hierarchy
903 project.wiki.pages.each do |page|
903 project.wiki.pages.each do |page|
904 if page.parent_id && wiki_pages_map[page.id]
904 if page.parent_id && wiki_pages_map[page.id]
905 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
905 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
906 wiki_pages_map[page.id].save
906 wiki_pages_map[page.id].save
907 end
907 end
908 end
908 end
909 end
909 end
910 end
910 end
911
911
912 # Copies versions from +project+
912 # Copies versions from +project+
913 def copy_versions(project)
913 def copy_versions(project)
914 project.versions.each do |version|
914 project.versions.each do |version|
915 new_version = Version.new
915 new_version = Version.new
916 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
916 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
917 self.versions << new_version
917 self.versions << new_version
918 end
918 end
919 end
919 end
920
920
921 # Copies issue categories from +project+
921 # Copies issue categories from +project+
922 def copy_issue_categories(project)
922 def copy_issue_categories(project)
923 project.issue_categories.each do |issue_category|
923 project.issue_categories.each do |issue_category|
924 new_issue_category = IssueCategory.new
924 new_issue_category = IssueCategory.new
925 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
925 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
926 self.issue_categories << new_issue_category
926 self.issue_categories << new_issue_category
927 end
927 end
928 end
928 end
929
929
930 # Copies issues from +project+
930 # Copies issues from +project+
931 def copy_issues(project)
931 def copy_issues(project)
932 # Stores the source issue id as a key and the copied issues as the
932 # Stores the source issue id as a key and the copied issues as the
933 # value. Used to map the two together for issue relations.
933 # value. Used to map the two together for issue relations.
934 issues_map = {}
934 issues_map = {}
935
935
936 # Store status and reopen locked/closed versions
936 # Store status and reopen locked/closed versions
937 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
937 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
938 version_statuses.each do |version, status|
938 version_statuses.each do |version, status|
939 version.update_attribute :status, 'open'
939 version.update_attribute :status, 'open'
940 end
940 end
941
941
942 # Get issues sorted by root_id, lft so that parent issues
942 # Get issues sorted by root_id, lft so that parent issues
943 # get copied before their children
943 # get copied before their children
944 project.issues.reorder('root_id, lft').each do |issue|
944 project.issues.reorder('root_id, lft').each do |issue|
945 new_issue = Issue.new
945 new_issue = Issue.new
946 new_issue.copy_from(issue, :subtasks => false, :link => false)
946 new_issue.copy_from(issue, :subtasks => false, :link => false)
947 new_issue.project = self
947 new_issue.project = self
948 # Changing project resets the custom field values
948 # Changing project resets the custom field values
949 # TODO: handle this in Issue#project=
949 # TODO: handle this in Issue#project=
950 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
950 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
951 # Reassign fixed_versions by name, since names are unique per project
951 # Reassign fixed_versions by name, since names are unique per project
952 if issue.fixed_version && issue.fixed_version.project == project
952 if issue.fixed_version && issue.fixed_version.project == project
953 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
953 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
954 end
954 end
955 # Reassign version custom field values
955 # Reassign version custom field values
956 new_issue.custom_field_values.each do |custom_value|
956 new_issue.custom_field_values.each do |custom_value|
957 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
957 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
958 versions = Version.where(:id => custom_value.value).to_a
958 versions = Version.where(:id => custom_value.value).to_a
959 new_value = versions.map do |version|
959 new_value = versions.map do |version|
960 if version.project == project
960 if version.project == project
961 self.versions.detect {|v| v.name == version.name}.try(:id)
961 self.versions.detect {|v| v.name == version.name}.try(:id)
962 else
962 else
963 version.id
963 version.id
964 end
964 end
965 end
965 end
966 new_value.compact!
966 new_value.compact!
967 new_value = new_value.first unless custom_value.custom_field.multiple?
967 new_value = new_value.first unless custom_value.custom_field.multiple?
968 custom_value.value = new_value
968 custom_value.value = new_value
969 end
969 end
970 end
970 end
971 # Reassign the category by name, since names are unique per project
971 # Reassign the category by name, since names are unique per project
972 if issue.category
972 if issue.category
973 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
973 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
974 end
974 end
975 # Parent issue
975 # Parent issue
976 if issue.parent_id
976 if issue.parent_id
977 if copied_parent = issues_map[issue.parent_id]
977 if copied_parent = issues_map[issue.parent_id]
978 new_issue.parent_issue_id = copied_parent.id
978 new_issue.parent_issue_id = copied_parent.id
979 end
979 end
980 end
980 end
981
981
982 self.issues << new_issue
982 self.issues << new_issue
983 if new_issue.new_record?
983 if new_issue.new_record?
984 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
984 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
985 else
985 else
986 issues_map[issue.id] = new_issue unless new_issue.new_record?
986 issues_map[issue.id] = new_issue unless new_issue.new_record?
987 end
987 end
988 end
988 end
989
989
990 # Restore locked/closed version statuses
990 # Restore locked/closed version statuses
991 version_statuses.each do |version, status|
991 version_statuses.each do |version, status|
992 version.update_attribute :status, status
992 version.update_attribute :status, status
993 end
993 end
994
994
995 # Relations after in case issues related each other
995 # Relations after in case issues related each other
996 project.issues.each do |issue|
996 project.issues.each do |issue|
997 new_issue = issues_map[issue.id]
997 new_issue = issues_map[issue.id]
998 unless new_issue
998 unless new_issue
999 # Issue was not copied
999 # Issue was not copied
1000 next
1000 next
1001 end
1001 end
1002
1002
1003 # Relations
1003 # Relations
1004 issue.relations_from.each do |source_relation|
1004 issue.relations_from.each do |source_relation|
1005 new_issue_relation = IssueRelation.new
1005 new_issue_relation = IssueRelation.new
1006 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
1006 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
1007 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
1007 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
1008 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
1008 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
1009 new_issue_relation.issue_to = source_relation.issue_to
1009 new_issue_relation.issue_to = source_relation.issue_to
1010 end
1010 end
1011 new_issue.relations_from << new_issue_relation
1011 new_issue.relations_from << new_issue_relation
1012 end
1012 end
1013
1013
1014 issue.relations_to.each do |source_relation|
1014 issue.relations_to.each do |source_relation|
1015 new_issue_relation = IssueRelation.new
1015 new_issue_relation = IssueRelation.new
1016 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
1016 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
1017 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
1017 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
1018 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
1018 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
1019 new_issue_relation.issue_from = source_relation.issue_from
1019 new_issue_relation.issue_from = source_relation.issue_from
1020 end
1020 end
1021 new_issue.relations_to << new_issue_relation
1021 new_issue.relations_to << new_issue_relation
1022 end
1022 end
1023 end
1023 end
1024 end
1024 end
1025
1025
1026 # Copies members from +project+
1026 # Copies members from +project+
1027 def copy_members(project)
1027 def copy_members(project)
1028 # Copy users first, then groups to handle members with inherited and given roles
1028 # Copy users first, then groups to handle members with inherited and given roles
1029 members_to_copy = []
1029 members_to_copy = []
1030 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
1030 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
1031 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
1031 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
1032
1032
1033 members_to_copy.each do |member|
1033 members_to_copy.each do |member|
1034 new_member = Member.new
1034 new_member = Member.new
1035 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
1035 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
1036 # only copy non inherited roles
1036 # only copy non inherited roles
1037 # inherited roles will be added when copying the group membership
1037 # inherited roles will be added when copying the group membership
1038 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
1038 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
1039 next if role_ids.empty?
1039 next if role_ids.empty?
1040 new_member.role_ids = role_ids
1040 new_member.role_ids = role_ids
1041 new_member.project = self
1041 new_member.project = self
1042 self.members << new_member
1042 self.members << new_member
1043 end
1043 end
1044 end
1044 end
1045
1045
1046 # Copies queries from +project+
1046 # Copies queries from +project+
1047 def copy_queries(project)
1047 def copy_queries(project)
1048 project.queries.each do |query|
1048 project.queries.each do |query|
1049 new_query = IssueQuery.new
1049 new_query = IssueQuery.new
1050 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1050 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1051 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1051 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1052 new_query.project = self
1052 new_query.project = self
1053 new_query.user_id = query.user_id
1053 new_query.user_id = query.user_id
1054 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1054 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1055 self.queries << new_query
1055 self.queries << new_query
1056 end
1056 end
1057 end
1057 end
1058
1058
1059 # Copies boards from +project+
1059 # Copies boards from +project+
1060 def copy_boards(project)
1060 def copy_boards(project)
1061 project.boards.each do |board|
1061 project.boards.each do |board|
1062 new_board = Board.new
1062 new_board = Board.new
1063 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1063 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1064 new_board.project = self
1064 new_board.project = self
1065 self.boards << new_board
1065 self.boards << new_board
1066 end
1066 end
1067 end
1067 end
1068
1068
1069 def allowed_permissions
1069 def allowed_permissions
1070 @allowed_permissions ||= begin
1070 @allowed_permissions ||= begin
1071 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1071 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1072 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1072 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1073 end
1073 end
1074 end
1074 end
1075
1075
1076 def allowed_actions
1076 def allowed_actions
1077 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1077 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1078 end
1078 end
1079
1079
1080 # Archives subprojects recursively
1080 # Archives subprojects recursively
1081 def archive!
1081 def archive!
1082 children.each do |subproject|
1082 children.each do |subproject|
1083 subproject.send :archive!
1083 subproject.send :archive!
1084 end
1084 end
1085 update_attribute :status, STATUS_ARCHIVED
1085 update_attribute :status, STATUS_ARCHIVED
1086 end
1086 end
1087 end
1087 end
General Comments 0
You need to be logged in to leave comments. Login now