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