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