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