##// END OF EJS Templates
Merged r11332 from trunk (#13097)....
Jean-Philippe Lang -
r11134:ecef96a6b9e3
parent child
Show More
@@ -1,974 +1,976
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 => "#{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 # Recalculates all lft and rgt values based on project names
396 # Recalculates all lft and rgt values based on project names
397 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
397 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
398 # Used in BuildProjectsTree migration
398 # Used in BuildProjectsTree migration
399 def self.rebuild_tree!
399 def self.rebuild_tree!
400 transaction do
400 transaction do
401 update_all "lft = NULL, rgt = NULL"
401 update_all "lft = NULL, rgt = NULL"
402 rebuild!(false)
402 rebuild!(false)
403 end
403 end
404 end
404 end
405
405
406 # Returns an array of the trackers used by the project and its active sub projects
406 # Returns an array of the trackers used by the project and its active sub projects
407 def rolled_up_trackers
407 def rolled_up_trackers
408 @rolled_up_trackers ||=
408 @rolled_up_trackers ||=
409 Tracker.find(:all, :joins => :projects,
409 Tracker.find(:all, :joins => :projects,
410 :select => "DISTINCT #{Tracker.table_name}.*",
410 :select => "DISTINCT #{Tracker.table_name}.*",
411 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt],
411 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt],
412 :order => "#{Tracker.table_name}.position")
412 :order => "#{Tracker.table_name}.position")
413 end
413 end
414
414
415 # Closes open and locked project versions that are completed
415 # Closes open and locked project versions that are completed
416 def close_completed_versions
416 def close_completed_versions
417 Version.transaction do
417 Version.transaction do
418 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
418 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
419 if version.completed?
419 if version.completed?
420 version.update_attribute(:status, 'closed')
420 version.update_attribute(:status, 'closed')
421 end
421 end
422 end
422 end
423 end
423 end
424 end
424 end
425
425
426 # Returns a scope of the Versions on subprojects
426 # Returns a scope of the Versions on subprojects
427 def rolled_up_versions
427 def rolled_up_versions
428 @rolled_up_versions ||=
428 @rolled_up_versions ||=
429 Version.scoped(:include => :project,
429 Version.scoped(:include => :project,
430 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
430 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
431 end
431 end
432
432
433 # Returns a scope of the Versions used by the project
433 # Returns a scope of the Versions used by the project
434 def shared_versions
434 def shared_versions
435 if new_record?
435 if new_record?
436 Version.scoped(:include => :project,
436 Version.scoped(:include => :project,
437 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
437 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
438 else
438 else
439 @shared_versions ||= begin
439 @shared_versions ||= begin
440 r = root? ? self : root
440 r = root? ? self : root
441 Version.scoped(:include => :project,
441 Version.scoped(:include => :project,
442 :conditions => "#{Project.table_name}.id = #{id}" +
442 :conditions => "#{Project.table_name}.id = #{id}" +
443 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
443 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
444 " #{Version.table_name}.sharing = 'system'" +
444 " #{Version.table_name}.sharing = 'system'" +
445 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
445 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
446 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
446 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
447 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
447 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
448 "))")
448 "))")
449 end
449 end
450 end
450 end
451 end
451 end
452
452
453 # Returns a hash of project users grouped by role
453 # Returns a hash of project users grouped by role
454 def users_by_role
454 def users_by_role
455 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
455 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
456 m.roles.each do |r|
456 m.roles.each do |r|
457 h[r] ||= []
457 h[r] ||= []
458 h[r] << m.user
458 h[r] << m.user
459 end
459 end
460 h
460 h
461 end
461 end
462 end
462 end
463
463
464 # Deletes all project's members
464 # Deletes all project's members
465 def delete_all_members
465 def delete_all_members
466 me, mr = Member.table_name, MemberRole.table_name
466 me, mr = Member.table_name, MemberRole.table_name
467 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
467 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
468 Member.delete_all(['project_id = ?', id])
468 Member.delete_all(['project_id = ?', id])
469 end
469 end
470
470
471 # Users/groups issues can be assigned to
471 # Users/groups issues can be assigned to
472 def assignable_users
472 def assignable_users
473 assignable = Setting.issue_group_assignment? ? member_principals : members
473 assignable = Setting.issue_group_assignment? ? member_principals : members
474 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
474 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
475 end
475 end
476
476
477 # Returns the mail adresses of users that should be always notified on project events
477 # Returns the mail adresses of users that should be always notified on project events
478 def recipients
478 def recipients
479 notified_users.collect {|user| user.mail}
479 notified_users.collect {|user| user.mail}
480 end
480 end
481
481
482 # Returns the users that should be notified on project events
482 # Returns the users that should be notified on project events
483 def notified_users
483 def notified_users
484 # TODO: User part should be extracted to User#notify_about?
484 # TODO: User part should be extracted to User#notify_about?
485 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
485 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
486 end
486 end
487
487
488 # Returns an array of all custom fields enabled for project issues
488 # Returns an array of all custom fields enabled for project issues
489 # (explictly associated custom fields and custom fields enabled for all projects)
489 # (explictly associated custom fields and custom fields enabled for all projects)
490 def all_issue_custom_fields
490 def all_issue_custom_fields
491 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
491 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
492 end
492 end
493
493
494 # Returns an array of all custom fields enabled for project time entries
494 # Returns an array of all custom fields enabled for project time entries
495 # (explictly associated custom fields and custom fields enabled for all projects)
495 # (explictly associated custom fields and custom fields enabled for all projects)
496 def all_time_entry_custom_fields
496 def all_time_entry_custom_fields
497 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
497 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
498 end
498 end
499
499
500 def project
500 def project
501 self
501 self
502 end
502 end
503
503
504 def <=>(project)
504 def <=>(project)
505 name.downcase <=> project.name.downcase
505 name.downcase <=> project.name.downcase
506 end
506 end
507
507
508 def to_s
508 def to_s
509 name
509 name
510 end
510 end
511
511
512 # Returns a short description of the projects (first lines)
512 # Returns a short description of the projects (first lines)
513 def short_description(length = 255)
513 def short_description(length = 255)
514 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
514 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
515 end
515 end
516
516
517 def css_classes
517 def css_classes
518 s = 'project'
518 s = 'project'
519 s << ' root' if root?
519 s << ' root' if root?
520 s << ' child' if child?
520 s << ' child' if child?
521 s << (leaf? ? ' leaf' : ' parent')
521 s << (leaf? ? ' leaf' : ' parent')
522 unless active?
522 unless active?
523 if archived?
523 if archived?
524 s << ' archived'
524 s << ' archived'
525 else
525 else
526 s << ' closed'
526 s << ' closed'
527 end
527 end
528 end
528 end
529 s
529 s
530 end
530 end
531
531
532 # The earliest start date of a project, based on it's issues and versions
532 # The earliest start date of a project, based on it's issues and versions
533 def start_date
533 def start_date
534 [
534 [
535 issues.minimum('start_date'),
535 issues.minimum('start_date'),
536 shared_versions.collect(&:effective_date),
536 shared_versions.collect(&:effective_date),
537 shared_versions.collect(&:start_date)
537 shared_versions.collect(&:start_date)
538 ].flatten.compact.min
538 ].flatten.compact.min
539 end
539 end
540
540
541 # The latest due date of an issue or version
541 # The latest due date of an issue or version
542 def due_date
542 def due_date
543 [
543 [
544 issues.maximum('due_date'),
544 issues.maximum('due_date'),
545 shared_versions.collect(&:effective_date),
545 shared_versions.collect(&:effective_date),
546 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
546 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
547 ].flatten.compact.max
547 ].flatten.compact.max
548 end
548 end
549
549
550 def overdue?
550 def overdue?
551 active? && !due_date.nil? && (due_date < Date.today)
551 active? && !due_date.nil? && (due_date < Date.today)
552 end
552 end
553
553
554 # Returns the percent completed for this project, based on the
554 # Returns the percent completed for this project, based on the
555 # progress on it's versions.
555 # progress on it's versions.
556 def completed_percent(options={:include_subprojects => false})
556 def completed_percent(options={:include_subprojects => false})
557 if options.delete(:include_subprojects)
557 if options.delete(:include_subprojects)
558 total = self_and_descendants.collect(&:completed_percent).sum
558 total = self_and_descendants.collect(&:completed_percent).sum
559
559
560 total / self_and_descendants.count
560 total / self_and_descendants.count
561 else
561 else
562 if versions.count > 0
562 if versions.count > 0
563 total = versions.collect(&:completed_pourcent).sum
563 total = versions.collect(&:completed_pourcent).sum
564
564
565 total / versions.count
565 total / versions.count
566 else
566 else
567 100
567 100
568 end
568 end
569 end
569 end
570 end
570 end
571
571
572 # Return true if this project allows to do the specified action.
572 # Return true if this project allows to do the specified action.
573 # action can be:
573 # action can be:
574 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
574 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
575 # * a permission Symbol (eg. :edit_project)
575 # * a permission Symbol (eg. :edit_project)
576 def allows_to?(action)
576 def allows_to?(action)
577 if archived?
577 if archived?
578 # No action allowed on archived projects
578 # No action allowed on archived projects
579 return false
579 return false
580 end
580 end
581 unless active? || Redmine::AccessControl.read_action?(action)
581 unless active? || Redmine::AccessControl.read_action?(action)
582 # No write action allowed on closed projects
582 # No write action allowed on closed projects
583 return false
583 return false
584 end
584 end
585 # No action allowed on disabled modules
585 # No action allowed on disabled modules
586 if action.is_a? Hash
586 if action.is_a? Hash
587 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
587 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
588 else
588 else
589 allowed_permissions.include? action
589 allowed_permissions.include? action
590 end
590 end
591 end
591 end
592
592
593 def module_enabled?(module_name)
593 def module_enabled?(module_name)
594 module_name = module_name.to_s
594 module_name = module_name.to_s
595 enabled_modules.detect {|m| m.name == module_name}
595 enabled_modules.detect {|m| m.name == module_name}
596 end
596 end
597
597
598 def enabled_module_names=(module_names)
598 def enabled_module_names=(module_names)
599 if module_names && module_names.is_a?(Array)
599 if module_names && module_names.is_a?(Array)
600 module_names = module_names.collect(&:to_s).reject(&:blank?)
600 module_names = module_names.collect(&:to_s).reject(&:blank?)
601 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
601 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
602 else
602 else
603 enabled_modules.clear
603 enabled_modules.clear
604 end
604 end
605 end
605 end
606
606
607 # Returns an array of the enabled modules names
607 # Returns an array of the enabled modules names
608 def enabled_module_names
608 def enabled_module_names
609 enabled_modules.collect(&:name)
609 enabled_modules.collect(&:name)
610 end
610 end
611
611
612 # Enable a specific module
612 # Enable a specific module
613 #
613 #
614 # Examples:
614 # Examples:
615 # project.enable_module!(:issue_tracking)
615 # project.enable_module!(:issue_tracking)
616 # project.enable_module!("issue_tracking")
616 # project.enable_module!("issue_tracking")
617 def enable_module!(name)
617 def enable_module!(name)
618 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
618 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
619 end
619 end
620
620
621 # Disable a module if it exists
621 # Disable a module if it exists
622 #
622 #
623 # Examples:
623 # Examples:
624 # project.disable_module!(:issue_tracking)
624 # project.disable_module!(:issue_tracking)
625 # project.disable_module!("issue_tracking")
625 # project.disable_module!("issue_tracking")
626 # project.disable_module!(project.enabled_modules.first)
626 # project.disable_module!(project.enabled_modules.first)
627 def disable_module!(target)
627 def disable_module!(target)
628 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
628 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
629 target.destroy unless target.blank?
629 target.destroy unless target.blank?
630 end
630 end
631
631
632 safe_attributes 'name',
632 safe_attributes 'name',
633 'description',
633 'description',
634 'homepage',
634 'homepage',
635 'is_public',
635 'is_public',
636 'identifier',
636 'identifier',
637 'custom_field_values',
637 'custom_field_values',
638 'custom_fields',
638 'custom_fields',
639 'tracker_ids',
639 'tracker_ids',
640 'issue_custom_field_ids'
640 'issue_custom_field_ids'
641
641
642 safe_attributes 'enabled_module_names',
642 safe_attributes 'enabled_module_names',
643 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
643 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
644
644
645 # Returns an array of projects that are in this project's hierarchy
645 # Returns an array of projects that are in this project's hierarchy
646 #
646 #
647 # Example: parents, children, siblings
647 # Example: parents, children, siblings
648 def hierarchy
648 def hierarchy
649 parents = project.self_and_ancestors || []
649 parents = project.self_and_ancestors || []
650 descendants = project.descendants || []
650 descendants = project.descendants || []
651 project_hierarchy = parents | descendants # Set union
651 project_hierarchy = parents | descendants # Set union
652 end
652 end
653
653
654 # Returns an auto-generated project identifier based on the last identifier used
654 # Returns an auto-generated project identifier based on the last identifier used
655 def self.next_identifier
655 def self.next_identifier
656 p = Project.find(:first, :order => 'created_on DESC')
656 p = Project.find(:first, :order => 'created_on DESC')
657 p.nil? ? nil : p.identifier.to_s.succ
657 p.nil? ? nil : p.identifier.to_s.succ
658 end
658 end
659
659
660 # Copies and saves the Project instance based on the +project+.
660 # Copies and saves the Project instance based on the +project+.
661 # Duplicates the source project's:
661 # Duplicates the source project's:
662 # * Wiki
662 # * Wiki
663 # * Versions
663 # * Versions
664 # * Categories
664 # * Categories
665 # * Issues
665 # * Issues
666 # * Members
666 # * Members
667 # * Queries
667 # * Queries
668 #
668 #
669 # Accepts an +options+ argument to specify what to copy
669 # Accepts an +options+ argument to specify what to copy
670 #
670 #
671 # Examples:
671 # Examples:
672 # project.copy(1) # => copies everything
672 # project.copy(1) # => copies everything
673 # project.copy(1, :only => 'members') # => copies members only
673 # project.copy(1, :only => 'members') # => copies members only
674 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
674 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
675 def copy(project, options={})
675 def copy(project, options={})
676 project = project.is_a?(Project) ? project : Project.find(project)
676 project = project.is_a?(Project) ? project : Project.find(project)
677
677
678 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
678 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
679 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
679 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
680
680
681 Project.transaction do
681 Project.transaction do
682 if save
682 if save
683 reload
683 reload
684 to_be_copied.each do |name|
684 to_be_copied.each do |name|
685 send "copy_#{name}", project
685 send "copy_#{name}", project
686 end
686 end
687 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
687 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
688 save
688 save
689 end
689 end
690 end
690 end
691 end
691 end
692
692
693
693
694 # Copies +project+ and returns the new instance. This will not save
694 # Copies +project+ and returns the new instance. This will not save
695 # the copy
695 # the copy
696 def self.copy_from(project)
696 def self.copy_from(project)
697 begin
697 begin
698 project = project.is_a?(Project) ? project : Project.find(project)
698 project = project.is_a?(Project) ? project : Project.find(project)
699 if project
699 if project
700 # clear unique attributes
700 # clear unique attributes
701 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
701 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
702 copy = Project.new(attributes)
702 copy = Project.new(attributes)
703 copy.enabled_modules = project.enabled_modules
703 copy.enabled_modules = project.enabled_modules
704 copy.trackers = project.trackers
704 copy.trackers = project.trackers
705 copy.custom_values = project.custom_values.collect {|v| v.clone}
705 copy.custom_values = project.custom_values.collect {|v| v.clone}
706 copy.issue_custom_fields = project.issue_custom_fields
706 copy.issue_custom_fields = project.issue_custom_fields
707 return copy
707 return copy
708 else
708 else
709 return nil
709 return nil
710 end
710 end
711 rescue ActiveRecord::RecordNotFound
711 rescue ActiveRecord::RecordNotFound
712 return nil
712 return nil
713 end
713 end
714 end
714 end
715
715
716 # Yields the given block for each project with its level in the tree
716 # Yields the given block for each project with its level in the tree
717 def self.project_tree(projects, &block)
717 def self.project_tree(projects, &block)
718 ancestors = []
718 ancestors = []
719 projects.sort_by(&:lft).each do |project|
719 projects.sort_by(&:lft).each do |project|
720 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
720 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
721 ancestors.pop
721 ancestors.pop
722 end
722 end
723 yield project, ancestors.size
723 yield project, ancestors.size
724 ancestors << project
724 ancestors << project
725 end
725 end
726 end
726 end
727
727
728 private
728 private
729
729
730 # Copies wiki from +project+
730 # Copies wiki from +project+
731 def copy_wiki(project)
731 def copy_wiki(project)
732 # Check that the source project has a wiki first
732 # Check that the source project has a wiki first
733 unless project.wiki.nil?
733 unless project.wiki.nil?
734 self.wiki ||= Wiki.new
734 wiki = self.wiki || Wiki.new
735 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
735 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
736 wiki_pages_map = {}
736 wiki_pages_map = {}
737 project.wiki.pages.each do |page|
737 project.wiki.pages.each do |page|
738 # Skip pages without content
738 # Skip pages without content
739 next if page.content.nil?
739 next if page.content.nil?
740 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
740 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
741 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
741 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
742 new_wiki_page.content = new_wiki_content
742 new_wiki_page.content = new_wiki_content
743 wiki.pages << new_wiki_page
743 wiki.pages << new_wiki_page
744 wiki_pages_map[page.id] = new_wiki_page
744 wiki_pages_map[page.id] = new_wiki_page
745 end
745 end
746
747 self.wiki = wiki
746 wiki.save
748 wiki.save
747 # Reproduce page hierarchy
749 # Reproduce page hierarchy
748 project.wiki.pages.each do |page|
750 project.wiki.pages.each do |page|
749 if page.parent_id && wiki_pages_map[page.id]
751 if page.parent_id && wiki_pages_map[page.id]
750 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
752 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
751 wiki_pages_map[page.id].save
753 wiki_pages_map[page.id].save
752 end
754 end
753 end
755 end
754 end
756 end
755 end
757 end
756
758
757 # Copies versions from +project+
759 # Copies versions from +project+
758 def copy_versions(project)
760 def copy_versions(project)
759 project.versions.each do |version|
761 project.versions.each do |version|
760 new_version = Version.new
762 new_version = Version.new
761 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
763 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
762 self.versions << new_version
764 self.versions << new_version
763 end
765 end
764 end
766 end
765
767
766 # Copies issue categories from +project+
768 # Copies issue categories from +project+
767 def copy_issue_categories(project)
769 def copy_issue_categories(project)
768 project.issue_categories.each do |issue_category|
770 project.issue_categories.each do |issue_category|
769 new_issue_category = IssueCategory.new
771 new_issue_category = IssueCategory.new
770 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
772 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
771 self.issue_categories << new_issue_category
773 self.issue_categories << new_issue_category
772 end
774 end
773 end
775 end
774
776
775 # Copies issues from +project+
777 # Copies issues from +project+
776 def copy_issues(project)
778 def copy_issues(project)
777 # Stores the source issue id as a key and the copied issues as the
779 # Stores the source issue id as a key and the copied issues as the
778 # value. Used to map the two togeather for issue relations.
780 # value. Used to map the two togeather for issue relations.
779 issues_map = {}
781 issues_map = {}
780
782
781 # Store status and reopen locked/closed versions
783 # Store status and reopen locked/closed versions
782 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
784 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
783 version_statuses.each do |version, status|
785 version_statuses.each do |version, status|
784 version.update_attribute :status, 'open'
786 version.update_attribute :status, 'open'
785 end
787 end
786
788
787 # Get issues sorted by root_id, lft so that parent issues
789 # Get issues sorted by root_id, lft so that parent issues
788 # get copied before their children
790 # get copied before their children
789 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
791 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
790 new_issue = Issue.new
792 new_issue = Issue.new
791 new_issue.copy_from(issue, :subtasks => false, :link => false)
793 new_issue.copy_from(issue, :subtasks => false, :link => false)
792 new_issue.project = self
794 new_issue.project = self
793 # Reassign fixed_versions by name, since names are unique per project
795 # Reassign fixed_versions by name, since names are unique per project
794 if issue.fixed_version && issue.fixed_version.project == project
796 if issue.fixed_version && issue.fixed_version.project == project
795 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
797 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
796 end
798 end
797 # Reassign the category by name, since names are unique per project
799 # Reassign the category by name, since names are unique per project
798 if issue.category
800 if issue.category
799 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
801 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
800 end
802 end
801 # Parent issue
803 # Parent issue
802 if issue.parent_id
804 if issue.parent_id
803 if copied_parent = issues_map[issue.parent_id]
805 if copied_parent = issues_map[issue.parent_id]
804 new_issue.parent_issue_id = copied_parent.id
806 new_issue.parent_issue_id = copied_parent.id
805 end
807 end
806 end
808 end
807
809
808 self.issues << new_issue
810 self.issues << new_issue
809 if new_issue.new_record?
811 if new_issue.new_record?
810 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
812 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
811 else
813 else
812 issues_map[issue.id] = new_issue unless new_issue.new_record?
814 issues_map[issue.id] = new_issue unless new_issue.new_record?
813 end
815 end
814 end
816 end
815
817
816 # Restore locked/closed version statuses
818 # Restore locked/closed version statuses
817 version_statuses.each do |version, status|
819 version_statuses.each do |version, status|
818 version.update_attribute :status, status
820 version.update_attribute :status, status
819 end
821 end
820
822
821 # Relations after in case issues related each other
823 # Relations after in case issues related each other
822 project.issues.each do |issue|
824 project.issues.each do |issue|
823 new_issue = issues_map[issue.id]
825 new_issue = issues_map[issue.id]
824 unless new_issue
826 unless new_issue
825 # Issue was not copied
827 # Issue was not copied
826 next
828 next
827 end
829 end
828
830
829 # Relations
831 # Relations
830 issue.relations_from.each do |source_relation|
832 issue.relations_from.each do |source_relation|
831 new_issue_relation = IssueRelation.new
833 new_issue_relation = IssueRelation.new
832 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
834 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
833 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
835 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
834 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
836 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
835 new_issue_relation.issue_to = source_relation.issue_to
837 new_issue_relation.issue_to = source_relation.issue_to
836 end
838 end
837 new_issue.relations_from << new_issue_relation
839 new_issue.relations_from << new_issue_relation
838 end
840 end
839
841
840 issue.relations_to.each do |source_relation|
842 issue.relations_to.each do |source_relation|
841 new_issue_relation = IssueRelation.new
843 new_issue_relation = IssueRelation.new
842 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
844 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
843 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
845 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
844 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
846 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
845 new_issue_relation.issue_from = source_relation.issue_from
847 new_issue_relation.issue_from = source_relation.issue_from
846 end
848 end
847 new_issue.relations_to << new_issue_relation
849 new_issue.relations_to << new_issue_relation
848 end
850 end
849 end
851 end
850 end
852 end
851
853
852 # Copies members from +project+
854 # Copies members from +project+
853 def copy_members(project)
855 def copy_members(project)
854 # Copy users first, then groups to handle members with inherited and given roles
856 # Copy users first, then groups to handle members with inherited and given roles
855 members_to_copy = []
857 members_to_copy = []
856 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
858 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
857 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
859 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
858
860
859 members_to_copy.each do |member|
861 members_to_copy.each do |member|
860 new_member = Member.new
862 new_member = Member.new
861 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
863 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
862 # only copy non inherited roles
864 # only copy non inherited roles
863 # inherited roles will be added when copying the group membership
865 # inherited roles will be added when copying the group membership
864 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
866 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
865 next if role_ids.empty?
867 next if role_ids.empty?
866 new_member.role_ids = role_ids
868 new_member.role_ids = role_ids
867 new_member.project = self
869 new_member.project = self
868 self.members << new_member
870 self.members << new_member
869 end
871 end
870 end
872 end
871
873
872 # Copies queries from +project+
874 # Copies queries from +project+
873 def copy_queries(project)
875 def copy_queries(project)
874 project.queries.each do |query|
876 project.queries.each do |query|
875 new_query = ::Query.new
877 new_query = ::Query.new
876 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
878 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
877 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
879 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
878 new_query.project = self
880 new_query.project = self
879 new_query.user_id = query.user_id
881 new_query.user_id = query.user_id
880 self.queries << new_query
882 self.queries << new_query
881 end
883 end
882 end
884 end
883
885
884 # Copies boards from +project+
886 # Copies boards from +project+
885 def copy_boards(project)
887 def copy_boards(project)
886 project.boards.each do |board|
888 project.boards.each do |board|
887 new_board = Board.new
889 new_board = Board.new
888 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
890 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
889 new_board.project = self
891 new_board.project = self
890 self.boards << new_board
892 self.boards << new_board
891 end
893 end
892 end
894 end
893
895
894 def allowed_permissions
896 def allowed_permissions
895 @allowed_permissions ||= begin
897 @allowed_permissions ||= begin
896 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
898 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
897 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
899 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
898 end
900 end
899 end
901 end
900
902
901 def allowed_actions
903 def allowed_actions
902 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
904 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
903 end
905 end
904
906
905 # Returns all the active Systemwide and project specific activities
907 # Returns all the active Systemwide and project specific activities
906 def active_activities
908 def active_activities
907 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
909 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
908
910
909 if overridden_activity_ids.empty?
911 if overridden_activity_ids.empty?
910 return TimeEntryActivity.shared.active
912 return TimeEntryActivity.shared.active
911 else
913 else
912 return system_activities_and_project_overrides
914 return system_activities_and_project_overrides
913 end
915 end
914 end
916 end
915
917
916 # Returns all the Systemwide and project specific activities
918 # Returns all the Systemwide and project specific activities
917 # (inactive and active)
919 # (inactive and active)
918 def all_activities
920 def all_activities
919 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
921 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
920
922
921 if overridden_activity_ids.empty?
923 if overridden_activity_ids.empty?
922 return TimeEntryActivity.shared
924 return TimeEntryActivity.shared
923 else
925 else
924 return system_activities_and_project_overrides(true)
926 return system_activities_and_project_overrides(true)
925 end
927 end
926 end
928 end
927
929
928 # Returns the systemwide active activities merged with the project specific overrides
930 # Returns the systemwide active activities merged with the project specific overrides
929 def system_activities_and_project_overrides(include_inactive=false)
931 def system_activities_and_project_overrides(include_inactive=false)
930 if include_inactive
932 if include_inactive
931 return TimeEntryActivity.shared.
933 return TimeEntryActivity.shared.
932 find(:all,
934 find(:all,
933 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
935 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
934 self.time_entry_activities
936 self.time_entry_activities
935 else
937 else
936 return TimeEntryActivity.shared.active.
938 return TimeEntryActivity.shared.active.
937 find(:all,
939 find(:all,
938 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
940 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
939 self.time_entry_activities.active
941 self.time_entry_activities.active
940 end
942 end
941 end
943 end
942
944
943 # Archives subprojects recursively
945 # Archives subprojects recursively
944 def archive!
946 def archive!
945 children.each do |subproject|
947 children.each do |subproject|
946 subproject.send :archive!
948 subproject.send :archive!
947 end
949 end
948 update_attribute :status, STATUS_ARCHIVED
950 update_attribute :status, STATUS_ARCHIVED
949 end
951 end
950
952
951 def update_position_under_parent
953 def update_position_under_parent
952 set_or_update_position_under(parent)
954 set_or_update_position_under(parent)
953 end
955 end
954
956
955 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
957 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
956 def set_or_update_position_under(target_parent)
958 def set_or_update_position_under(target_parent)
957 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
959 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
958 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
960 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
959
961
960 if to_be_inserted_before
962 if to_be_inserted_before
961 move_to_left_of(to_be_inserted_before)
963 move_to_left_of(to_be_inserted_before)
962 elsif target_parent.nil?
964 elsif target_parent.nil?
963 if sibs.empty?
965 if sibs.empty?
964 # move_to_root adds the project in first (ie. left) position
966 # move_to_root adds the project in first (ie. left) position
965 move_to_root
967 move_to_root
966 else
968 else
967 move_to_right_of(sibs.last) unless self == sibs.last
969 move_to_right_of(sibs.last) unless self == sibs.last
968 end
970 end
969 else
971 else
970 # move_to_child_of adds the project in last (ie.right) position
972 # move_to_child_of adds the project in last (ie.right) position
971 move_to_child_of(target_parent)
973 move_to_child_of(target_parent)
972 end
974 end
973 end
975 end
974 end
976 end
General Comments 0
You need to be logged in to leave comments. Login now