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