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