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