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