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