##// END OF EJS Templates
Use association instead of a manual JOIN (#21583)....
Jean-Philippe Lang -
r14680:3a52b08405ad
parent child
Show More
@@ -1,1047 +1,1047
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id = #{user.id}))"
200 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id = #{user.id}))"
201 end
201 end
202 statement_by_role[role] = s
202 statement_by_role[role] = s
203 end
203 end
204 end
204 end
205 user.projects_by_role.each do |role, projects|
205 user.projects_by_role.each do |role, projects|
206 if role.allowed_to?(permission) && projects.any?
206 if role.allowed_to?(permission) && projects.any?
207 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
207 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
208 end
208 end
209 end
209 end
210 if statement_by_role.empty?
210 if statement_by_role.empty?
211 "1=0"
211 "1=0"
212 else
212 else
213 if block_given?
213 if block_given?
214 statement_by_role.each do |role, statement|
214 statement_by_role.each do |role, statement|
215 if s = yield(role, user)
215 if s = yield(role, user)
216 statement_by_role[role] = "(#{statement} AND (#{s}))"
216 statement_by_role[role] = "(#{statement} AND (#{s}))"
217 end
217 end
218 end
218 end
219 end
219 end
220 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
220 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
221 end
221 end
222 end
222 end
223 end
223 end
224
224
225 def override_roles(role)
225 def override_roles(role)
226 @override_members ||= memberships.
226 @override_members ||= memberships.
227 joins(:principal).
227 joins(:principal).
228 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
228 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
229
229
230 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
230 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
231 member = @override_members.detect {|m| m.principal.is_a? group_class}
231 member = @override_members.detect {|m| m.principal.is_a? group_class}
232 member ? member.roles.to_a : [role]
232 member ? member.roles.to_a : [role]
233 end
233 end
234
234
235 def principals
235 def principals
236 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
236 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
237 end
237 end
238
238
239 def users
239 def users
240 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
240 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
241 end
241 end
242
242
243 # Returns the Systemwide and project specific activities
243 # Returns the Systemwide and project specific activities
244 def activities(include_inactive=false)
244 def activities(include_inactive=false)
245 t = TimeEntryActivity.table_name
245 t = TimeEntryActivity.table_name
246 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
246 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
247
247
248 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
248 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
249 if overridden_activity_ids.any?
249 if overridden_activity_ids.any?
250 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
250 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
251 end
251 end
252 unless include_inactive
252 unless include_inactive
253 scope = scope.active
253 scope = scope.active
254 end
254 end
255 scope
255 scope
256 end
256 end
257
257
258 # Will create a new Project specific Activity or update an existing one
258 # Will create a new Project specific Activity or update an existing one
259 #
259 #
260 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
260 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
261 # does not successfully save.
261 # does not successfully save.
262 def update_or_create_time_entry_activity(id, activity_hash)
262 def update_or_create_time_entry_activity(id, activity_hash)
263 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
263 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
264 self.create_time_entry_activity_if_needed(activity_hash)
264 self.create_time_entry_activity_if_needed(activity_hash)
265 else
265 else
266 activity = project.time_entry_activities.find_by_id(id.to_i)
266 activity = project.time_entry_activities.find_by_id(id.to_i)
267 activity.update_attributes(activity_hash) if activity
267 activity.update_attributes(activity_hash) if activity
268 end
268 end
269 end
269 end
270
270
271 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
271 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
272 #
272 #
273 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
273 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
274 # does not successfully save.
274 # does not successfully save.
275 def create_time_entry_activity_if_needed(activity)
275 def create_time_entry_activity_if_needed(activity)
276 if activity['parent_id']
276 if activity['parent_id']
277 parent_activity = TimeEntryActivity.find(activity['parent_id'])
277 parent_activity = TimeEntryActivity.find(activity['parent_id'])
278 activity['name'] = parent_activity.name
278 activity['name'] = parent_activity.name
279 activity['position'] = parent_activity.position
279 activity['position'] = parent_activity.position
280 if Enumeration.overriding_change?(activity, parent_activity)
280 if Enumeration.overriding_change?(activity, parent_activity)
281 project_activity = self.time_entry_activities.create(activity)
281 project_activity = self.time_entry_activities.create(activity)
282 if project_activity.new_record?
282 if project_activity.new_record?
283 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
283 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
284 else
284 else
285 self.time_entries.
285 self.time_entries.
286 where(:activity_id => parent_activity.id).
286 where(:activity_id => parent_activity.id).
287 update_all(:activity_id => project_activity.id)
287 update_all(:activity_id => project_activity.id)
288 end
288 end
289 end
289 end
290 end
290 end
291 end
291 end
292
292
293 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
293 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
294 #
294 #
295 # Examples:
295 # Examples:
296 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
296 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
297 # project.project_condition(false) => "projects.id = 1"
297 # project.project_condition(false) => "projects.id = 1"
298 def project_condition(with_subprojects)
298 def project_condition(with_subprojects)
299 cond = "#{Project.table_name}.id = #{id}"
299 cond = "#{Project.table_name}.id = #{id}"
300 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
300 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
301 cond
301 cond
302 end
302 end
303
303
304 def self.find(*args)
304 def self.find(*args)
305 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
305 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
306 project = find_by_identifier(*args)
306 project = find_by_identifier(*args)
307 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
307 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
308 project
308 project
309 else
309 else
310 super
310 super
311 end
311 end
312 end
312 end
313
313
314 def self.find_by_param(*args)
314 def self.find_by_param(*args)
315 self.find(*args)
315 self.find(*args)
316 end
316 end
317
317
318 alias :base_reload :reload
318 alias :base_reload :reload
319 def reload(*args)
319 def reload(*args)
320 @principals = nil
320 @principals = nil
321 @users = nil
321 @users = nil
322 @shared_versions = nil
322 @shared_versions = nil
323 @rolled_up_versions = nil
323 @rolled_up_versions = nil
324 @rolled_up_trackers = nil
324 @rolled_up_trackers = nil
325 @all_issue_custom_fields = nil
325 @all_issue_custom_fields = nil
326 @all_time_entry_custom_fields = nil
326 @all_time_entry_custom_fields = nil
327 @to_param = nil
327 @to_param = nil
328 @allowed_parents = nil
328 @allowed_parents = nil
329 @allowed_permissions = nil
329 @allowed_permissions = nil
330 @actions_allowed = nil
330 @actions_allowed = nil
331 @start_date = nil
331 @start_date = nil
332 @due_date = nil
332 @due_date = nil
333 @override_members = nil
333 @override_members = nil
334 @assignable_users = nil
334 @assignable_users = nil
335 base_reload(*args)
335 base_reload(*args)
336 end
336 end
337
337
338 def to_param
338 def to_param
339 if new_record?
339 if new_record?
340 nil
340 nil
341 else
341 else
342 # id is used for projects with a numeric identifier (compatibility)
342 # id is used for projects with a numeric identifier (compatibility)
343 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
343 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
344 end
344 end
345 end
345 end
346
346
347 def active?
347 def active?
348 self.status == STATUS_ACTIVE
348 self.status == STATUS_ACTIVE
349 end
349 end
350
350
351 def archived?
351 def archived?
352 self.status == STATUS_ARCHIVED
352 self.status == STATUS_ARCHIVED
353 end
353 end
354
354
355 # Archives the project and its descendants
355 # Archives the project and its descendants
356 def archive
356 def archive
357 # Check that there is no issue of a non descendant project that is assigned
357 # Check that there is no issue of a non descendant project that is assigned
358 # to one of the project or descendant versions
358 # to one of the project or descendant versions
359 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
359 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
360
360
361 if version_ids.any? &&
361 if version_ids.any? &&
362 Issue.
362 Issue.
363 includes(:project).
363 includes(:project).
364 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
364 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
365 where(:fixed_version_id => version_ids).
365 where(:fixed_version_id => version_ids).
366 exists?
366 exists?
367 return false
367 return false
368 end
368 end
369 Project.transaction do
369 Project.transaction do
370 archive!
370 archive!
371 end
371 end
372 true
372 true
373 end
373 end
374
374
375 # Unarchives the project
375 # Unarchives the project
376 # All its ancestors must be active
376 # All its ancestors must be active
377 def unarchive
377 def unarchive
378 return false if ancestors.detect {|a| !a.active?}
378 return false if ancestors.detect {|a| !a.active?}
379 update_attribute :status, STATUS_ACTIVE
379 update_attribute :status, STATUS_ACTIVE
380 end
380 end
381
381
382 def close
382 def close
383 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
383 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
384 end
384 end
385
385
386 def reopen
386 def reopen
387 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
387 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
388 end
388 end
389
389
390 # Returns an array of projects the project can be moved to
390 # Returns an array of projects the project can be moved to
391 # by the current user
391 # by the current user
392 def allowed_parents(user=User.current)
392 def allowed_parents(user=User.current)
393 return @allowed_parents if @allowed_parents
393 return @allowed_parents if @allowed_parents
394 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
394 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
395 @allowed_parents = @allowed_parents - self_and_descendants
395 @allowed_parents = @allowed_parents - self_and_descendants
396 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
396 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
397 @allowed_parents << nil
397 @allowed_parents << nil
398 end
398 end
399 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
399 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
400 @allowed_parents << parent
400 @allowed_parents << parent
401 end
401 end
402 @allowed_parents
402 @allowed_parents
403 end
403 end
404
404
405 # Sets the parent of the project with authorization check
405 # Sets the parent of the project with authorization check
406 def set_allowed_parent!(p)
406 def set_allowed_parent!(p)
407 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
407 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
408 p = p.id if p.is_a?(Project)
408 p = p.id if p.is_a?(Project)
409 send :safe_attributes, {:project_id => p}
409 send :safe_attributes, {:project_id => p}
410 save
410 save
411 end
411 end
412
412
413 # Sets the parent of the project and saves the project
413 # Sets the parent of the project and saves the project
414 # Argument can be either a Project, a String, a Fixnum or nil
414 # Argument can be either a Project, a String, a Fixnum or nil
415 def set_parent!(p)
415 def set_parent!(p)
416 if p.is_a?(Project)
416 if p.is_a?(Project)
417 self.parent = p
417 self.parent = p
418 else
418 else
419 self.parent_id = p
419 self.parent_id = p
420 end
420 end
421 save
421 save
422 end
422 end
423
423
424 # Returns an array of the trackers used by the project and its active sub projects
424 # Returns an array of the trackers used by the project and its active sub projects
425 def rolled_up_trackers
425 def rolled_up_trackers
426 @rolled_up_trackers ||=
426 @rolled_up_trackers ||=
427 Tracker.
427 Tracker.
428 joins(:projects).
428 joins(projects: :enabled_modules).
429 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
430 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED).
429 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED).
430 where("#{EnabledModule.table_name}.name = ?", 'issue_tracking').
431 uniq.
431 uniq.
432 sorted.
432 sorted.
433 to_a
433 to_a
434 end
434 end
435
435
436 # Closes open and locked project versions that are completed
436 # Closes open and locked project versions that are completed
437 def close_completed_versions
437 def close_completed_versions
438 Version.transaction do
438 Version.transaction do
439 versions.where(:status => %w(open locked)).each do |version|
439 versions.where(:status => %w(open locked)).each do |version|
440 if version.completed?
440 if version.completed?
441 version.update_attribute(:status, 'closed')
441 version.update_attribute(:status, 'closed')
442 end
442 end
443 end
443 end
444 end
444 end
445 end
445 end
446
446
447 # Returns a scope of the Versions on subprojects
447 # Returns a scope of the Versions on subprojects
448 def rolled_up_versions
448 def rolled_up_versions
449 @rolled_up_versions ||=
449 @rolled_up_versions ||=
450 Version.
450 Version.
451 joins(:project).
451 joins(:project).
452 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
452 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
453 end
453 end
454
454
455 # Returns a scope of the Versions used by the project
455 # Returns a scope of the Versions used by the project
456 def shared_versions
456 def shared_versions
457 if new_record?
457 if new_record?
458 Version.
458 Version.
459 joins(:project).
459 joins(:project).
460 preload(:project).
460 preload(:project).
461 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
461 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
462 else
462 else
463 @shared_versions ||= begin
463 @shared_versions ||= begin
464 r = root? ? self : root
464 r = root? ? self : root
465 Version.
465 Version.
466 joins(:project).
466 joins(:project).
467 preload(:project).
467 preload(:project).
468 where("#{Project.table_name}.id = #{id}" +
468 where("#{Project.table_name}.id = #{id}" +
469 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
469 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
470 " #{Version.table_name}.sharing = 'system'" +
470 " #{Version.table_name}.sharing = 'system'" +
471 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
471 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
472 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
472 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
473 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
473 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
474 "))")
474 "))")
475 end
475 end
476 end
476 end
477 end
477 end
478
478
479 # Returns a hash of project users grouped by role
479 # Returns a hash of project users grouped by role
480 def users_by_role
480 def users_by_role
481 members.includes(:user, :roles).inject({}) do |h, m|
481 members.includes(:user, :roles).inject({}) do |h, m|
482 m.roles.each do |r|
482 m.roles.each do |r|
483 h[r] ||= []
483 h[r] ||= []
484 h[r] << m.user
484 h[r] << m.user
485 end
485 end
486 h
486 h
487 end
487 end
488 end
488 end
489
489
490 # Adds user as a project member with the default role
490 # Adds user as a project member with the default role
491 # Used for when a non-admin user creates a project
491 # Used for when a non-admin user creates a project
492 def add_default_member(user)
492 def add_default_member(user)
493 role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
493 role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
494 member = Member.new(:project => self, :principal => user, :roles => [role])
494 member = Member.new(:project => self, :principal => user, :roles => [role])
495 self.members << member
495 self.members << member
496 member
496 member
497 end
497 end
498
498
499 # Deletes all project's members
499 # Deletes all project's members
500 def delete_all_members
500 def delete_all_members
501 me, mr = Member.table_name, MemberRole.table_name
501 me, mr = Member.table_name, MemberRole.table_name
502 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
502 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
503 Member.delete_all(['project_id = ?', id])
503 Member.delete_all(['project_id = ?', id])
504 end
504 end
505
505
506 # Return a Principal scope of users/groups issues can be assigned to
506 # Return a Principal scope of users/groups issues can be assigned to
507 def assignable_users
507 def assignable_users
508 types = ['User']
508 types = ['User']
509 types << 'Group' if Setting.issue_group_assignment?
509 types << 'Group' if Setting.issue_group_assignment?
510
510
511 @assignable_users ||= Principal.
511 @assignable_users ||= Principal.
512 active.
512 active.
513 joins(:members => :roles).
513 joins(:members => :roles).
514 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
514 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
515 uniq.
515 uniq.
516 sorted
516 sorted
517 end
517 end
518
518
519 # Returns the mail addresses of users that should be always notified on project events
519 # Returns the mail addresses of users that should be always notified on project events
520 def recipients
520 def recipients
521 notified_users.collect {|user| user.mail}
521 notified_users.collect {|user| user.mail}
522 end
522 end
523
523
524 # Returns the users that should be notified on project events
524 # Returns the users that should be notified on project events
525 def notified_users
525 def notified_users
526 # TODO: User part should be extracted to User#notify_about?
526 # TODO: User part should be extracted to User#notify_about?
527 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
527 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
528 end
528 end
529
529
530 # Returns a scope of all custom fields enabled for project issues
530 # Returns a scope of all custom fields enabled for project issues
531 # (explicitly associated custom fields and custom fields enabled for all projects)
531 # (explicitly associated custom fields and custom fields enabled for all projects)
532 def all_issue_custom_fields
532 def all_issue_custom_fields
533 if new_record?
533 if new_record?
534 @all_issue_custom_fields ||= IssueCustomField.
534 @all_issue_custom_fields ||= IssueCustomField.
535 sorted.
535 sorted.
536 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
536 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
537 else
537 else
538 @all_issue_custom_fields ||= IssueCustomField.
538 @all_issue_custom_fields ||= IssueCustomField.
539 sorted.
539 sorted.
540 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
540 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
541 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
541 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
542 " WHERE cfp.project_id = ?)", true, id)
542 " WHERE cfp.project_id = ?)", true, id)
543 end
543 end
544 end
544 end
545
545
546 def project
546 def project
547 self
547 self
548 end
548 end
549
549
550 def <=>(project)
550 def <=>(project)
551 name.casecmp(project.name)
551 name.casecmp(project.name)
552 end
552 end
553
553
554 def to_s
554 def to_s
555 name
555 name
556 end
556 end
557
557
558 # Returns a short description of the projects (first lines)
558 # Returns a short description of the projects (first lines)
559 def short_description(length = 255)
559 def short_description(length = 255)
560 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
560 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
561 end
561 end
562
562
563 def css_classes
563 def css_classes
564 s = 'project'
564 s = 'project'
565 s << ' root' if root?
565 s << ' root' if root?
566 s << ' child' if child?
566 s << ' child' if child?
567 s << (leaf? ? ' leaf' : ' parent')
567 s << (leaf? ? ' leaf' : ' parent')
568 unless active?
568 unless active?
569 if archived?
569 if archived?
570 s << ' archived'
570 s << ' archived'
571 else
571 else
572 s << ' closed'
572 s << ' closed'
573 end
573 end
574 end
574 end
575 s
575 s
576 end
576 end
577
577
578 # The earliest start date of a project, based on it's issues and versions
578 # The earliest start date of a project, based on it's issues and versions
579 def start_date
579 def start_date
580 @start_date ||= [
580 @start_date ||= [
581 issues.minimum('start_date'),
581 issues.minimum('start_date'),
582 shared_versions.minimum('effective_date'),
582 shared_versions.minimum('effective_date'),
583 Issue.fixed_version(shared_versions).minimum('start_date')
583 Issue.fixed_version(shared_versions).minimum('start_date')
584 ].compact.min
584 ].compact.min
585 end
585 end
586
586
587 # The latest due date of an issue or version
587 # The latest due date of an issue or version
588 def due_date
588 def due_date
589 @due_date ||= [
589 @due_date ||= [
590 issues.maximum('due_date'),
590 issues.maximum('due_date'),
591 shared_versions.maximum('effective_date'),
591 shared_versions.maximum('effective_date'),
592 Issue.fixed_version(shared_versions).maximum('due_date')
592 Issue.fixed_version(shared_versions).maximum('due_date')
593 ].compact.max
593 ].compact.max
594 end
594 end
595
595
596 def overdue?
596 def overdue?
597 active? && !due_date.nil? && (due_date < Date.today)
597 active? && !due_date.nil? && (due_date < Date.today)
598 end
598 end
599
599
600 # Returns the percent completed for this project, based on the
600 # Returns the percent completed for this project, based on the
601 # progress on it's versions.
601 # progress on it's versions.
602 def completed_percent(options={:include_subprojects => false})
602 def completed_percent(options={:include_subprojects => false})
603 if options.delete(:include_subprojects)
603 if options.delete(:include_subprojects)
604 total = self_and_descendants.collect(&:completed_percent).sum
604 total = self_and_descendants.collect(&:completed_percent).sum
605
605
606 total / self_and_descendants.count
606 total / self_and_descendants.count
607 else
607 else
608 if versions.count > 0
608 if versions.count > 0
609 total = versions.collect(&:completed_percent).sum
609 total = versions.collect(&:completed_percent).sum
610
610
611 total / versions.count
611 total / versions.count
612 else
612 else
613 100
613 100
614 end
614 end
615 end
615 end
616 end
616 end
617
617
618 # Return true if this project allows to do the specified action.
618 # Return true if this project allows to do the specified action.
619 # action can be:
619 # action can be:
620 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
620 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
621 # * a permission Symbol (eg. :edit_project)
621 # * a permission Symbol (eg. :edit_project)
622 def allows_to?(action)
622 def allows_to?(action)
623 if archived?
623 if archived?
624 # No action allowed on archived projects
624 # No action allowed on archived projects
625 return false
625 return false
626 end
626 end
627 unless active? || Redmine::AccessControl.read_action?(action)
627 unless active? || Redmine::AccessControl.read_action?(action)
628 # No write action allowed on closed projects
628 # No write action allowed on closed projects
629 return false
629 return false
630 end
630 end
631 # No action allowed on disabled modules
631 # No action allowed on disabled modules
632 if action.is_a? Hash
632 if action.is_a? Hash
633 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
633 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
634 else
634 else
635 allowed_permissions.include? action
635 allowed_permissions.include? action
636 end
636 end
637 end
637 end
638
638
639 # Return the enabled module with the given name
639 # Return the enabled module with the given name
640 # or nil if the module is not enabled for the project
640 # or nil if the module is not enabled for the project
641 def enabled_module(name)
641 def enabled_module(name)
642 name = name.to_s
642 name = name.to_s
643 enabled_modules.detect {|m| m.name == name}
643 enabled_modules.detect {|m| m.name == name}
644 end
644 end
645
645
646 # Return true if the module with the given name is enabled
646 # Return true if the module with the given name is enabled
647 def module_enabled?(name)
647 def module_enabled?(name)
648 enabled_module(name).present?
648 enabled_module(name).present?
649 end
649 end
650
650
651 def enabled_module_names=(module_names)
651 def enabled_module_names=(module_names)
652 if module_names && module_names.is_a?(Array)
652 if module_names && module_names.is_a?(Array)
653 module_names = module_names.collect(&:to_s).reject(&:blank?)
653 module_names = module_names.collect(&:to_s).reject(&:blank?)
654 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
654 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
655 else
655 else
656 enabled_modules.clear
656 enabled_modules.clear
657 end
657 end
658 end
658 end
659
659
660 # Returns an array of the enabled modules names
660 # Returns an array of the enabled modules names
661 def enabled_module_names
661 def enabled_module_names
662 enabled_modules.collect(&:name)
662 enabled_modules.collect(&:name)
663 end
663 end
664
664
665 # Enable a specific module
665 # Enable a specific module
666 #
666 #
667 # Examples:
667 # Examples:
668 # project.enable_module!(:issue_tracking)
668 # project.enable_module!(:issue_tracking)
669 # project.enable_module!("issue_tracking")
669 # project.enable_module!("issue_tracking")
670 def enable_module!(name)
670 def enable_module!(name)
671 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
671 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
672 end
672 end
673
673
674 # Disable a module if it exists
674 # Disable a module if it exists
675 #
675 #
676 # Examples:
676 # Examples:
677 # project.disable_module!(:issue_tracking)
677 # project.disable_module!(:issue_tracking)
678 # project.disable_module!("issue_tracking")
678 # project.disable_module!("issue_tracking")
679 # project.disable_module!(project.enabled_modules.first)
679 # project.disable_module!(project.enabled_modules.first)
680 def disable_module!(target)
680 def disable_module!(target)
681 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
681 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
682 target.destroy unless target.blank?
682 target.destroy unless target.blank?
683 end
683 end
684
684
685 safe_attributes 'name',
685 safe_attributes 'name',
686 'description',
686 'description',
687 'homepage',
687 'homepage',
688 'is_public',
688 'is_public',
689 'identifier',
689 'identifier',
690 'custom_field_values',
690 'custom_field_values',
691 'custom_fields',
691 'custom_fields',
692 'tracker_ids',
692 'tracker_ids',
693 'issue_custom_field_ids',
693 'issue_custom_field_ids',
694 'parent_id',
694 'parent_id',
695 'default_version_id'
695 'default_version_id'
696
696
697 safe_attributes 'enabled_module_names',
697 safe_attributes 'enabled_module_names',
698 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
698 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
699
699
700 safe_attributes 'inherit_members',
700 safe_attributes 'inherit_members',
701 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
701 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
702
702
703 def safe_attributes=(attrs, user=User.current)
703 def safe_attributes=(attrs, user=User.current)
704 return unless attrs.is_a?(Hash)
704 return unless attrs.is_a?(Hash)
705 attrs = attrs.deep_dup
705 attrs = attrs.deep_dup
706
706
707 @unallowed_parent_id = nil
707 @unallowed_parent_id = nil
708 if new_record? || attrs.key?('parent_id')
708 if new_record? || attrs.key?('parent_id')
709 parent_id_param = attrs['parent_id'].to_s
709 parent_id_param = attrs['parent_id'].to_s
710 if new_record? || parent_id_param != parent_id.to_s
710 if new_record? || parent_id_param != parent_id.to_s
711 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
711 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
712 unless allowed_parents(user).include?(p)
712 unless allowed_parents(user).include?(p)
713 attrs.delete('parent_id')
713 attrs.delete('parent_id')
714 @unallowed_parent_id = true
714 @unallowed_parent_id = true
715 end
715 end
716 end
716 end
717 end
717 end
718
718
719 super(attrs, user)
719 super(attrs, user)
720 end
720 end
721
721
722 # Returns an auto-generated project identifier based on the last identifier used
722 # Returns an auto-generated project identifier based on the last identifier used
723 def self.next_identifier
723 def self.next_identifier
724 p = Project.order('id DESC').first
724 p = Project.order('id DESC').first
725 p.nil? ? nil : p.identifier.to_s.succ
725 p.nil? ? nil : p.identifier.to_s.succ
726 end
726 end
727
727
728 # Copies and saves the Project instance based on the +project+.
728 # Copies and saves the Project instance based on the +project+.
729 # Duplicates the source project's:
729 # Duplicates the source project's:
730 # * Wiki
730 # * Wiki
731 # * Versions
731 # * Versions
732 # * Categories
732 # * Categories
733 # * Issues
733 # * Issues
734 # * Members
734 # * Members
735 # * Queries
735 # * Queries
736 #
736 #
737 # Accepts an +options+ argument to specify what to copy
737 # Accepts an +options+ argument to specify what to copy
738 #
738 #
739 # Examples:
739 # Examples:
740 # project.copy(1) # => copies everything
740 # project.copy(1) # => copies everything
741 # project.copy(1, :only => 'members') # => copies members only
741 # project.copy(1, :only => 'members') # => copies members only
742 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
742 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
743 def copy(project, options={})
743 def copy(project, options={})
744 project = project.is_a?(Project) ? project : Project.find(project)
744 project = project.is_a?(Project) ? project : Project.find(project)
745
745
746 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
746 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
747 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
747 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
748
748
749 Project.transaction do
749 Project.transaction do
750 if save
750 if save
751 reload
751 reload
752 to_be_copied.each do |name|
752 to_be_copied.each do |name|
753 send "copy_#{name}", project
753 send "copy_#{name}", project
754 end
754 end
755 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
755 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
756 save
756 save
757 else
757 else
758 false
758 false
759 end
759 end
760 end
760 end
761 end
761 end
762
762
763 def member_principals
763 def member_principals
764 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
764 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
765 memberships.active
765 memberships.active
766 end
766 end
767
767
768 # Returns a new unsaved Project instance with attributes copied from +project+
768 # Returns a new unsaved Project instance with attributes copied from +project+
769 def self.copy_from(project)
769 def self.copy_from(project)
770 project = project.is_a?(Project) ? project : Project.find(project)
770 project = project.is_a?(Project) ? project : Project.find(project)
771 # clear unique attributes
771 # clear unique attributes
772 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
772 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
773 copy = Project.new(attributes)
773 copy = Project.new(attributes)
774 copy.enabled_module_names = project.enabled_module_names
774 copy.enabled_module_names = project.enabled_module_names
775 copy.trackers = project.trackers
775 copy.trackers = project.trackers
776 copy.custom_values = project.custom_values.collect {|v| v.clone}
776 copy.custom_values = project.custom_values.collect {|v| v.clone}
777 copy.issue_custom_fields = project.issue_custom_fields
777 copy.issue_custom_fields = project.issue_custom_fields
778 copy
778 copy
779 end
779 end
780
780
781 # Yields the given block for each project with its level in the tree
781 # Yields the given block for each project with its level in the tree
782 def self.project_tree(projects, &block)
782 def self.project_tree(projects, &block)
783 ancestors = []
783 ancestors = []
784 projects.sort_by(&:lft).each do |project|
784 projects.sort_by(&:lft).each do |project|
785 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
785 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
786 ancestors.pop
786 ancestors.pop
787 end
787 end
788 yield project, ancestors.size
788 yield project, ancestors.size
789 ancestors << project
789 ancestors << project
790 end
790 end
791 end
791 end
792
792
793 private
793 private
794
794
795 def update_inherited_members
795 def update_inherited_members
796 if parent
796 if parent
797 if inherit_members? && !inherit_members_was
797 if inherit_members? && !inherit_members_was
798 remove_inherited_member_roles
798 remove_inherited_member_roles
799 add_inherited_member_roles
799 add_inherited_member_roles
800 elsif !inherit_members? && inherit_members_was
800 elsif !inherit_members? && inherit_members_was
801 remove_inherited_member_roles
801 remove_inherited_member_roles
802 end
802 end
803 end
803 end
804 end
804 end
805
805
806 def remove_inherited_member_roles
806 def remove_inherited_member_roles
807 member_roles = memberships.map(&:member_roles).flatten
807 member_roles = memberships.map(&:member_roles).flatten
808 member_role_ids = member_roles.map(&:id)
808 member_role_ids = member_roles.map(&:id)
809 member_roles.each do |member_role|
809 member_roles.each do |member_role|
810 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
810 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
811 member_role.destroy
811 member_role.destroy
812 end
812 end
813 end
813 end
814 end
814 end
815
815
816 def add_inherited_member_roles
816 def add_inherited_member_roles
817 if inherit_members? && parent
817 if inherit_members? && parent
818 parent.memberships.each do |parent_member|
818 parent.memberships.each do |parent_member|
819 member = Member.find_or_new(self.id, parent_member.user_id)
819 member = Member.find_or_new(self.id, parent_member.user_id)
820 parent_member.member_roles.each do |parent_member_role|
820 parent_member.member_roles.each do |parent_member_role|
821 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
821 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
822 end
822 end
823 member.save!
823 member.save!
824 end
824 end
825 memberships.reset
825 memberships.reset
826 end
826 end
827 end
827 end
828
828
829 def update_versions_from_hierarchy_change
829 def update_versions_from_hierarchy_change
830 Issue.update_versions_from_hierarchy_change(self)
830 Issue.update_versions_from_hierarchy_change(self)
831 end
831 end
832
832
833 def validate_parent
833 def validate_parent
834 if @unallowed_parent_id
834 if @unallowed_parent_id
835 errors.add(:parent_id, :invalid)
835 errors.add(:parent_id, :invalid)
836 elsif parent_id_changed?
836 elsif parent_id_changed?
837 unless parent.nil? || (parent.active? && move_possible?(parent))
837 unless parent.nil? || (parent.active? && move_possible?(parent))
838 errors.add(:parent_id, :invalid)
838 errors.add(:parent_id, :invalid)
839 end
839 end
840 end
840 end
841 end
841 end
842
842
843 # Copies wiki from +project+
843 # Copies wiki from +project+
844 def copy_wiki(project)
844 def copy_wiki(project)
845 # Check that the source project has a wiki first
845 # Check that the source project has a wiki first
846 unless project.wiki.nil?
846 unless project.wiki.nil?
847 wiki = self.wiki || Wiki.new
847 wiki = self.wiki || Wiki.new
848 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
848 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
849 wiki_pages_map = {}
849 wiki_pages_map = {}
850 project.wiki.pages.each do |page|
850 project.wiki.pages.each do |page|
851 # Skip pages without content
851 # Skip pages without content
852 next if page.content.nil?
852 next if page.content.nil?
853 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
853 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
854 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
854 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
855 new_wiki_page.content = new_wiki_content
855 new_wiki_page.content = new_wiki_content
856 wiki.pages << new_wiki_page
856 wiki.pages << new_wiki_page
857 wiki_pages_map[page.id] = new_wiki_page
857 wiki_pages_map[page.id] = new_wiki_page
858 end
858 end
859
859
860 self.wiki = wiki
860 self.wiki = wiki
861 wiki.save
861 wiki.save
862 # Reproduce page hierarchy
862 # Reproduce page hierarchy
863 project.wiki.pages.each do |page|
863 project.wiki.pages.each do |page|
864 if page.parent_id && wiki_pages_map[page.id]
864 if page.parent_id && wiki_pages_map[page.id]
865 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
865 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
866 wiki_pages_map[page.id].save
866 wiki_pages_map[page.id].save
867 end
867 end
868 end
868 end
869 end
869 end
870 end
870 end
871
871
872 # Copies versions from +project+
872 # Copies versions from +project+
873 def copy_versions(project)
873 def copy_versions(project)
874 project.versions.each do |version|
874 project.versions.each do |version|
875 new_version = Version.new
875 new_version = Version.new
876 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
876 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
877 self.versions << new_version
877 self.versions << new_version
878 end
878 end
879 end
879 end
880
880
881 # Copies issue categories from +project+
881 # Copies issue categories from +project+
882 def copy_issue_categories(project)
882 def copy_issue_categories(project)
883 project.issue_categories.each do |issue_category|
883 project.issue_categories.each do |issue_category|
884 new_issue_category = IssueCategory.new
884 new_issue_category = IssueCategory.new
885 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
885 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
886 self.issue_categories << new_issue_category
886 self.issue_categories << new_issue_category
887 end
887 end
888 end
888 end
889
889
890 # Copies issues from +project+
890 # Copies issues from +project+
891 def copy_issues(project)
891 def copy_issues(project)
892 # Stores the source issue id as a key and the copied issues as the
892 # Stores the source issue id as a key and the copied issues as the
893 # value. Used to map the two together for issue relations.
893 # value. Used to map the two together for issue relations.
894 issues_map = {}
894 issues_map = {}
895
895
896 # Store status and reopen locked/closed versions
896 # Store status and reopen locked/closed versions
897 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
897 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
898 version_statuses.each do |version, status|
898 version_statuses.each do |version, status|
899 version.update_attribute :status, 'open'
899 version.update_attribute :status, 'open'
900 end
900 end
901
901
902 # Get issues sorted by root_id, lft so that parent issues
902 # Get issues sorted by root_id, lft so that parent issues
903 # get copied before their children
903 # get copied before their children
904 project.issues.reorder('root_id, lft').each do |issue|
904 project.issues.reorder('root_id, lft').each do |issue|
905 new_issue = Issue.new
905 new_issue = Issue.new
906 new_issue.copy_from(issue, :subtasks => false, :link => false)
906 new_issue.copy_from(issue, :subtasks => false, :link => false)
907 new_issue.project = self
907 new_issue.project = self
908 # Changing project resets the custom field values
908 # Changing project resets the custom field values
909 # TODO: handle this in Issue#project=
909 # TODO: handle this in Issue#project=
910 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
910 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
911 # Reassign fixed_versions by name, since names are unique per project
911 # Reassign fixed_versions by name, since names are unique per project
912 if issue.fixed_version && issue.fixed_version.project == project
912 if issue.fixed_version && issue.fixed_version.project == project
913 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
913 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
914 end
914 end
915 # Reassign version custom field values
915 # Reassign version custom field values
916 new_issue.custom_field_values.each do |custom_value|
916 new_issue.custom_field_values.each do |custom_value|
917 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
917 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
918 versions = Version.where(:id => custom_value.value).to_a
918 versions = Version.where(:id => custom_value.value).to_a
919 new_value = versions.map do |version|
919 new_value = versions.map do |version|
920 if version.project == project
920 if version.project == project
921 self.versions.detect {|v| v.name == version.name}.try(:id)
921 self.versions.detect {|v| v.name == version.name}.try(:id)
922 else
922 else
923 version.id
923 version.id
924 end
924 end
925 end
925 end
926 new_value.compact!
926 new_value.compact!
927 new_value = new_value.first unless custom_value.custom_field.multiple?
927 new_value = new_value.first unless custom_value.custom_field.multiple?
928 custom_value.value = new_value
928 custom_value.value = new_value
929 end
929 end
930 end
930 end
931 # Reassign the category by name, since names are unique per project
931 # Reassign the category by name, since names are unique per project
932 if issue.category
932 if issue.category
933 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
933 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
934 end
934 end
935 # Parent issue
935 # Parent issue
936 if issue.parent_id
936 if issue.parent_id
937 if copied_parent = issues_map[issue.parent_id]
937 if copied_parent = issues_map[issue.parent_id]
938 new_issue.parent_issue_id = copied_parent.id
938 new_issue.parent_issue_id = copied_parent.id
939 end
939 end
940 end
940 end
941
941
942 self.issues << new_issue
942 self.issues << new_issue
943 if new_issue.new_record?
943 if new_issue.new_record?
944 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
944 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
945 else
945 else
946 issues_map[issue.id] = new_issue unless new_issue.new_record?
946 issues_map[issue.id] = new_issue unless new_issue.new_record?
947 end
947 end
948 end
948 end
949
949
950 # Restore locked/closed version statuses
950 # Restore locked/closed version statuses
951 version_statuses.each do |version, status|
951 version_statuses.each do |version, status|
952 version.update_attribute :status, status
952 version.update_attribute :status, status
953 end
953 end
954
954
955 # Relations after in case issues related each other
955 # Relations after in case issues related each other
956 project.issues.each do |issue|
956 project.issues.each do |issue|
957 new_issue = issues_map[issue.id]
957 new_issue = issues_map[issue.id]
958 unless new_issue
958 unless new_issue
959 # Issue was not copied
959 # Issue was not copied
960 next
960 next
961 end
961 end
962
962
963 # Relations
963 # Relations
964 issue.relations_from.each do |source_relation|
964 issue.relations_from.each do |source_relation|
965 new_issue_relation = IssueRelation.new
965 new_issue_relation = IssueRelation.new
966 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
966 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
967 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
967 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
968 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
968 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
969 new_issue_relation.issue_to = source_relation.issue_to
969 new_issue_relation.issue_to = source_relation.issue_to
970 end
970 end
971 new_issue.relations_from << new_issue_relation
971 new_issue.relations_from << new_issue_relation
972 end
972 end
973
973
974 issue.relations_to.each do |source_relation|
974 issue.relations_to.each do |source_relation|
975 new_issue_relation = IssueRelation.new
975 new_issue_relation = IssueRelation.new
976 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
976 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
977 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
977 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
978 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
978 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
979 new_issue_relation.issue_from = source_relation.issue_from
979 new_issue_relation.issue_from = source_relation.issue_from
980 end
980 end
981 new_issue.relations_to << new_issue_relation
981 new_issue.relations_to << new_issue_relation
982 end
982 end
983 end
983 end
984 end
984 end
985
985
986 # Copies members from +project+
986 # Copies members from +project+
987 def copy_members(project)
987 def copy_members(project)
988 # Copy users first, then groups to handle members with inherited and given roles
988 # Copy users first, then groups to handle members with inherited and given roles
989 members_to_copy = []
989 members_to_copy = []
990 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
990 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
991 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
991 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
992
992
993 members_to_copy.each do |member|
993 members_to_copy.each do |member|
994 new_member = Member.new
994 new_member = Member.new
995 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
995 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
996 # only copy non inherited roles
996 # only copy non inherited roles
997 # inherited roles will be added when copying the group membership
997 # inherited roles will be added when copying the group membership
998 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
998 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
999 next if role_ids.empty?
999 next if role_ids.empty?
1000 new_member.role_ids = role_ids
1000 new_member.role_ids = role_ids
1001 new_member.project = self
1001 new_member.project = self
1002 self.members << new_member
1002 self.members << new_member
1003 end
1003 end
1004 end
1004 end
1005
1005
1006 # Copies queries from +project+
1006 # Copies queries from +project+
1007 def copy_queries(project)
1007 def copy_queries(project)
1008 project.queries.each do |query|
1008 project.queries.each do |query|
1009 new_query = IssueQuery.new
1009 new_query = IssueQuery.new
1010 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1010 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1011 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1011 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1012 new_query.project = self
1012 new_query.project = self
1013 new_query.user_id = query.user_id
1013 new_query.user_id = query.user_id
1014 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1014 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1015 self.queries << new_query
1015 self.queries << new_query
1016 end
1016 end
1017 end
1017 end
1018
1018
1019 # Copies boards from +project+
1019 # Copies boards from +project+
1020 def copy_boards(project)
1020 def copy_boards(project)
1021 project.boards.each do |board|
1021 project.boards.each do |board|
1022 new_board = Board.new
1022 new_board = Board.new
1023 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1023 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1024 new_board.project = self
1024 new_board.project = self
1025 self.boards << new_board
1025 self.boards << new_board
1026 end
1026 end
1027 end
1027 end
1028
1028
1029 def allowed_permissions
1029 def allowed_permissions
1030 @allowed_permissions ||= begin
1030 @allowed_permissions ||= begin
1031 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1031 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1032 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1032 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1033 end
1033 end
1034 end
1034 end
1035
1035
1036 def allowed_actions
1036 def allowed_actions
1037 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1037 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1038 end
1038 end
1039
1039
1040 # Archives subprojects recursively
1040 # Archives subprojects recursively
1041 def archive!
1041 def archive!
1042 children.each do |subproject|
1042 children.each do |subproject|
1043 subproject.send :archive!
1043 subproject.send :archive!
1044 end
1044 end
1045 update_attribute :status, STATUS_ARCHIVED
1045 update_attribute :status, STATUS_ARCHIVED
1046 end
1046 end
1047 end
1047 end
General Comments 0
You need to be logged in to leave comments. Login now