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