##// END OF EJS Templates
Do not load projects association in #rolled_up_trackers....
Jean-Philippe Lang -
r5173:8cd93159d68c
parent child
Show More
@@ -1,839 +1,839
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, :joins => :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 ||= begin
382 @shared_versions ||= begin
383 r = root? ? self : root
383 r = root? ? self : root
384 Version.scoped(:include => :project,
384 Version.scoped(:include => :project,
385 :conditions => "#{Project.table_name}.id = #{id}" +
385 :conditions => "#{Project.table_name}.id = #{id}" +
386 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
386 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
387 " #{Version.table_name}.sharing = 'system'" +
387 " #{Version.table_name}.sharing = 'system'" +
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 >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
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 IN ('hierarchy', 'descendants'))" +
390 " 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')" +
391 "))")
391 "))")
392 end
392 end
393 end
393 end
394
394
395 # Returns a hash of project users grouped by role
395 # Returns a hash of project users grouped by role
396 def users_by_role
396 def users_by_role
397 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
397 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
398 m.roles.each do |r|
398 m.roles.each do |r|
399 h[r] ||= []
399 h[r] ||= []
400 h[r] << m.user
400 h[r] << m.user
401 end
401 end
402 h
402 h
403 end
403 end
404 end
404 end
405
405
406 # Deletes all project's members
406 # Deletes all project's members
407 def delete_all_members
407 def delete_all_members
408 me, mr = Member.table_name, MemberRole.table_name
408 me, mr = Member.table_name, MemberRole.table_name
409 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})")
410 Member.delete_all(['project_id = ?', id])
410 Member.delete_all(['project_id = ?', id])
411 end
411 end
412
412
413 # Users issues can be assigned to
413 # Users issues can be assigned to
414 def assignable_users
414 def assignable_users
415 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
416 end
416 end
417
417
418 # 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
419 def recipients
419 def recipients
420 notified_users.collect {|user| user.mail}
420 notified_users.collect {|user| user.mail}
421 end
421 end
422
422
423 # Returns the users that should be notified on project events
423 # Returns the users that should be notified on project events
424 def notified_users
424 def notified_users
425 # TODO: User part should be extracted to User#notify_about?
425 # TODO: User part should be extracted to User#notify_about?
426 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}
427 end
427 end
428
428
429 # Returns an array of all custom fields enabled for project issues
429 # Returns an array of all custom fields enabled for project issues
430 # (explictly associated custom fields and custom fields enabled for all projects)
430 # (explictly associated custom fields and custom fields enabled for all projects)
431 def all_issue_custom_fields
431 def all_issue_custom_fields
432 @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
433 end
433 end
434
434
435 def project
435 def project
436 self
436 self
437 end
437 end
438
438
439 def <=>(project)
439 def <=>(project)
440 name.downcase <=> project.name.downcase
440 name.downcase <=> project.name.downcase
441 end
441 end
442
442
443 def to_s
443 def to_s
444 name
444 name
445 end
445 end
446
446
447 # Returns a short description of the projects (first lines)
447 # Returns a short description of the projects (first lines)
448 def short_description(length = 255)
448 def short_description(length = 255)
449 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
449 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
450 end
450 end
451
451
452 def css_classes
452 def css_classes
453 s = 'project'
453 s = 'project'
454 s << ' root' if root?
454 s << ' root' if root?
455 s << ' child' if child?
455 s << ' child' if child?
456 s << (leaf? ? ' leaf' : ' parent')
456 s << (leaf? ? ' leaf' : ' parent')
457 s
457 s
458 end
458 end
459
459
460 # 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
461 def start_date
461 def start_date
462 [
462 [
463 issues.minimum('start_date'),
463 issues.minimum('start_date'),
464 shared_versions.collect(&:effective_date),
464 shared_versions.collect(&:effective_date),
465 shared_versions.collect(&:start_date)
465 shared_versions.collect(&:start_date)
466 ].flatten.compact.min
466 ].flatten.compact.min
467 end
467 end
468
468
469 # The latest due date of an issue or version
469 # The latest due date of an issue or version
470 def due_date
470 def due_date
471 [
471 [
472 issues.maximum('due_date'),
472 issues.maximum('due_date'),
473 shared_versions.collect(&:effective_date),
473 shared_versions.collect(&:effective_date),
474 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
474 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
475 ].flatten.compact.max
475 ].flatten.compact.max
476 end
476 end
477
477
478 def overdue?
478 def overdue?
479 active? && !due_date.nil? && (due_date < Date.today)
479 active? && !due_date.nil? && (due_date < Date.today)
480 end
480 end
481
481
482 # Returns the percent completed for this project, based on the
482 # Returns the percent completed for this project, based on the
483 # progress on it's versions.
483 # progress on it's versions.
484 def completed_percent(options={:include_subprojects => false})
484 def completed_percent(options={:include_subprojects => false})
485 if options.delete(:include_subprojects)
485 if options.delete(:include_subprojects)
486 total = self_and_descendants.collect(&:completed_percent).sum
486 total = self_and_descendants.collect(&:completed_percent).sum
487
487
488 total / self_and_descendants.count
488 total / self_and_descendants.count
489 else
489 else
490 if versions.count > 0
490 if versions.count > 0
491 total = versions.collect(&:completed_pourcent).sum
491 total = versions.collect(&:completed_pourcent).sum
492
492
493 total / versions.count
493 total / versions.count
494 else
494 else
495 100
495 100
496 end
496 end
497 end
497 end
498 end
498 end
499
499
500 # 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.
501 # action can be:
501 # action can be:
502 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
502 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
503 # * a permission Symbol (eg. :edit_project)
503 # * a permission Symbol (eg. :edit_project)
504 def allows_to?(action)
504 def allows_to?(action)
505 if action.is_a? Hash
505 if action.is_a? Hash
506 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
506 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
507 else
507 else
508 allowed_permissions.include? action
508 allowed_permissions.include? action
509 end
509 end
510 end
510 end
511
511
512 def module_enabled?(module_name)
512 def module_enabled?(module_name)
513 module_name = module_name.to_s
513 module_name = module_name.to_s
514 enabled_modules.detect {|m| m.name == module_name}
514 enabled_modules.detect {|m| m.name == module_name}
515 end
515 end
516
516
517 def enabled_module_names=(module_names)
517 def enabled_module_names=(module_names)
518 if module_names && module_names.is_a?(Array)
518 if module_names && module_names.is_a?(Array)
519 module_names = module_names.collect(&:to_s).reject(&:blank?)
519 module_names = module_names.collect(&:to_s).reject(&:blank?)
520 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
520 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
521 else
521 else
522 enabled_modules.clear
522 enabled_modules.clear
523 end
523 end
524 end
524 end
525
525
526 # Returns an array of the enabled modules names
526 # Returns an array of the enabled modules names
527 def enabled_module_names
527 def enabled_module_names
528 enabled_modules.collect(&:name)
528 enabled_modules.collect(&:name)
529 end
529 end
530
530
531 safe_attributes 'name',
531 safe_attributes 'name',
532 'description',
532 'description',
533 'homepage',
533 'homepage',
534 'is_public',
534 'is_public',
535 'identifier',
535 'identifier',
536 'custom_field_values',
536 'custom_field_values',
537 'custom_fields',
537 'custom_fields',
538 'tracker_ids',
538 'tracker_ids',
539 'issue_custom_field_ids'
539 'issue_custom_field_ids'
540
540
541 safe_attributes 'enabled_module_names',
541 safe_attributes 'enabled_module_names',
542 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
542 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
543
543
544 # Returns an array of projects that are in this project's hierarchy
544 # Returns an array of projects that are in this project's hierarchy
545 #
545 #
546 # Example: parents, children, siblings
546 # Example: parents, children, siblings
547 def hierarchy
547 def hierarchy
548 parents = project.self_and_ancestors || []
548 parents = project.self_and_ancestors || []
549 descendants = project.descendants || []
549 descendants = project.descendants || []
550 project_hierarchy = parents | descendants # Set union
550 project_hierarchy = parents | descendants # Set union
551 end
551 end
552
552
553 # Returns an auto-generated project identifier based on the last identifier used
553 # Returns an auto-generated project identifier based on the last identifier used
554 def self.next_identifier
554 def self.next_identifier
555 p = Project.find(:first, :order => 'created_on DESC')
555 p = Project.find(:first, :order => 'created_on DESC')
556 p.nil? ? nil : p.identifier.to_s.succ
556 p.nil? ? nil : p.identifier.to_s.succ
557 end
557 end
558
558
559 # Copies and saves the Project instance based on the +project+.
559 # Copies and saves the Project instance based on the +project+.
560 # Duplicates the source project's:
560 # Duplicates the source project's:
561 # * Wiki
561 # * Wiki
562 # * Versions
562 # * Versions
563 # * Categories
563 # * Categories
564 # * Issues
564 # * Issues
565 # * Members
565 # * Members
566 # * Queries
566 # * Queries
567 #
567 #
568 # Accepts an +options+ argument to specify what to copy
568 # Accepts an +options+ argument to specify what to copy
569 #
569 #
570 # Examples:
570 # Examples:
571 # project.copy(1) # => copies everything
571 # project.copy(1) # => copies everything
572 # project.copy(1, :only => 'members') # => copies members only
572 # project.copy(1, :only => 'members') # => copies members only
573 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
573 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
574 def copy(project, options={})
574 def copy(project, options={})
575 project = project.is_a?(Project) ? project : Project.find(project)
575 project = project.is_a?(Project) ? project : Project.find(project)
576
576
577 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
577 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
578 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
578 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
579
579
580 Project.transaction do
580 Project.transaction do
581 if save
581 if save
582 reload
582 reload
583 to_be_copied.each do |name|
583 to_be_copied.each do |name|
584 send "copy_#{name}", project
584 send "copy_#{name}", project
585 end
585 end
586 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
586 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
587 save
587 save
588 end
588 end
589 end
589 end
590 end
590 end
591
591
592
592
593 # Copies +project+ and returns the new instance. This will not save
593 # Copies +project+ and returns the new instance. This will not save
594 # the copy
594 # the copy
595 def self.copy_from(project)
595 def self.copy_from(project)
596 begin
596 begin
597 project = project.is_a?(Project) ? project : Project.find(project)
597 project = project.is_a?(Project) ? project : Project.find(project)
598 if project
598 if project
599 # clear unique attributes
599 # clear unique attributes
600 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
600 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
601 copy = Project.new(attributes)
601 copy = Project.new(attributes)
602 copy.enabled_modules = project.enabled_modules
602 copy.enabled_modules = project.enabled_modules
603 copy.trackers = project.trackers
603 copy.trackers = project.trackers
604 copy.custom_values = project.custom_values.collect {|v| v.clone}
604 copy.custom_values = project.custom_values.collect {|v| v.clone}
605 copy.issue_custom_fields = project.issue_custom_fields
605 copy.issue_custom_fields = project.issue_custom_fields
606 return copy
606 return copy
607 else
607 else
608 return nil
608 return nil
609 end
609 end
610 rescue ActiveRecord::RecordNotFound
610 rescue ActiveRecord::RecordNotFound
611 return nil
611 return nil
612 end
612 end
613 end
613 end
614
614
615 # Yields the given block for each project with its level in the tree
615 # Yields the given block for each project with its level in the tree
616 def self.project_tree(projects, &block)
616 def self.project_tree(projects, &block)
617 ancestors = []
617 ancestors = []
618 projects.sort_by(&:lft).each do |project|
618 projects.sort_by(&:lft).each do |project|
619 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
619 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
620 ancestors.pop
620 ancestors.pop
621 end
621 end
622 yield project, ancestors.size
622 yield project, ancestors.size
623 ancestors << project
623 ancestors << project
624 end
624 end
625 end
625 end
626
626
627 private
627 private
628
628
629 # Copies wiki from +project+
629 # Copies wiki from +project+
630 def copy_wiki(project)
630 def copy_wiki(project)
631 # Check that the source project has a wiki first
631 # Check that the source project has a wiki first
632 unless project.wiki.nil?
632 unless project.wiki.nil?
633 self.wiki ||= Wiki.new
633 self.wiki ||= Wiki.new
634 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
634 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
635 wiki_pages_map = {}
635 wiki_pages_map = {}
636 project.wiki.pages.each do |page|
636 project.wiki.pages.each do |page|
637 # Skip pages without content
637 # Skip pages without content
638 next if page.content.nil?
638 next if page.content.nil?
639 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
639 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
640 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
640 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
641 new_wiki_page.content = new_wiki_content
641 new_wiki_page.content = new_wiki_content
642 wiki.pages << new_wiki_page
642 wiki.pages << new_wiki_page
643 wiki_pages_map[page.id] = new_wiki_page
643 wiki_pages_map[page.id] = new_wiki_page
644 end
644 end
645 wiki.save
645 wiki.save
646 # Reproduce page hierarchy
646 # Reproduce page hierarchy
647 project.wiki.pages.each do |page|
647 project.wiki.pages.each do |page|
648 if page.parent_id && wiki_pages_map[page.id]
648 if page.parent_id && wiki_pages_map[page.id]
649 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
649 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
650 wiki_pages_map[page.id].save
650 wiki_pages_map[page.id].save
651 end
651 end
652 end
652 end
653 end
653 end
654 end
654 end
655
655
656 # Copies versions from +project+
656 # Copies versions from +project+
657 def copy_versions(project)
657 def copy_versions(project)
658 project.versions.each do |version|
658 project.versions.each do |version|
659 new_version = Version.new
659 new_version = Version.new
660 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
660 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
661 self.versions << new_version
661 self.versions << new_version
662 end
662 end
663 end
663 end
664
664
665 # Copies issue categories from +project+
665 # Copies issue categories from +project+
666 def copy_issue_categories(project)
666 def copy_issue_categories(project)
667 project.issue_categories.each do |issue_category|
667 project.issue_categories.each do |issue_category|
668 new_issue_category = IssueCategory.new
668 new_issue_category = IssueCategory.new
669 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
669 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
670 self.issue_categories << new_issue_category
670 self.issue_categories << new_issue_category
671 end
671 end
672 end
672 end
673
673
674 # Copies issues from +project+
674 # Copies issues from +project+
675 def copy_issues(project)
675 def copy_issues(project)
676 # Stores the source issue id as a key and the copied issues as the
676 # Stores the source issue id as a key and the copied issues as the
677 # value. Used to map the two togeather for issue relations.
677 # value. Used to map the two togeather for issue relations.
678 issues_map = {}
678 issues_map = {}
679
679
680 # Get issues sorted by root_id, lft so that parent issues
680 # Get issues sorted by root_id, lft so that parent issues
681 # get copied before their children
681 # get copied before their children
682 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
682 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
683 new_issue = Issue.new
683 new_issue = Issue.new
684 new_issue.copy_from(issue)
684 new_issue.copy_from(issue)
685 new_issue.project = self
685 new_issue.project = self
686 # Reassign fixed_versions by name, since names are unique per
686 # Reassign fixed_versions by name, since names are unique per
687 # project and the versions for self are not yet saved
687 # project and the versions for self are not yet saved
688 if issue.fixed_version
688 if issue.fixed_version
689 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
689 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
690 end
690 end
691 # Reassign the category by name, since names are unique per
691 # Reassign the category by name, since names are unique per
692 # project and the categories for self are not yet saved
692 # project and the categories for self are not yet saved
693 if issue.category
693 if issue.category
694 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
694 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
695 end
695 end
696 # Parent issue
696 # Parent issue
697 if issue.parent_id
697 if issue.parent_id
698 if copied_parent = issues_map[issue.parent_id]
698 if copied_parent = issues_map[issue.parent_id]
699 new_issue.parent_issue_id = copied_parent.id
699 new_issue.parent_issue_id = copied_parent.id
700 end
700 end
701 end
701 end
702
702
703 self.issues << new_issue
703 self.issues << new_issue
704 if new_issue.new_record?
704 if new_issue.new_record?
705 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
705 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
706 else
706 else
707 issues_map[issue.id] = new_issue unless new_issue.new_record?
707 issues_map[issue.id] = new_issue unless new_issue.new_record?
708 end
708 end
709 end
709 end
710
710
711 # Relations after in case issues related each other
711 # Relations after in case issues related each other
712 project.issues.each do |issue|
712 project.issues.each do |issue|
713 new_issue = issues_map[issue.id]
713 new_issue = issues_map[issue.id]
714 unless new_issue
714 unless new_issue
715 # Issue was not copied
715 # Issue was not copied
716 next
716 next
717 end
717 end
718
718
719 # Relations
719 # Relations
720 issue.relations_from.each do |source_relation|
720 issue.relations_from.each do |source_relation|
721 new_issue_relation = IssueRelation.new
721 new_issue_relation = IssueRelation.new
722 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
722 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
723 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
723 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
724 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
724 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
725 new_issue_relation.issue_to = source_relation.issue_to
725 new_issue_relation.issue_to = source_relation.issue_to
726 end
726 end
727 new_issue.relations_from << new_issue_relation
727 new_issue.relations_from << new_issue_relation
728 end
728 end
729
729
730 issue.relations_to.each do |source_relation|
730 issue.relations_to.each do |source_relation|
731 new_issue_relation = IssueRelation.new
731 new_issue_relation = IssueRelation.new
732 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
732 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
733 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
733 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
734 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
734 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
735 new_issue_relation.issue_from = source_relation.issue_from
735 new_issue_relation.issue_from = source_relation.issue_from
736 end
736 end
737 new_issue.relations_to << new_issue_relation
737 new_issue.relations_to << new_issue_relation
738 end
738 end
739 end
739 end
740 end
740 end
741
741
742 # Copies members from +project+
742 # Copies members from +project+
743 def copy_members(project)
743 def copy_members(project)
744 # Copy users first, then groups to handle members with inherited and given roles
744 # Copy users first, then groups to handle members with inherited and given roles
745 members_to_copy = []
745 members_to_copy = []
746 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
746 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
747 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
747 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
748
748
749 members_to_copy.each do |member|
749 members_to_copy.each do |member|
750 new_member = Member.new
750 new_member = Member.new
751 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
751 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
752 # only copy non inherited roles
752 # only copy non inherited roles
753 # inherited roles will be added when copying the group membership
753 # inherited roles will be added when copying the group membership
754 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
754 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
755 next if role_ids.empty?
755 next if role_ids.empty?
756 new_member.role_ids = role_ids
756 new_member.role_ids = role_ids
757 new_member.project = self
757 new_member.project = self
758 self.members << new_member
758 self.members << new_member
759 end
759 end
760 end
760 end
761
761
762 # Copies queries from +project+
762 # Copies queries from +project+
763 def copy_queries(project)
763 def copy_queries(project)
764 project.queries.each do |query|
764 project.queries.each do |query|
765 new_query = Query.new
765 new_query = Query.new
766 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
766 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
767 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
767 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
768 new_query.project = self
768 new_query.project = self
769 self.queries << new_query
769 self.queries << new_query
770 end
770 end
771 end
771 end
772
772
773 # Copies boards from +project+
773 # Copies boards from +project+
774 def copy_boards(project)
774 def copy_boards(project)
775 project.boards.each do |board|
775 project.boards.each do |board|
776 new_board = Board.new
776 new_board = Board.new
777 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
777 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
778 new_board.project = self
778 new_board.project = self
779 self.boards << new_board
779 self.boards << new_board
780 end
780 end
781 end
781 end
782
782
783 def allowed_permissions
783 def allowed_permissions
784 @allowed_permissions ||= begin
784 @allowed_permissions ||= begin
785 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
785 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
786 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
786 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
787 end
787 end
788 end
788 end
789
789
790 def allowed_actions
790 def allowed_actions
791 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
791 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
792 end
792 end
793
793
794 # Returns all the active Systemwide and project specific activities
794 # Returns all the active Systemwide and project specific activities
795 def active_activities
795 def active_activities
796 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
796 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
797
797
798 if overridden_activity_ids.empty?
798 if overridden_activity_ids.empty?
799 return TimeEntryActivity.shared.active
799 return TimeEntryActivity.shared.active
800 else
800 else
801 return system_activities_and_project_overrides
801 return system_activities_and_project_overrides
802 end
802 end
803 end
803 end
804
804
805 # Returns all the Systemwide and project specific activities
805 # Returns all the Systemwide and project specific activities
806 # (inactive and active)
806 # (inactive and active)
807 def all_activities
807 def all_activities
808 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
808 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
809
809
810 if overridden_activity_ids.empty?
810 if overridden_activity_ids.empty?
811 return TimeEntryActivity.shared
811 return TimeEntryActivity.shared
812 else
812 else
813 return system_activities_and_project_overrides(true)
813 return system_activities_and_project_overrides(true)
814 end
814 end
815 end
815 end
816
816
817 # Returns the systemwide active activities merged with the project specific overrides
817 # Returns the systemwide active activities merged with the project specific overrides
818 def system_activities_and_project_overrides(include_inactive=false)
818 def system_activities_and_project_overrides(include_inactive=false)
819 if include_inactive
819 if include_inactive
820 return TimeEntryActivity.shared.
820 return TimeEntryActivity.shared.
821 find(:all,
821 find(:all,
822 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
822 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
823 self.time_entry_activities
823 self.time_entry_activities
824 else
824 else
825 return TimeEntryActivity.shared.active.
825 return TimeEntryActivity.shared.active.
826 find(:all,
826 find(:all,
827 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
827 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
828 self.time_entry_activities.active
828 self.time_entry_activities.active
829 end
829 end
830 end
830 end
831
831
832 # Archives subprojects recursively
832 # Archives subprojects recursively
833 def archive!
833 def archive!
834 children.each do |subproject|
834 children.each do |subproject|
835 subproject.send :archive!
835 subproject.send :archive!
836 end
836 end
837 update_attribute :status, STATUS_ARCHIVED
837 update_attribute :status, STATUS_ARCHIVED
838 end
838 end
839 end
839 end
General Comments 0
You need to be logged in to leave comments. Login now