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