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