##// END OF EJS Templates
Code cleanup....
Jean-Philippe Lang -
r8406:f3a2c8d7f72a
parent child
Show More
@@ -1,987 +1,993
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 Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 belongs_to :project
21 belongs_to :project
22 belongs_to :tracker
22 belongs_to :tracker
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29
29
30 has_many :journals, :as => :journalized, :dependent => :destroy
30 has_many :journals, :as => :journalized, :dependent => :destroy
31 has_many :time_entries, :dependent => :delete_all
31 has_many :time_entries, :dependent => :delete_all
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33
33
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36
36
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 acts_as_customizable
39 acts_as_customizable
40 acts_as_watchable
40 acts_as_watchable
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 :include => [:project, :journals],
42 :include => [:project, :journals],
43 # sort by id so that limited eager loading doesn't break with postgresql
43 # sort by id so that limited eager loading doesn't break with postgresql
44 :order_column => "#{table_name}.id"
44 :order_column => "#{table_name}.id"
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48
48
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 :author_key => :author_id
50 :author_key => :author_id
51
51
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53
53
54 attr_reader :current_journal
54 attr_reader :current_journal
55
55
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57
57
58 validates_length_of :subject, :maximum => 255
58 validates_length_of :subject, :maximum => 255
59 validates_inclusion_of :done_ratio, :in => 0..100
59 validates_inclusion_of :done_ratio, :in => 0..100
60 validates_numericality_of :estimated_hours, :allow_nil => true
60 validates_numericality_of :estimated_hours, :allow_nil => true
61 validate :validate_issue
61 validate :validate_issue
62
62
63 named_scope :visible, lambda {|*args| { :include => :project,
63 named_scope :visible, lambda {|*args| { :include => :project,
64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65
65
66 named_scope :open, lambda {|*args|
66 named_scope :open, lambda {|*args|
67 is_closed = args.size > 0 ? !args.first : false
67 is_closed = args.size > 0 ? !args.first : false
68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
69 }
69 }
70
70
71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
73 named_scope :on_active_project, :include => [:status, :project, :tracker],
73 named_scope :on_active_project, :include => [:status, :project, :tracker],
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
75
75
76 before_create :default_assign
76 before_create :default_assign
77 before_save :close_duplicates, :update_done_ratio_from_issue_status
77 before_save :close_duplicates, :update_done_ratio_from_issue_status
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
80 after_destroy :update_parent_attributes
80 after_destroy :update_parent_attributes
81
81
82 # Returns a SQL conditions string used to find all issues visible by the specified user
82 # Returns a SQL conditions string used to find all issues visible by the specified user
83 def self.visible_condition(user, options={})
83 def self.visible_condition(user, options={})
84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
85 case role.issues_visibility
85 case role.issues_visibility
86 when 'all'
86 when 'all'
87 nil
87 nil
88 when 'default'
88 when 'default'
89 user_ids = [user.id] + user.groups.map(&:id)
89 user_ids = [user.id] + user.groups.map(&:id)
90 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
90 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
91 when 'own'
91 when 'own'
92 user_ids = [user.id] + user.groups.map(&:id)
92 user_ids = [user.id] + user.groups.map(&:id)
93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
94 else
94 else
95 '1=0'
95 '1=0'
96 end
96 end
97 end
97 end
98 end
98 end
99
99
100 # Returns true if usr or current user is allowed to view the issue
100 # Returns true if usr or current user is allowed to view the issue
101 def visible?(usr=nil)
101 def visible?(usr=nil)
102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
103 case role.issues_visibility
103 case role.issues_visibility
104 when 'all'
104 when 'all'
105 true
105 true
106 when 'default'
106 when 'default'
107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
108 when 'own'
108 when 'own'
109 self.author == user || user.is_or_belongs_to?(assigned_to)
109 self.author == user || user.is_or_belongs_to?(assigned_to)
110 else
110 else
111 false
111 false
112 end
112 end
113 end
113 end
114 end
114 end
115
115
116 def initialize(attributes=nil, *args)
116 def initialize(attributes=nil, *args)
117 super
117 super
118 if new_record?
118 if new_record?
119 # set default values for new records only
119 # set default values for new records only
120 self.status ||= IssueStatus.default
120 self.status ||= IssueStatus.default
121 self.priority ||= IssuePriority.default
121 self.priority ||= IssuePriority.default
122 end
122 end
123 end
123 end
124
124
125 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
125 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
126 def available_custom_fields
126 def available_custom_fields
127 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
127 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
128 end
128 end
129
129
130 def copy_from(arg)
130 def copy_from(arg)
131 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
131 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
132 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
132 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
133 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
133 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
134 self.status = issue.status
134 self.status = issue.status
135 self.author = User.current
135 self.author = User.current
136 self
136 self
137 end
137 end
138
138
139 # Moves/copies an issue to a new project and tracker
139 # Moves/copies an issue to a new project and tracker
140 # Returns the moved/copied issue on success, false on failure
140 # Returns the moved/copied issue on success, false on failure
141 def move_to_project(*args)
141 def move_to_project(*args)
142 ret = Issue.transaction do
142 ret = Issue.transaction do
143 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
143 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
144 end || false
144 end || false
145 end
145 end
146
146
147 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
147 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
148 options ||= {}
148 options ||= {}
149
149
150 if options[:copy]
150 if options[:copy]
151 issue = self.class.new.copy_from(self)
151 issue = self.class.new.copy_from(self)
152 else
152 else
153 issue = self
153 issue = self
154 issue.init_journal(User.current, options[:notes])
155 end
154 end
156
155
156 issue.init_journal(User.current, options[:notes])
157
157 issue.project = new_project
158 issue.project = new_project
158 if new_tracker
159 if new_tracker
159 issue.tracker = new_tracker
160 issue.tracker = new_tracker
160 end
161 end
161 # Allow bulk setting of attributes on the issue
162 # Allow bulk setting of attributes on the issue
162 if options[:attributes]
163 if options[:attributes]
163 issue.attributes = options[:attributes]
164 issue.attributes = options[:attributes]
164 end
165 end
165 if options[:copy] && options[:notes].present?
166
166 issue.init_journal(User.current, options[:notes])
167 issue.current_journal.notify = false
168 end
169 unless issue.save
167 unless issue.save
170 return false
168 return false
171 end
169 end
172 issue
170 issue
173 end
171 end
174
172
175 def status_id=(sid)
173 def status_id=(sid)
176 self.status = nil
174 self.status = nil
177 write_attribute(:status_id, sid)
175 write_attribute(:status_id, sid)
178 end
176 end
179
177
180 def priority_id=(pid)
178 def priority_id=(pid)
181 self.priority = nil
179 self.priority = nil
182 write_attribute(:priority_id, pid)
180 write_attribute(:priority_id, pid)
183 end
181 end
184
182
185 def tracker_id=(tid)
183 def tracker_id=(tid)
186 self.tracker = nil
184 self.tracker = nil
187 result = write_attribute(:tracker_id, tid)
185 result = write_attribute(:tracker_id, tid)
188 @custom_field_values = nil
186 @custom_field_values = nil
189 result
187 result
190 end
188 end
191
189
192 def project_id=(project_id)
190 def project_id=(project_id)
193 if project_id.to_s != self.project_id.to_s
191 if project_id.to_s != self.project_id.to_s
194 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
192 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
195 end
193 end
196 end
194 end
197
195
198 def project=(project)
196 def project=(project)
199 project_was = self.project
197 project_was = self.project
200 write_attribute(:project_id, project ? project.id : nil)
198 write_attribute(:project_id, project ? project.id : nil)
201 association_instance_set('project', project)
199 association_instance_set('project', project)
202 if project_was && project && project_was != project
200 if project_was && project && project_was != project
203 # Reassign to the category with same name if any
201 # Reassign to the category with same name if any
204 if category
202 if category
205 self.category = project.issue_categories.find_by_name(category.name)
203 self.category = project.issue_categories.find_by_name(category.name)
206 end
204 end
207 # Keep the fixed_version if it's still valid in the new_project
205 # Keep the fixed_version if it's still valid in the new_project
208 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
206 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
209 self.fixed_version = nil
207 self.fixed_version = nil
210 end
208 end
211 if parent && parent.project_id != project_id
209 if parent && parent.project_id != project_id
212 self.parent_issue_id = nil
210 self.parent_issue_id = nil
213 end
211 end
214 @custom_field_values = nil
212 @custom_field_values = nil
215 end
213 end
216 end
214 end
217
215
218 def description=(arg)
216 def description=(arg)
219 if arg.is_a?(String)
217 if arg.is_a?(String)
220 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
218 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
221 end
219 end
222 write_attribute(:description, arg)
220 write_attribute(:description, arg)
223 end
221 end
224
222
225 # Overrides attributes= so that project and tracker get assigned first
223 # Overrides attributes= so that project and tracker get assigned first
226 def attributes_with_project_and_tracker_first=(new_attributes, *args)
224 def attributes_with_project_and_tracker_first=(new_attributes, *args)
227 return if new_attributes.nil?
225 return if new_attributes.nil?
228 attrs = new_attributes.dup
226 attrs = new_attributes.dup
229 attrs.stringify_keys!
227 attrs.stringify_keys!
230
228
231 %w(project project_id tracker tracker_id).each do |attr|
229 %w(project project_id tracker tracker_id).each do |attr|
232 if attrs.has_key?(attr)
230 if attrs.has_key?(attr)
233 send "#{attr}=", attrs.delete(attr)
231 send "#{attr}=", attrs.delete(attr)
234 end
232 end
235 end
233 end
236 send :attributes_without_project_and_tracker_first=, attrs, *args
234 send :attributes_without_project_and_tracker_first=, attrs, *args
237 end
235 end
238 # Do not redefine alias chain on reload (see #4838)
236 # Do not redefine alias chain on reload (see #4838)
239 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
237 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
240
238
241 def estimated_hours=(h)
239 def estimated_hours=(h)
242 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
240 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
243 end
241 end
244
242
245 safe_attributes 'tracker_id',
243 safe_attributes 'tracker_id',
246 'status_id',
244 'status_id',
247 'category_id',
245 'category_id',
248 'assigned_to_id',
246 'assigned_to_id',
249 'priority_id',
247 'priority_id',
250 'fixed_version_id',
248 'fixed_version_id',
251 'subject',
249 'subject',
252 'description',
250 'description',
253 'start_date',
251 'start_date',
254 'due_date',
252 'due_date',
255 'done_ratio',
253 'done_ratio',
256 'estimated_hours',
254 'estimated_hours',
257 'custom_field_values',
255 'custom_field_values',
258 'custom_fields',
256 'custom_fields',
259 'lock_version',
257 'lock_version',
260 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
258 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
261
259
262 safe_attributes 'status_id',
260 safe_attributes 'status_id',
263 'assigned_to_id',
261 'assigned_to_id',
264 'fixed_version_id',
262 'fixed_version_id',
265 'done_ratio',
263 'done_ratio',
266 'lock_version',
264 'lock_version',
267 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
265 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
268
266
269 safe_attributes 'watcher_user_ids',
267 safe_attributes 'watcher_user_ids',
270 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
268 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
271
269
272 safe_attributes 'is_private',
270 safe_attributes 'is_private',
273 :if => lambda {|issue, user|
271 :if => lambda {|issue, user|
274 user.allowed_to?(:set_issues_private, issue.project) ||
272 user.allowed_to?(:set_issues_private, issue.project) ||
275 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
273 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
276 }
274 }
277
275
278 safe_attributes 'parent_issue_id',
276 safe_attributes 'parent_issue_id',
279 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
277 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
280 user.allowed_to?(:manage_subtasks, issue.project)}
278 user.allowed_to?(:manage_subtasks, issue.project)}
281
279
282 # Safely sets attributes
280 # Safely sets attributes
283 # Should be called from controllers instead of #attributes=
281 # Should be called from controllers instead of #attributes=
284 # attr_accessible is too rough because we still want things like
282 # attr_accessible is too rough because we still want things like
285 # Issue.new(:project => foo) to work
283 # Issue.new(:project => foo) to work
286 # TODO: move workflow/permission checks from controllers to here
284 # TODO: move workflow/permission checks from controllers to here
287 def safe_attributes=(attrs, user=User.current)
285 def safe_attributes=(attrs, user=User.current)
288 return unless attrs.is_a?(Hash)
286 return unless attrs.is_a?(Hash)
289
287
290 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
288 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
291 attrs = delete_unsafe_attributes(attrs, user)
289 attrs = delete_unsafe_attributes(attrs, user)
292 return if attrs.empty?
290 return if attrs.empty?
293
291
294 # Tracker must be set before since new_statuses_allowed_to depends on it.
292 # Tracker must be set before since new_statuses_allowed_to depends on it.
295 if t = attrs.delete('tracker_id')
293 if t = attrs.delete('tracker_id')
296 self.tracker_id = t
294 self.tracker_id = t
297 end
295 end
298
296
299 if attrs['status_id']
297 if attrs['status_id']
300 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
298 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
301 attrs.delete('status_id')
299 attrs.delete('status_id')
302 end
300 end
303 end
301 end
304
302
305 unless leaf?
303 unless leaf?
306 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
304 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
307 end
305 end
308
306
309 if attrs['parent_issue_id'].present?
307 if attrs['parent_issue_id'].present?
310 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
308 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
311 end
309 end
312
310
313 # mass-assignment security bypass
311 # mass-assignment security bypass
314 self.send :attributes=, attrs, false
312 self.send :attributes=, attrs, false
315 end
313 end
316
314
317 def done_ratio
315 def done_ratio
318 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
316 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
319 status.default_done_ratio
317 status.default_done_ratio
320 else
318 else
321 read_attribute(:done_ratio)
319 read_attribute(:done_ratio)
322 end
320 end
323 end
321 end
324
322
325 def self.use_status_for_done_ratio?
323 def self.use_status_for_done_ratio?
326 Setting.issue_done_ratio == 'issue_status'
324 Setting.issue_done_ratio == 'issue_status'
327 end
325 end
328
326
329 def self.use_field_for_done_ratio?
327 def self.use_field_for_done_ratio?
330 Setting.issue_done_ratio == 'issue_field'
328 Setting.issue_done_ratio == 'issue_field'
331 end
329 end
332
330
333 def validate_issue
331 def validate_issue
334 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
332 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
335 errors.add :due_date, :not_a_date
333 errors.add :due_date, :not_a_date
336 end
334 end
337
335
338 if self.due_date and self.start_date and self.due_date < self.start_date
336 if self.due_date and self.start_date and self.due_date < self.start_date
339 errors.add :due_date, :greater_than_start_date
337 errors.add :due_date, :greater_than_start_date
340 end
338 end
341
339
342 if start_date && soonest_start && start_date < soonest_start
340 if start_date && soonest_start && start_date < soonest_start
343 errors.add :start_date, :invalid
341 errors.add :start_date, :invalid
344 end
342 end
345
343
346 if fixed_version
344 if fixed_version
347 if !assignable_versions.include?(fixed_version)
345 if !assignable_versions.include?(fixed_version)
348 errors.add :fixed_version_id, :inclusion
346 errors.add :fixed_version_id, :inclusion
349 elsif reopened? && fixed_version.closed?
347 elsif reopened? && fixed_version.closed?
350 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
348 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
351 end
349 end
352 end
350 end
353
351
354 # Checks that the issue can not be added/moved to a disabled tracker
352 # Checks that the issue can not be added/moved to a disabled tracker
355 if project && (tracker_id_changed? || project_id_changed?)
353 if project && (tracker_id_changed? || project_id_changed?)
356 unless project.trackers.include?(tracker)
354 unless project.trackers.include?(tracker)
357 errors.add :tracker_id, :inclusion
355 errors.add :tracker_id, :inclusion
358 end
356 end
359 end
357 end
360
358
361 # Checks parent issue assignment
359 # Checks parent issue assignment
362 if @parent_issue
360 if @parent_issue
363 if @parent_issue.project_id != project_id
361 if @parent_issue.project_id != project_id
364 errors.add :parent_issue_id, :not_same_project
362 errors.add :parent_issue_id, :not_same_project
365 elsif !new_record?
363 elsif !new_record?
366 # moving an existing issue
364 # moving an existing issue
367 if @parent_issue.root_id != root_id
365 if @parent_issue.root_id != root_id
368 # we can always move to another tree
366 # we can always move to another tree
369 elsif move_possible?(@parent_issue)
367 elsif move_possible?(@parent_issue)
370 # move accepted inside tree
368 # move accepted inside tree
371 else
369 else
372 errors.add :parent_issue_id, :not_a_valid_parent
370 errors.add :parent_issue_id, :not_a_valid_parent
373 end
371 end
374 end
372 end
375 end
373 end
376 end
374 end
377
375
378 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
376 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
379 # even if the user turns off the setting later
377 # even if the user turns off the setting later
380 def update_done_ratio_from_issue_status
378 def update_done_ratio_from_issue_status
381 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
379 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
382 self.done_ratio = status.default_done_ratio
380 self.done_ratio = status.default_done_ratio
383 end
381 end
384 end
382 end
385
383
386 def init_journal(user, notes = "")
384 def init_journal(user, notes = "")
387 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
385 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
386 if new_record?
387 @current_journal.notify = false
388 else
388 @attributes_before_change = attributes.dup
389 @attributes_before_change = attributes.dup
389 @custom_values_before_change = {}
390 @custom_values_before_change = {}
390 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
391 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
392 end
391 # Make sure updated_on is updated when adding a note.
393 # Make sure updated_on is updated when adding a note.
392 updated_on_will_change!
394 updated_on_will_change!
393 @current_journal
395 @current_journal
394 end
396 end
395
397
396 # Return true if the issue is closed, otherwise false
398 # Return true if the issue is closed, otherwise false
397 def closed?
399 def closed?
398 self.status.is_closed?
400 self.status.is_closed?
399 end
401 end
400
402
401 # Return true if the issue is being reopened
403 # Return true if the issue is being reopened
402 def reopened?
404 def reopened?
403 if !new_record? && status_id_changed?
405 if !new_record? && status_id_changed?
404 status_was = IssueStatus.find_by_id(status_id_was)
406 status_was = IssueStatus.find_by_id(status_id_was)
405 status_new = IssueStatus.find_by_id(status_id)
407 status_new = IssueStatus.find_by_id(status_id)
406 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
408 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
407 return true
409 return true
408 end
410 end
409 end
411 end
410 false
412 false
411 end
413 end
412
414
413 # Return true if the issue is being closed
415 # Return true if the issue is being closed
414 def closing?
416 def closing?
415 if !new_record? && status_id_changed?
417 if !new_record? && status_id_changed?
416 status_was = IssueStatus.find_by_id(status_id_was)
418 status_was = IssueStatus.find_by_id(status_id_was)
417 status_new = IssueStatus.find_by_id(status_id)
419 status_new = IssueStatus.find_by_id(status_id)
418 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
420 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
419 return true
421 return true
420 end
422 end
421 end
423 end
422 false
424 false
423 end
425 end
424
426
425 # Returns true if the issue is overdue
427 # Returns true if the issue is overdue
426 def overdue?
428 def overdue?
427 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
429 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
428 end
430 end
429
431
430 # Is the amount of work done less than it should for the due date
432 # Is the amount of work done less than it should for the due date
431 def behind_schedule?
433 def behind_schedule?
432 return false if start_date.nil? || due_date.nil?
434 return false if start_date.nil? || due_date.nil?
433 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
435 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
434 return done_date <= Date.today
436 return done_date <= Date.today
435 end
437 end
436
438
437 # Does this issue have children?
439 # Does this issue have children?
438 def children?
440 def children?
439 !leaf?
441 !leaf?
440 end
442 end
441
443
442 # Users the issue can be assigned to
444 # Users the issue can be assigned to
443 def assignable_users
445 def assignable_users
444 users = project.assignable_users
446 users = project.assignable_users
445 users << author if author
447 users << author if author
446 users << assigned_to if assigned_to
448 users << assigned_to if assigned_to
447 users.uniq.sort
449 users.uniq.sort
448 end
450 end
449
451
450 # Versions that the issue can be assigned to
452 # Versions that the issue can be assigned to
451 def assignable_versions
453 def assignable_versions
452 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
454 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
453 end
455 end
454
456
455 # Returns true if this issue is blocked by another issue that is still open
457 # Returns true if this issue is blocked by another issue that is still open
456 def blocked?
458 def blocked?
457 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
459 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
458 end
460 end
459
461
460 # Returns an array of status that user is able to apply
462 # Returns an array of status that user is able to apply
461 def new_statuses_allowed_to(user, include_default=false)
463 def new_statuses_allowed_to(user, include_default=false)
462 statuses = status.find_new_statuses_allowed_to(
464 statuses = status.find_new_statuses_allowed_to(
463 user.roles_for_project(project),
465 user.roles_for_project(project),
464 tracker,
466 tracker,
465 author == user,
467 author == user,
466 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
468 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
467 )
469 )
468 statuses << status unless statuses.empty?
470 statuses << status unless statuses.empty?
469 statuses << IssueStatus.default if include_default
471 statuses << IssueStatus.default if include_default
470 statuses = statuses.uniq.sort
472 statuses = statuses.uniq.sort
471 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
473 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
472 end
474 end
473
475
474 # Returns the mail adresses of users that should be notified
476 # Returns the mail adresses of users that should be notified
475 def recipients
477 def recipients
476 notified = project.notified_users
478 notified = project.notified_users
477 # Author and assignee are always notified unless they have been
479 # Author and assignee are always notified unless they have been
478 # locked or don't want to be notified
480 # locked or don't want to be notified
479 notified << author if author && author.active? && author.notify_about?(self)
481 notified << author if author && author.active? && author.notify_about?(self)
480 if assigned_to
482 if assigned_to
481 if assigned_to.is_a?(Group)
483 if assigned_to.is_a?(Group)
482 notified += assigned_to.users.select {|u| u.active? && u.notify_about?(self)}
484 notified += assigned_to.users.select {|u| u.active? && u.notify_about?(self)}
483 else
485 else
484 notified << assigned_to if assigned_to.active? && assigned_to.notify_about?(self)
486 notified << assigned_to if assigned_to.active? && assigned_to.notify_about?(self)
485 end
487 end
486 end
488 end
487 notified.uniq!
489 notified.uniq!
488 # Remove users that can not view the issue
490 # Remove users that can not view the issue
489 notified.reject! {|user| !visible?(user)}
491 notified.reject! {|user| !visible?(user)}
490 notified.collect(&:mail)
492 notified.collect(&:mail)
491 end
493 end
492
494
493 # Returns the number of hours spent on this issue
495 # Returns the number of hours spent on this issue
494 def spent_hours
496 def spent_hours
495 @spent_hours ||= time_entries.sum(:hours) || 0
497 @spent_hours ||= time_entries.sum(:hours) || 0
496 end
498 end
497
499
498 # Returns the total number of hours spent on this issue and its descendants
500 # Returns the total number of hours spent on this issue and its descendants
499 #
501 #
500 # Example:
502 # Example:
501 # spent_hours => 0.0
503 # spent_hours => 0.0
502 # spent_hours => 50.2
504 # spent_hours => 50.2
503 def total_spent_hours
505 def total_spent_hours
504 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
506 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
505 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
507 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
506 end
508 end
507
509
508 def relations
510 def relations
509 @relations ||= (relations_from + relations_to).sort
511 @relations ||= (relations_from + relations_to).sort
510 end
512 end
511
513
512 # Preloads relations for a collection of issues
514 # Preloads relations for a collection of issues
513 def self.load_relations(issues)
515 def self.load_relations(issues)
514 if issues.any?
516 if issues.any?
515 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
517 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
516 issues.each do |issue|
518 issues.each do |issue|
517 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
519 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
518 end
520 end
519 end
521 end
520 end
522 end
521
523
522 # Preloads visible spent time for a collection of issues
524 # Preloads visible spent time for a collection of issues
523 def self.load_visible_spent_hours(issues, user=User.current)
525 def self.load_visible_spent_hours(issues, user=User.current)
524 if issues.any?
526 if issues.any?
525 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
527 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
526 issues.each do |issue|
528 issues.each do |issue|
527 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
529 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
528 end
530 end
529 end
531 end
530 end
532 end
531
533
532 # Finds an issue relation given its id.
534 # Finds an issue relation given its id.
533 def find_relation(relation_id)
535 def find_relation(relation_id)
534 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
536 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
535 end
537 end
536
538
537 def all_dependent_issues(except=[])
539 def all_dependent_issues(except=[])
538 except << self
540 except << self
539 dependencies = []
541 dependencies = []
540 relations_from.each do |relation|
542 relations_from.each do |relation|
541 if relation.issue_to && !except.include?(relation.issue_to)
543 if relation.issue_to && !except.include?(relation.issue_to)
542 dependencies << relation.issue_to
544 dependencies << relation.issue_to
543 dependencies += relation.issue_to.all_dependent_issues(except)
545 dependencies += relation.issue_to.all_dependent_issues(except)
544 end
546 end
545 end
547 end
546 dependencies
548 dependencies
547 end
549 end
548
550
549 # Returns an array of issues that duplicate this one
551 # Returns an array of issues that duplicate this one
550 def duplicates
552 def duplicates
551 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
553 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
552 end
554 end
553
555
554 # Returns the due date or the target due date if any
556 # Returns the due date or the target due date if any
555 # Used on gantt chart
557 # Used on gantt chart
556 def due_before
558 def due_before
557 due_date || (fixed_version ? fixed_version.effective_date : nil)
559 due_date || (fixed_version ? fixed_version.effective_date : nil)
558 end
560 end
559
561
560 # Returns the time scheduled for this issue.
562 # Returns the time scheduled for this issue.
561 #
563 #
562 # Example:
564 # Example:
563 # Start Date: 2/26/09, End Date: 3/04/09
565 # Start Date: 2/26/09, End Date: 3/04/09
564 # duration => 6
566 # duration => 6
565 def duration
567 def duration
566 (start_date && due_date) ? due_date - start_date : 0
568 (start_date && due_date) ? due_date - start_date : 0
567 end
569 end
568
570
569 def soonest_start
571 def soonest_start
570 @soonest_start ||= (
572 @soonest_start ||= (
571 relations_to.collect{|relation| relation.successor_soonest_start} +
573 relations_to.collect{|relation| relation.successor_soonest_start} +
572 ancestors.collect(&:soonest_start)
574 ancestors.collect(&:soonest_start)
573 ).compact.max
575 ).compact.max
574 end
576 end
575
577
576 def reschedule_after(date)
578 def reschedule_after(date)
577 return if date.nil?
579 return if date.nil?
578 if leaf?
580 if leaf?
579 if start_date.nil? || start_date < date
581 if start_date.nil? || start_date < date
580 self.start_date, self.due_date = date, date + duration
582 self.start_date, self.due_date = date, date + duration
581 save
583 save
582 end
584 end
583 else
585 else
584 leaves.each do |leaf|
586 leaves.each do |leaf|
585 leaf.reschedule_after(date)
587 leaf.reschedule_after(date)
586 end
588 end
587 end
589 end
588 end
590 end
589
591
590 def <=>(issue)
592 def <=>(issue)
591 if issue.nil?
593 if issue.nil?
592 -1
594 -1
593 elsif root_id != issue.root_id
595 elsif root_id != issue.root_id
594 (root_id || 0) <=> (issue.root_id || 0)
596 (root_id || 0) <=> (issue.root_id || 0)
595 else
597 else
596 (lft || 0) <=> (issue.lft || 0)
598 (lft || 0) <=> (issue.lft || 0)
597 end
599 end
598 end
600 end
599
601
600 def to_s
602 def to_s
601 "#{tracker} ##{id}: #{subject}"
603 "#{tracker} ##{id}: #{subject}"
602 end
604 end
603
605
604 # Returns a string of css classes that apply to the issue
606 # Returns a string of css classes that apply to the issue
605 def css_classes
607 def css_classes
606 s = "issue status-#{status.position} priority-#{priority.position}"
608 s = "issue status-#{status.position} priority-#{priority.position}"
607 s << ' closed' if closed?
609 s << ' closed' if closed?
608 s << ' overdue' if overdue?
610 s << ' overdue' if overdue?
609 s << ' child' if child?
611 s << ' child' if child?
610 s << ' parent' unless leaf?
612 s << ' parent' unless leaf?
611 s << ' private' if is_private?
613 s << ' private' if is_private?
612 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
614 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
613 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
615 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
614 s
616 s
615 end
617 end
616
618
617 # Saves an issue, time_entry, attachments, and a journal from the parameters
619 # Saves an issue, time_entry, attachments, and a journal from the parameters
618 # Returns false if save fails
620 # Returns false if save fails
619 def save_issue_with_child_records(params, existing_time_entry=nil)
621 def save_issue_with_child_records(params, existing_time_entry=nil)
620 Issue.transaction do
622 Issue.transaction do
621 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
623 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
622 @time_entry = existing_time_entry || TimeEntry.new
624 @time_entry = existing_time_entry || TimeEntry.new
623 @time_entry.project = project
625 @time_entry.project = project
624 @time_entry.issue = self
626 @time_entry.issue = self
625 @time_entry.user = User.current
627 @time_entry.user = User.current
626 @time_entry.spent_on = User.current.today
628 @time_entry.spent_on = User.current.today
627 @time_entry.attributes = params[:time_entry]
629 @time_entry.attributes = params[:time_entry]
628 self.time_entries << @time_entry
630 self.time_entries << @time_entry
629 end
631 end
630
632
631 if valid?
633 if valid?
632 attachments = Attachment.attach_files(self, params[:attachments])
634 attachments = Attachment.attach_files(self, params[:attachments])
633 # TODO: Rename hook
635 # TODO: Rename hook
634 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
636 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
635 begin
637 begin
636 if save
638 if save
637 # TODO: Rename hook
639 # TODO: Rename hook
638 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
640 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
639 else
641 else
640 raise ActiveRecord::Rollback
642 raise ActiveRecord::Rollback
641 end
643 end
642 rescue ActiveRecord::StaleObjectError
644 rescue ActiveRecord::StaleObjectError
643 attachments[:files].each(&:destroy)
645 attachments[:files].each(&:destroy)
644 errors.add :base, l(:notice_locking_conflict)
646 errors.add :base, l(:notice_locking_conflict)
645 raise ActiveRecord::Rollback
647 raise ActiveRecord::Rollback
646 end
648 end
647 end
649 end
648 end
650 end
649 end
651 end
650
652
651 # Unassigns issues from +version+ if it's no longer shared with issue's project
653 # Unassigns issues from +version+ if it's no longer shared with issue's project
652 def self.update_versions_from_sharing_change(version)
654 def self.update_versions_from_sharing_change(version)
653 # Update issues assigned to the version
655 # Update issues assigned to the version
654 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
656 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
655 end
657 end
656
658
657 # Unassigns issues from versions that are no longer shared
659 # Unassigns issues from versions that are no longer shared
658 # after +project+ was moved
660 # after +project+ was moved
659 def self.update_versions_from_hierarchy_change(project)
661 def self.update_versions_from_hierarchy_change(project)
660 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
662 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
661 # Update issues of the moved projects and issues assigned to a version of a moved project
663 # Update issues of the moved projects and issues assigned to a version of a moved project
662 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
664 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
663 end
665 end
664
666
665 def parent_issue_id=(arg)
667 def parent_issue_id=(arg)
666 parent_issue_id = arg.blank? ? nil : arg.to_i
668 parent_issue_id = arg.blank? ? nil : arg.to_i
667 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
669 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
668 @parent_issue.id
670 @parent_issue.id
669 else
671 else
670 @parent_issue = nil
672 @parent_issue = nil
671 nil
673 nil
672 end
674 end
673 end
675 end
674
676
675 def parent_issue_id
677 def parent_issue_id
676 if instance_variable_defined? :@parent_issue
678 if instance_variable_defined? :@parent_issue
677 @parent_issue.nil? ? nil : @parent_issue.id
679 @parent_issue.nil? ? nil : @parent_issue.id
678 else
680 else
679 parent_id
681 parent_id
680 end
682 end
681 end
683 end
682
684
683 # Extracted from the ReportsController.
685 # Extracted from the ReportsController.
684 def self.by_tracker(project)
686 def self.by_tracker(project)
685 count_and_group_by(:project => project,
687 count_and_group_by(:project => project,
686 :field => 'tracker_id',
688 :field => 'tracker_id',
687 :joins => Tracker.table_name)
689 :joins => Tracker.table_name)
688 end
690 end
689
691
690 def self.by_version(project)
692 def self.by_version(project)
691 count_and_group_by(:project => project,
693 count_and_group_by(:project => project,
692 :field => 'fixed_version_id',
694 :field => 'fixed_version_id',
693 :joins => Version.table_name)
695 :joins => Version.table_name)
694 end
696 end
695
697
696 def self.by_priority(project)
698 def self.by_priority(project)
697 count_and_group_by(:project => project,
699 count_and_group_by(:project => project,
698 :field => 'priority_id',
700 :field => 'priority_id',
699 :joins => IssuePriority.table_name)
701 :joins => IssuePriority.table_name)
700 end
702 end
701
703
702 def self.by_category(project)
704 def self.by_category(project)
703 count_and_group_by(:project => project,
705 count_and_group_by(:project => project,
704 :field => 'category_id',
706 :field => 'category_id',
705 :joins => IssueCategory.table_name)
707 :joins => IssueCategory.table_name)
706 end
708 end
707
709
708 def self.by_assigned_to(project)
710 def self.by_assigned_to(project)
709 count_and_group_by(:project => project,
711 count_and_group_by(:project => project,
710 :field => 'assigned_to_id',
712 :field => 'assigned_to_id',
711 :joins => User.table_name)
713 :joins => User.table_name)
712 end
714 end
713
715
714 def self.by_author(project)
716 def self.by_author(project)
715 count_and_group_by(:project => project,
717 count_and_group_by(:project => project,
716 :field => 'author_id',
718 :field => 'author_id',
717 :joins => User.table_name)
719 :joins => User.table_name)
718 end
720 end
719
721
720 def self.by_subproject(project)
722 def self.by_subproject(project)
721 ActiveRecord::Base.connection.select_all("select s.id as status_id,
723 ActiveRecord::Base.connection.select_all("select s.id as status_id,
722 s.is_closed as closed,
724 s.is_closed as closed,
723 #{Issue.table_name}.project_id as project_id,
725 #{Issue.table_name}.project_id as project_id,
724 count(#{Issue.table_name}.id) as total
726 count(#{Issue.table_name}.id) as total
725 from
727 from
726 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
728 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
727 where
729 where
728 #{Issue.table_name}.status_id=s.id
730 #{Issue.table_name}.status_id=s.id
729 and #{Issue.table_name}.project_id = #{Project.table_name}.id
731 and #{Issue.table_name}.project_id = #{Project.table_name}.id
730 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
732 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
731 and #{Issue.table_name}.project_id <> #{project.id}
733 and #{Issue.table_name}.project_id <> #{project.id}
732 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
734 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
733 end
735 end
734 # End ReportsController extraction
736 # End ReportsController extraction
735
737
736 # Returns an array of projects that current user can move issues to
738 # Returns an array of projects that current user can move issues to
737 def self.allowed_target_projects_on_move
739 def self.allowed_target_projects_on_move
738 projects = []
740 projects = []
739 if User.current.admin?
741 if User.current.admin?
740 # admin is allowed to move issues to any active (visible) project
742 # admin is allowed to move issues to any active (visible) project
741 projects = Project.visible.all
743 projects = Project.visible.all
742 elsif User.current.logged?
744 elsif User.current.logged?
743 if Role.non_member.allowed_to?(:move_issues)
745 if Role.non_member.allowed_to?(:move_issues)
744 projects = Project.visible.all
746 projects = Project.visible.all
745 else
747 else
746 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
748 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
747 end
749 end
748 end
750 end
749 projects
751 projects
750 end
752 end
751
753
752 private
754 private
753
755
754 def after_project_change
756 def after_project_change
755 # Update project_id on related time entries
757 # Update project_id on related time entries
756 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
758 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
757
759
758 # Delete issue relations
760 # Delete issue relations
759 unless Setting.cross_project_issue_relations?
761 unless Setting.cross_project_issue_relations?
760 relations_from.clear
762 relations_from.clear
761 relations_to.clear
763 relations_to.clear
762 end
764 end
763
765
764 # Move subtasks
766 # Move subtasks
765 children.each do |child|
767 children.each do |child|
766 child.project = project
768 child.project = project
767 unless child.save
769 unless child.save
768 raise ActiveRecord::Rollback
770 raise ActiveRecord::Rollback
769 end
771 end
770 end
772 end
771 end
773 end
772
774
773 def update_nested_set_attributes
775 def update_nested_set_attributes
774 if root_id.nil?
776 if root_id.nil?
775 # issue was just created
777 # issue was just created
776 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
778 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
777 set_default_left_and_right
779 set_default_left_and_right
778 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
780 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
779 if @parent_issue
781 if @parent_issue
780 move_to_child_of(@parent_issue)
782 move_to_child_of(@parent_issue)
781 end
783 end
782 reload
784 reload
783 elsif parent_issue_id != parent_id
785 elsif parent_issue_id != parent_id
784 former_parent_id = parent_id
786 former_parent_id = parent_id
785 # moving an existing issue
787 # moving an existing issue
786 if @parent_issue && @parent_issue.root_id == root_id
788 if @parent_issue && @parent_issue.root_id == root_id
787 # inside the same tree
789 # inside the same tree
788 move_to_child_of(@parent_issue)
790 move_to_child_of(@parent_issue)
789 else
791 else
790 # to another tree
792 # to another tree
791 unless root?
793 unless root?
792 move_to_right_of(root)
794 move_to_right_of(root)
793 reload
795 reload
794 end
796 end
795 old_root_id = root_id
797 old_root_id = root_id
796 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
798 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
797 target_maxright = nested_set_scope.maximum(right_column_name) || 0
799 target_maxright = nested_set_scope.maximum(right_column_name) || 0
798 offset = target_maxright + 1 - lft
800 offset = target_maxright + 1 - lft
799 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
801 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
800 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
802 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
801 self[left_column_name] = lft + offset
803 self[left_column_name] = lft + offset
802 self[right_column_name] = rgt + offset
804 self[right_column_name] = rgt + offset
803 if @parent_issue
805 if @parent_issue
804 move_to_child_of(@parent_issue)
806 move_to_child_of(@parent_issue)
805 end
807 end
806 end
808 end
807 reload
809 reload
808 # delete invalid relations of all descendants
810 # delete invalid relations of all descendants
809 self_and_descendants.each do |issue|
811 self_and_descendants.each do |issue|
810 issue.relations.each do |relation|
812 issue.relations.each do |relation|
811 relation.destroy unless relation.valid?
813 relation.destroy unless relation.valid?
812 end
814 end
813 end
815 end
814 # update former parent
816 # update former parent
815 recalculate_attributes_for(former_parent_id) if former_parent_id
817 recalculate_attributes_for(former_parent_id) if former_parent_id
816 end
818 end
817 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
819 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
818 end
820 end
819
821
820 def update_parent_attributes
822 def update_parent_attributes
821 recalculate_attributes_for(parent_id) if parent_id
823 recalculate_attributes_for(parent_id) if parent_id
822 end
824 end
823
825
824 def recalculate_attributes_for(issue_id)
826 def recalculate_attributes_for(issue_id)
825 if issue_id && p = Issue.find_by_id(issue_id)
827 if issue_id && p = Issue.find_by_id(issue_id)
826 # priority = highest priority of children
828 # priority = highest priority of children
827 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
829 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
828 p.priority = IssuePriority.find_by_position(priority_position)
830 p.priority = IssuePriority.find_by_position(priority_position)
829 end
831 end
830
832
831 # start/due dates = lowest/highest dates of children
833 # start/due dates = lowest/highest dates of children
832 p.start_date = p.children.minimum(:start_date)
834 p.start_date = p.children.minimum(:start_date)
833 p.due_date = p.children.maximum(:due_date)
835 p.due_date = p.children.maximum(:due_date)
834 if p.start_date && p.due_date && p.due_date < p.start_date
836 if p.start_date && p.due_date && p.due_date < p.start_date
835 p.start_date, p.due_date = p.due_date, p.start_date
837 p.start_date, p.due_date = p.due_date, p.start_date
836 end
838 end
837
839
838 # done ratio = weighted average ratio of leaves
840 # done ratio = weighted average ratio of leaves
839 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
841 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
840 leaves_count = p.leaves.count
842 leaves_count = p.leaves.count
841 if leaves_count > 0
843 if leaves_count > 0
842 average = p.leaves.average(:estimated_hours).to_f
844 average = p.leaves.average(:estimated_hours).to_f
843 if average == 0
845 if average == 0
844 average = 1
846 average = 1
845 end
847 end
846 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
848 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
847 progress = done / (average * leaves_count)
849 progress = done / (average * leaves_count)
848 p.done_ratio = progress.round
850 p.done_ratio = progress.round
849 end
851 end
850 end
852 end
851
853
852 # estimate = sum of leaves estimates
854 # estimate = sum of leaves estimates
853 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
855 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
854 p.estimated_hours = nil if p.estimated_hours == 0.0
856 p.estimated_hours = nil if p.estimated_hours == 0.0
855
857
856 # ancestors will be recursively updated
858 # ancestors will be recursively updated
857 p.save(false)
859 p.save(false)
858 end
860 end
859 end
861 end
860
862
861 # Update issues so their versions are not pointing to a
863 # Update issues so their versions are not pointing to a
862 # fixed_version that is not shared with the issue's project
864 # fixed_version that is not shared with the issue's project
863 def self.update_versions(conditions=nil)
865 def self.update_versions(conditions=nil)
864 # Only need to update issues with a fixed_version from
866 # Only need to update issues with a fixed_version from
865 # a different project and that is not systemwide shared
867 # a different project and that is not systemwide shared
866 Issue.scoped(:conditions => conditions).all(
868 Issue.scoped(:conditions => conditions).all(
867 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
869 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
868 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
870 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
869 " AND #{Version.table_name}.sharing <> 'system'",
871 " AND #{Version.table_name}.sharing <> 'system'",
870 :include => [:project, :fixed_version]
872 :include => [:project, :fixed_version]
871 ).each do |issue|
873 ).each do |issue|
872 next if issue.project.nil? || issue.fixed_version.nil?
874 next if issue.project.nil? || issue.fixed_version.nil?
873 unless issue.project.shared_versions.include?(issue.fixed_version)
875 unless issue.project.shared_versions.include?(issue.fixed_version)
874 issue.init_journal(User.current)
876 issue.init_journal(User.current)
875 issue.fixed_version = nil
877 issue.fixed_version = nil
876 issue.save
878 issue.save
877 end
879 end
878 end
880 end
879 end
881 end
880
882
881 # Callback on attachment deletion
883 # Callback on attachment deletion
882 def attachment_added(obj)
884 def attachment_added(obj)
883 if @current_journal && !obj.new_record?
885 if @current_journal && !obj.new_record?
884 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
886 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
885 end
887 end
886 end
888 end
887
889
888 # Callback on attachment deletion
890 # Callback on attachment deletion
889 def attachment_removed(obj)
891 def attachment_removed(obj)
890 journal = init_journal(User.current)
892 journal = init_journal(User.current)
891 journal.details << JournalDetail.new(:property => 'attachment',
893 journal.details << JournalDetail.new(:property => 'attachment',
892 :prop_key => obj.id,
894 :prop_key => obj.id,
893 :old_value => obj.filename)
895 :old_value => obj.filename)
894 journal.save
896 journal.save
895 end
897 end
896
898
897 # Default assignment based on category
899 # Default assignment based on category
898 def default_assign
900 def default_assign
899 if assigned_to.nil? && category && category.assigned_to
901 if assigned_to.nil? && category && category.assigned_to
900 self.assigned_to = category.assigned_to
902 self.assigned_to = category.assigned_to
901 end
903 end
902 end
904 end
903
905
904 # Updates start/due dates of following issues
906 # Updates start/due dates of following issues
905 def reschedule_following_issues
907 def reschedule_following_issues
906 if start_date_changed? || due_date_changed?
908 if start_date_changed? || due_date_changed?
907 relations_from.each do |relation|
909 relations_from.each do |relation|
908 relation.set_issue_to_dates
910 relation.set_issue_to_dates
909 end
911 end
910 end
912 end
911 end
913 end
912
914
913 # Closes duplicates if the issue is being closed
915 # Closes duplicates if the issue is being closed
914 def close_duplicates
916 def close_duplicates
915 if closing?
917 if closing?
916 duplicates.each do |duplicate|
918 duplicates.each do |duplicate|
917 # Reload is need in case the duplicate was updated by a previous duplicate
919 # Reload is need in case the duplicate was updated by a previous duplicate
918 duplicate.reload
920 duplicate.reload
919 # Don't re-close it if it's already closed
921 # Don't re-close it if it's already closed
920 next if duplicate.closed?
922 next if duplicate.closed?
921 # Same user and notes
923 # Same user and notes
922 if @current_journal
924 if @current_journal
923 duplicate.init_journal(@current_journal.user, @current_journal.notes)
925 duplicate.init_journal(@current_journal.user, @current_journal.notes)
924 end
926 end
925 duplicate.update_attribute :status, self.status
927 duplicate.update_attribute :status, self.status
926 end
928 end
927 end
929 end
928 end
930 end
929
931
930 # Saves the changes in a Journal
932 # Saves the changes in a Journal
931 # Called after_save
933 # Called after_save
932 def create_journal
934 def create_journal
933 if @current_journal
935 if @current_journal
934 # attributes changes
936 # attributes changes
937 if @attributes_before_change
935 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
938 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
936 before = @attributes_before_change[c]
939 before = @attributes_before_change[c]
937 after = send(c)
940 after = send(c)
938 next if before == after || (before.blank? && after.blank?)
941 next if before == after || (before.blank? && after.blank?)
939 @current_journal.details << JournalDetail.new(:property => 'attr',
942 @current_journal.details << JournalDetail.new(:property => 'attr',
940 :prop_key => c,
943 :prop_key => c,
941 :old_value => before,
944 :old_value => before,
942 :value => after)
945 :value => after)
943 }
946 }
947 end
948 if @custom_values_before_change
944 # custom fields changes
949 # custom fields changes
945 custom_values.each {|c|
950 custom_values.each {|c|
946 before = @custom_values_before_change[c.custom_field_id]
951 before = @custom_values_before_change[c.custom_field_id]
947 after = c.value
952 after = c.value
948 next if before == after || (before.blank? && after.blank?)
953 next if before == after || (before.blank? && after.blank?)
949 @current_journal.details << JournalDetail.new(:property => 'cf',
954 @current_journal.details << JournalDetail.new(:property => 'cf',
950 :prop_key => c.custom_field_id,
955 :prop_key => c.custom_field_id,
951 :old_value => before,
956 :old_value => before,
952 :value => after)
957 :value => after)
953 }
958 }
959 end
954 @current_journal.save
960 @current_journal.save
955 # reset current journal
961 # reset current journal
956 init_journal @current_journal.user, @current_journal.notes
962 init_journal @current_journal.user, @current_journal.notes
957 end
963 end
958 end
964 end
959
965
960 # Query generator for selecting groups of issue counts for a project
966 # Query generator for selecting groups of issue counts for a project
961 # based on specific criteria
967 # based on specific criteria
962 #
968 #
963 # Options
969 # Options
964 # * project - Project to search in.
970 # * project - Project to search in.
965 # * field - String. Issue field to key off of in the grouping.
971 # * field - String. Issue field to key off of in the grouping.
966 # * joins - String. The table name to join against.
972 # * joins - String. The table name to join against.
967 def self.count_and_group_by(options)
973 def self.count_and_group_by(options)
968 project = options.delete(:project)
974 project = options.delete(:project)
969 select_field = options.delete(:field)
975 select_field = options.delete(:field)
970 joins = options.delete(:joins)
976 joins = options.delete(:joins)
971
977
972 where = "#{Issue.table_name}.#{select_field}=j.id"
978 where = "#{Issue.table_name}.#{select_field}=j.id"
973
979
974 ActiveRecord::Base.connection.select_all("select s.id as status_id,
980 ActiveRecord::Base.connection.select_all("select s.id as status_id,
975 s.is_closed as closed,
981 s.is_closed as closed,
976 j.id as #{select_field},
982 j.id as #{select_field},
977 count(#{Issue.table_name}.id) as total
983 count(#{Issue.table_name}.id) as total
978 from
984 from
979 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
985 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
980 where
986 where
981 #{Issue.table_name}.status_id=s.id
987 #{Issue.table_name}.status_id=s.id
982 and #{where}
988 and #{where}
983 and #{Issue.table_name}.project_id=#{Project.table_name}.id
989 and #{Issue.table_name}.project_id=#{Project.table_name}.id
984 and #{visible_condition(User.current, :project => project)}
990 and #{visible_condition(User.current, :project => project)}
985 group by s.id, s.is_closed, j.id")
991 group by s.id, s.is_closed, j.id")
986 end
992 end
987 end
993 end
General Comments 0
You need to be logged in to leave comments. Login now