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