##// END OF EJS Templates
Save queries....
Jean-Philippe Lang -
r5125:af968bfb2264
parent child
Show More
@@ -1,883 +1,883
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 => 'User', :foreign_key => 'assigned_to_id'
25 belongs_to :assigned_to, :class_name => 'User', :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_remove => :attachment_removed
38 acts_as_attachable :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
61
62 named_scope :visible, lambda {|*args| { :include => :project,
62 named_scope :visible, lambda {|*args| { :include => :project,
63 :conditions => Issue.visible_condition(args.first || User.current) } }
63 :conditions => Issue.visible_condition(args.first || User.current) } }
64
64
65 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
65 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
66
66
67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
68 named_scope :with_limit, lambda { |limit| { :limit => limit} }
68 named_scope :with_limit, lambda { |limit| { :limit => limit} }
69 named_scope :on_active_project, :include => [:status, :project, :tracker],
69 named_scope :on_active_project, :include => [:status, :project, :tracker],
70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
71
71
72 named_scope :without_version, lambda {
72 named_scope :without_version, lambda {
73 {
73 {
74 :conditions => { :fixed_version_id => nil}
74 :conditions => { :fixed_version_id => nil}
75 }
75 }
76 }
76 }
77
77
78 named_scope :with_query, lambda {|query|
78 named_scope :with_query, lambda {|query|
79 {
79 {
80 :conditions => Query.merge_conditions(query.statement)
80 :conditions => Query.merge_conditions(query.statement)
81 }
81 }
82 }
82 }
83
83
84 before_create :default_assign
84 before_create :default_assign
85 before_save :close_duplicates, :update_done_ratio_from_issue_status
85 before_save :close_duplicates, :update_done_ratio_from_issue_status
86 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
86 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
87 after_destroy :update_parent_attributes
87 after_destroy :update_parent_attributes
88
88
89 # Returns a SQL conditions string used to find all issues visible by the specified user
89 # Returns a SQL conditions string used to find all issues visible by the specified user
90 def self.visible_condition(user, options={})
90 def self.visible_condition(user, options={})
91 Project.allowed_to_condition(user, :view_issues, options)
91 Project.allowed_to_condition(user, :view_issues, options)
92 end
92 end
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 & tracker.custom_fields.all) : []
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 'tracker_id',
218 safe_attributes 'tracker_id',
219 'status_id',
219 'status_id',
220 'parent_issue_id',
220 'parent_issue_id',
221 'category_id',
221 'category_id',
222 'assigned_to_id',
222 'assigned_to_id',
223 'priority_id',
223 'priority_id',
224 'fixed_version_id',
224 'fixed_version_id',
225 'subject',
225 'subject',
226 'description',
226 'description',
227 'start_date',
227 'start_date',
228 'due_date',
228 'due_date',
229 'done_ratio',
229 'done_ratio',
230 'estimated_hours',
230 'estimated_hours',
231 'custom_field_values',
231 'custom_field_values',
232 'custom_fields',
232 'custom_fields',
233 'lock_version',
233 'lock_version',
234 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
234 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
235
235
236 safe_attributes 'status_id',
236 safe_attributes 'status_id',
237 'assigned_to_id',
237 'assigned_to_id',
238 'fixed_version_id',
238 'fixed_version_id',
239 'done_ratio',
239 'done_ratio',
240 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
240 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
241
241
242 # Safely sets attributes
242 # Safely sets attributes
243 # Should be called from controllers instead of #attributes=
243 # Should be called from controllers instead of #attributes=
244 # attr_accessible is too rough because we still want things like
244 # attr_accessible is too rough because we still want things like
245 # Issue.new(:project => foo) to work
245 # Issue.new(:project => foo) to work
246 # TODO: move workflow/permission checks from controllers to here
246 # TODO: move workflow/permission checks from controllers to here
247 def safe_attributes=(attrs, user=User.current)
247 def safe_attributes=(attrs, user=User.current)
248 return unless attrs.is_a?(Hash)
248 return unless attrs.is_a?(Hash)
249
249
250 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
250 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
251 attrs = delete_unsafe_attributes(attrs, user)
251 attrs = delete_unsafe_attributes(attrs, user)
252 return if attrs.empty?
252 return if attrs.empty?
253
253
254 # Tracker must be set before since new_statuses_allowed_to depends on it.
254 # Tracker must be set before since new_statuses_allowed_to depends on it.
255 if t = attrs.delete('tracker_id')
255 if t = attrs.delete('tracker_id')
256 self.tracker_id = t
256 self.tracker_id = t
257 end
257 end
258
258
259 if attrs['status_id']
259 if attrs['status_id']
260 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
260 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
261 attrs.delete('status_id')
261 attrs.delete('status_id')
262 end
262 end
263 end
263 end
264
264
265 unless leaf?
265 unless leaf?
266 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
266 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
267 end
267 end
268
268
269 if attrs.has_key?('parent_issue_id')
269 if attrs.has_key?('parent_issue_id')
270 if !user.allowed_to?(:manage_subtasks, project)
270 if !user.allowed_to?(:manage_subtasks, project)
271 attrs.delete('parent_issue_id')
271 attrs.delete('parent_issue_id')
272 elsif !attrs['parent_issue_id'].blank?
272 elsif !attrs['parent_issue_id'].blank?
273 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
273 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
274 end
274 end
275 end
275 end
276
276
277 self.attributes = attrs
277 self.attributes = attrs
278 end
278 end
279
279
280 def done_ratio
280 def done_ratio
281 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
281 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
282 status.default_done_ratio
282 status.default_done_ratio
283 else
283 else
284 read_attribute(:done_ratio)
284 read_attribute(:done_ratio)
285 end
285 end
286 end
286 end
287
287
288 def self.use_status_for_done_ratio?
288 def self.use_status_for_done_ratio?
289 Setting.issue_done_ratio == 'issue_status'
289 Setting.issue_done_ratio == 'issue_status'
290 end
290 end
291
291
292 def self.use_field_for_done_ratio?
292 def self.use_field_for_done_ratio?
293 Setting.issue_done_ratio == 'issue_field'
293 Setting.issue_done_ratio == 'issue_field'
294 end
294 end
295
295
296 def validate
296 def validate
297 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
297 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
298 errors.add :due_date, :not_a_date
298 errors.add :due_date, :not_a_date
299 end
299 end
300
300
301 if self.due_date and self.start_date and self.due_date < self.start_date
301 if self.due_date and self.start_date and self.due_date < self.start_date
302 errors.add :due_date, :greater_than_start_date
302 errors.add :due_date, :greater_than_start_date
303 end
303 end
304
304
305 if start_date && soonest_start && start_date < soonest_start
305 if start_date && soonest_start && start_date < soonest_start
306 errors.add :start_date, :invalid
306 errors.add :start_date, :invalid
307 end
307 end
308
308
309 if fixed_version
309 if fixed_version
310 if !assignable_versions.include?(fixed_version)
310 if !assignable_versions.include?(fixed_version)
311 errors.add :fixed_version_id, :inclusion
311 errors.add :fixed_version_id, :inclusion
312 elsif reopened? && fixed_version.closed?
312 elsif reopened? && fixed_version.closed?
313 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
313 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
314 end
314 end
315 end
315 end
316
316
317 # Checks that the issue can not be added/moved to a disabled tracker
317 # Checks that the issue can not be added/moved to a disabled tracker
318 if project && (tracker_id_changed? || project_id_changed?)
318 if project && (tracker_id_changed? || project_id_changed?)
319 unless project.trackers.include?(tracker)
319 unless project.trackers.include?(tracker)
320 errors.add :tracker_id, :inclusion
320 errors.add :tracker_id, :inclusion
321 end
321 end
322 end
322 end
323
323
324 # Checks parent issue assignment
324 # Checks parent issue assignment
325 if @parent_issue
325 if @parent_issue
326 if @parent_issue.project_id != project_id
326 if @parent_issue.project_id != project_id
327 errors.add :parent_issue_id, :not_same_project
327 errors.add :parent_issue_id, :not_same_project
328 elsif !new_record?
328 elsif !new_record?
329 # moving an existing issue
329 # moving an existing issue
330 if @parent_issue.root_id != root_id
330 if @parent_issue.root_id != root_id
331 # we can always move to another tree
331 # we can always move to another tree
332 elsif move_possible?(@parent_issue)
332 elsif move_possible?(@parent_issue)
333 # move accepted inside tree
333 # move accepted inside tree
334 else
334 else
335 errors.add :parent_issue_id, :not_a_valid_parent
335 errors.add :parent_issue_id, :not_a_valid_parent
336 end
336 end
337 end
337 end
338 end
338 end
339 end
339 end
340
340
341 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
341 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
342 # even if the user turns off the setting later
342 # even if the user turns off the setting later
343 def update_done_ratio_from_issue_status
343 def update_done_ratio_from_issue_status
344 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
344 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
345 self.done_ratio = status.default_done_ratio
345 self.done_ratio = status.default_done_ratio
346 end
346 end
347 end
347 end
348
348
349 def init_journal(user, notes = "")
349 def init_journal(user, notes = "")
350 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
350 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
351 @issue_before_change = self.clone
351 @issue_before_change = self.clone
352 @issue_before_change.status = self.status
352 @issue_before_change.status = self.status
353 @custom_values_before_change = {}
353 @custom_values_before_change = {}
354 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
354 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
355 # Make sure updated_on is updated when adding a note.
355 # Make sure updated_on is updated when adding a note.
356 updated_on_will_change!
356 updated_on_will_change!
357 @current_journal
357 @current_journal
358 end
358 end
359
359
360 # Return true if the issue is closed, otherwise false
360 # Return true if the issue is closed, otherwise false
361 def closed?
361 def closed?
362 self.status.is_closed?
362 self.status.is_closed?
363 end
363 end
364
364
365 # Return true if the issue is being reopened
365 # Return true if the issue is being reopened
366 def reopened?
366 def reopened?
367 if !new_record? && status_id_changed?
367 if !new_record? && status_id_changed?
368 status_was = IssueStatus.find_by_id(status_id_was)
368 status_was = IssueStatus.find_by_id(status_id_was)
369 status_new = IssueStatus.find_by_id(status_id)
369 status_new = IssueStatus.find_by_id(status_id)
370 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
370 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
371 return true
371 return true
372 end
372 end
373 end
373 end
374 false
374 false
375 end
375 end
376
376
377 # Return true if the issue is being closed
377 # Return true if the issue is being closed
378 def closing?
378 def closing?
379 if !new_record? && status_id_changed?
379 if !new_record? && status_id_changed?
380 status_was = IssueStatus.find_by_id(status_id_was)
380 status_was = IssueStatus.find_by_id(status_id_was)
381 status_new = IssueStatus.find_by_id(status_id)
381 status_new = IssueStatus.find_by_id(status_id)
382 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
382 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
383 return true
383 return true
384 end
384 end
385 end
385 end
386 false
386 false
387 end
387 end
388
388
389 # Returns true if the issue is overdue
389 # Returns true if the issue is overdue
390 def overdue?
390 def overdue?
391 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
391 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
392 end
392 end
393
393
394 # Is the amount of work done less than it should for the due date
394 # Is the amount of work done less than it should for the due date
395 def behind_schedule?
395 def behind_schedule?
396 return false if start_date.nil? || due_date.nil?
396 return false if start_date.nil? || due_date.nil?
397 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
397 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
398 return done_date <= Date.today
398 return done_date <= Date.today
399 end
399 end
400
400
401 # Does this issue have children?
401 # Does this issue have children?
402 def children?
402 def children?
403 !leaf?
403 !leaf?
404 end
404 end
405
405
406 # Users the issue can be assigned to
406 # Users the issue can be assigned to
407 def assignable_users
407 def assignable_users
408 users = project.assignable_users
408 users = project.assignable_users
409 users << author if author
409 users << author if author
410 users.uniq.sort
410 users.uniq.sort
411 end
411 end
412
412
413 # Versions that the issue can be assigned to
413 # Versions that the issue can be assigned to
414 def assignable_versions
414 def assignable_versions
415 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
415 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
416 end
416 end
417
417
418 # Returns true if this issue is blocked by another issue that is still open
418 # Returns true if this issue is blocked by another issue that is still open
419 def blocked?
419 def blocked?
420 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
420 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
421 end
421 end
422
422
423 # Returns an array of status that user is able to apply
423 # Returns an array of status that user is able to apply
424 def new_statuses_allowed_to(user, include_default=false)
424 def new_statuses_allowed_to(user, include_default=false)
425 statuses = status.find_new_statuses_allowed_to(
425 statuses = status.find_new_statuses_allowed_to(
426 user.roles_for_project(project),
426 user.roles_for_project(project),
427 tracker,
427 tracker,
428 author == user,
428 author == user,
429 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
429 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
430 )
430 )
431 statuses << status unless statuses.empty?
431 statuses << status unless statuses.empty?
432 statuses << IssueStatus.default if include_default
432 statuses << IssueStatus.default if include_default
433 statuses = statuses.uniq.sort
433 statuses = statuses.uniq.sort
434 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
434 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
435 end
435 end
436
436
437 # Returns the mail adresses of users that should be notified
437 # Returns the mail adresses of users that should be notified
438 def recipients
438 def recipients
439 notified = project.notified_users
439 notified = project.notified_users
440 # Author and assignee are always notified unless they have been
440 # Author and assignee are always notified unless they have been
441 # locked or don't want to be notified
441 # locked or don't want to be notified
442 notified << author if author && author.active? && author.notify_about?(self)
442 notified << author if author && author.active? && author.notify_about?(self)
443 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
443 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
444 notified.uniq!
444 notified.uniq!
445 # Remove users that can not view the issue
445 # Remove users that can not view the issue
446 notified.reject! {|user| !visible?(user)}
446 notified.reject! {|user| !visible?(user)}
447 notified.collect(&:mail)
447 notified.collect(&:mail)
448 end
448 end
449
449
450 # Returns the total number of hours spent on this issue and its descendants
450 # Returns the total number of hours spent on this issue and its descendants
451 #
451 #
452 # Example:
452 # Example:
453 # spent_hours => 0.0
453 # spent_hours => 0.0
454 # spent_hours => 50.2
454 # spent_hours => 50.2
455 def spent_hours
455 def spent_hours
456 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
456 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
457 end
457 end
458
458
459 def relations
459 def relations
460 (relations_from + relations_to).sort
460 (relations_from + relations_to).sort
461 end
461 end
462
462
463 def all_dependent_issues(except=[])
463 def all_dependent_issues(except=[])
464 except << self
464 except << self
465 dependencies = []
465 dependencies = []
466 relations_from.each do |relation|
466 relations_from.each do |relation|
467 if relation.issue_to && !except.include?(relation.issue_to)
467 if relation.issue_to && !except.include?(relation.issue_to)
468 dependencies << relation.issue_to
468 dependencies << relation.issue_to
469 dependencies += relation.issue_to.all_dependent_issues(except)
469 dependencies += relation.issue_to.all_dependent_issues(except)
470 end
470 end
471 end
471 end
472 dependencies
472 dependencies
473 end
473 end
474
474
475 # Returns an array of issues that duplicate this one
475 # Returns an array of issues that duplicate this one
476 def duplicates
476 def duplicates
477 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
477 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
478 end
478 end
479
479
480 # Returns the due date or the target due date if any
480 # Returns the due date or the target due date if any
481 # Used on gantt chart
481 # Used on gantt chart
482 def due_before
482 def due_before
483 due_date || (fixed_version ? fixed_version.effective_date : nil)
483 due_date || (fixed_version ? fixed_version.effective_date : nil)
484 end
484 end
485
485
486 # Returns the time scheduled for this issue.
486 # Returns the time scheduled for this issue.
487 #
487 #
488 # Example:
488 # Example:
489 # Start Date: 2/26/09, End Date: 3/04/09
489 # Start Date: 2/26/09, End Date: 3/04/09
490 # duration => 6
490 # duration => 6
491 def duration
491 def duration
492 (start_date && due_date) ? due_date - start_date : 0
492 (start_date && due_date) ? due_date - start_date : 0
493 end
493 end
494
494
495 def soonest_start
495 def soonest_start
496 @soonest_start ||= (
496 @soonest_start ||= (
497 relations_to.collect{|relation| relation.successor_soonest_start} +
497 relations_to.collect{|relation| relation.successor_soonest_start} +
498 ancestors.collect(&:soonest_start)
498 ancestors.collect(&:soonest_start)
499 ).compact.max
499 ).compact.max
500 end
500 end
501
501
502 def reschedule_after(date)
502 def reschedule_after(date)
503 return if date.nil?
503 return if date.nil?
504 if leaf?
504 if leaf?
505 if start_date.nil? || start_date < date
505 if start_date.nil? || start_date < date
506 self.start_date, self.due_date = date, date + duration
506 self.start_date, self.due_date = date, date + duration
507 save
507 save
508 end
508 end
509 else
509 else
510 leaves.each do |leaf|
510 leaves.each do |leaf|
511 leaf.reschedule_after(date)
511 leaf.reschedule_after(date)
512 end
512 end
513 end
513 end
514 end
514 end
515
515
516 def <=>(issue)
516 def <=>(issue)
517 if issue.nil?
517 if issue.nil?
518 -1
518 -1
519 elsif root_id != issue.root_id
519 elsif root_id != issue.root_id
520 (root_id || 0) <=> (issue.root_id || 0)
520 (root_id || 0) <=> (issue.root_id || 0)
521 else
521 else
522 (lft || 0) <=> (issue.lft || 0)
522 (lft || 0) <=> (issue.lft || 0)
523 end
523 end
524 end
524 end
525
525
526 def to_s
526 def to_s
527 "#{tracker} ##{id}: #{subject}"
527 "#{tracker} ##{id}: #{subject}"
528 end
528 end
529
529
530 # Returns a string of css classes that apply to the issue
530 # Returns a string of css classes that apply to the issue
531 def css_classes
531 def css_classes
532 s = "issue status-#{status.position} priority-#{priority.position}"
532 s = "issue status-#{status.position} priority-#{priority.position}"
533 s << ' closed' if closed?
533 s << ' closed' if closed?
534 s << ' overdue' if overdue?
534 s << ' overdue' if overdue?
535 s << ' child' if child?
535 s << ' child' if child?
536 s << ' parent' unless leaf?
536 s << ' parent' unless leaf?
537 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
537 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
538 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
538 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
539 s
539 s
540 end
540 end
541
541
542 # Saves an issue, time_entry, attachments, and a journal from the parameters
542 # Saves an issue, time_entry, attachments, and a journal from the parameters
543 # Returns false if save fails
543 # Returns false if save fails
544 def save_issue_with_child_records(params, existing_time_entry=nil)
544 def save_issue_with_child_records(params, existing_time_entry=nil)
545 Issue.transaction do
545 Issue.transaction do
546 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
546 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
547 @time_entry = existing_time_entry || TimeEntry.new
547 @time_entry = existing_time_entry || TimeEntry.new
548 @time_entry.project = project
548 @time_entry.project = project
549 @time_entry.issue = self
549 @time_entry.issue = self
550 @time_entry.user = User.current
550 @time_entry.user = User.current
551 @time_entry.spent_on = Date.today
551 @time_entry.spent_on = Date.today
552 @time_entry.attributes = params[:time_entry]
552 @time_entry.attributes = params[:time_entry]
553 self.time_entries << @time_entry
553 self.time_entries << @time_entry
554 end
554 end
555
555
556 if valid?
556 if valid?
557 attachments = Attachment.attach_files(self, params[:attachments])
557 attachments = Attachment.attach_files(self, params[:attachments])
558
558
559 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
559 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
560 # TODO: Rename hook
560 # TODO: Rename hook
561 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
561 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
562 begin
562 begin
563 if save
563 if save
564 # TODO: Rename hook
564 # TODO: Rename hook
565 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
565 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
566 else
566 else
567 raise ActiveRecord::Rollback
567 raise ActiveRecord::Rollback
568 end
568 end
569 rescue ActiveRecord::StaleObjectError
569 rescue ActiveRecord::StaleObjectError
570 attachments[:files].each(&:destroy)
570 attachments[:files].each(&:destroy)
571 errors.add_to_base l(:notice_locking_conflict)
571 errors.add_to_base l(:notice_locking_conflict)
572 raise ActiveRecord::Rollback
572 raise ActiveRecord::Rollback
573 end
573 end
574 end
574 end
575 end
575 end
576 end
576 end
577
577
578 # Unassigns issues from +version+ if it's no longer shared with issue's project
578 # Unassigns issues from +version+ if it's no longer shared with issue's project
579 def self.update_versions_from_sharing_change(version)
579 def self.update_versions_from_sharing_change(version)
580 # Update issues assigned to the version
580 # Update issues assigned to the version
581 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
581 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
582 end
582 end
583
583
584 # Unassigns issues from versions that are no longer shared
584 # Unassigns issues from versions that are no longer shared
585 # after +project+ was moved
585 # after +project+ was moved
586 def self.update_versions_from_hierarchy_change(project)
586 def self.update_versions_from_hierarchy_change(project)
587 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
587 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
588 # Update issues of the moved projects and issues assigned to a version of a moved project
588 # Update issues of the moved projects and issues assigned to a version of a moved project
589 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
589 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
590 end
590 end
591
591
592 def parent_issue_id=(arg)
592 def parent_issue_id=(arg)
593 parent_issue_id = arg.blank? ? nil : arg.to_i
593 parent_issue_id = arg.blank? ? nil : arg.to_i
594 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
594 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
595 @parent_issue.id
595 @parent_issue.id
596 else
596 else
597 @parent_issue = nil
597 @parent_issue = nil
598 nil
598 nil
599 end
599 end
600 end
600 end
601
601
602 def parent_issue_id
602 def parent_issue_id
603 if instance_variable_defined? :@parent_issue
603 if instance_variable_defined? :@parent_issue
604 @parent_issue.nil? ? nil : @parent_issue.id
604 @parent_issue.nil? ? nil : @parent_issue.id
605 else
605 else
606 parent_id
606 parent_id
607 end
607 end
608 end
608 end
609
609
610 # Extracted from the ReportsController.
610 # Extracted from the ReportsController.
611 def self.by_tracker(project)
611 def self.by_tracker(project)
612 count_and_group_by(:project => project,
612 count_and_group_by(:project => project,
613 :field => 'tracker_id',
613 :field => 'tracker_id',
614 :joins => Tracker.table_name)
614 :joins => Tracker.table_name)
615 end
615 end
616
616
617 def self.by_version(project)
617 def self.by_version(project)
618 count_and_group_by(:project => project,
618 count_and_group_by(:project => project,
619 :field => 'fixed_version_id',
619 :field => 'fixed_version_id',
620 :joins => Version.table_name)
620 :joins => Version.table_name)
621 end
621 end
622
622
623 def self.by_priority(project)
623 def self.by_priority(project)
624 count_and_group_by(:project => project,
624 count_and_group_by(:project => project,
625 :field => 'priority_id',
625 :field => 'priority_id',
626 :joins => IssuePriority.table_name)
626 :joins => IssuePriority.table_name)
627 end
627 end
628
628
629 def self.by_category(project)
629 def self.by_category(project)
630 count_and_group_by(:project => project,
630 count_and_group_by(:project => project,
631 :field => 'category_id',
631 :field => 'category_id',
632 :joins => IssueCategory.table_name)
632 :joins => IssueCategory.table_name)
633 end
633 end
634
634
635 def self.by_assigned_to(project)
635 def self.by_assigned_to(project)
636 count_and_group_by(:project => project,
636 count_and_group_by(:project => project,
637 :field => 'assigned_to_id',
637 :field => 'assigned_to_id',
638 :joins => User.table_name)
638 :joins => User.table_name)
639 end
639 end
640
640
641 def self.by_author(project)
641 def self.by_author(project)
642 count_and_group_by(:project => project,
642 count_and_group_by(:project => project,
643 :field => 'author_id',
643 :field => 'author_id',
644 :joins => User.table_name)
644 :joins => User.table_name)
645 end
645 end
646
646
647 def self.by_subproject(project)
647 def self.by_subproject(project)
648 ActiveRecord::Base.connection.select_all("select s.id as status_id,
648 ActiveRecord::Base.connection.select_all("select s.id as status_id,
649 s.is_closed as closed,
649 s.is_closed as closed,
650 i.project_id as project_id,
650 i.project_id as project_id,
651 count(i.id) as total
651 count(i.id) as total
652 from
652 from
653 #{Issue.table_name} i, #{IssueStatus.table_name} s
653 #{Issue.table_name} i, #{IssueStatus.table_name} s
654 where
654 where
655 i.status_id=s.id
655 i.status_id=s.id
656 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
656 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
657 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
657 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
658 end
658 end
659 # End ReportsController extraction
659 # End ReportsController extraction
660
660
661 # Returns an array of projects that current user can move issues to
661 # Returns an array of projects that current user can move issues to
662 def self.allowed_target_projects_on_move
662 def self.allowed_target_projects_on_move
663 projects = []
663 projects = []
664 if User.current.admin?
664 if User.current.admin?
665 # admin is allowed to move issues to any active (visible) project
665 # admin is allowed to move issues to any active (visible) project
666 projects = Project.visible.all
666 projects = Project.visible.all
667 elsif User.current.logged?
667 elsif User.current.logged?
668 if Role.non_member.allowed_to?(:move_issues)
668 if Role.non_member.allowed_to?(:move_issues)
669 projects = Project.visible.all
669 projects = Project.visible.all
670 else
670 else
671 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
671 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
672 end
672 end
673 end
673 end
674 projects
674 projects
675 end
675 end
676
676
677 private
677 private
678
678
679 def update_nested_set_attributes
679 def update_nested_set_attributes
680 if root_id.nil?
680 if root_id.nil?
681 # issue was just created
681 # issue was just created
682 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
682 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
683 set_default_left_and_right
683 set_default_left_and_right
684 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
684 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
685 if @parent_issue
685 if @parent_issue
686 move_to_child_of(@parent_issue)
686 move_to_child_of(@parent_issue)
687 end
687 end
688 reload
688 reload
689 elsif parent_issue_id != parent_id
689 elsif parent_issue_id != parent_id
690 former_parent_id = parent_id
690 former_parent_id = parent_id
691 # moving an existing issue
691 # moving an existing issue
692 if @parent_issue && @parent_issue.root_id == root_id
692 if @parent_issue && @parent_issue.root_id == root_id
693 # inside the same tree
693 # inside the same tree
694 move_to_child_of(@parent_issue)
694 move_to_child_of(@parent_issue)
695 else
695 else
696 # to another tree
696 # to another tree
697 unless root?
697 unless root?
698 move_to_right_of(root)
698 move_to_right_of(root)
699 reload
699 reload
700 end
700 end
701 old_root_id = root_id
701 old_root_id = root_id
702 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
702 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
703 target_maxright = nested_set_scope.maximum(right_column_name) || 0
703 target_maxright = nested_set_scope.maximum(right_column_name) || 0
704 offset = target_maxright + 1 - lft
704 offset = target_maxright + 1 - lft
705 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
705 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
706 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
706 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
707 self[left_column_name] = lft + offset
707 self[left_column_name] = lft + offset
708 self[right_column_name] = rgt + offset
708 self[right_column_name] = rgt + offset
709 if @parent_issue
709 if @parent_issue
710 move_to_child_of(@parent_issue)
710 move_to_child_of(@parent_issue)
711 end
711 end
712 end
712 end
713 reload
713 reload
714 # delete invalid relations of all descendants
714 # delete invalid relations of all descendants
715 self_and_descendants.each do |issue|
715 self_and_descendants.each do |issue|
716 issue.relations.each do |relation|
716 issue.relations.each do |relation|
717 relation.destroy unless relation.valid?
717 relation.destroy unless relation.valid?
718 end
718 end
719 end
719 end
720 # update former parent
720 # update former parent
721 recalculate_attributes_for(former_parent_id) if former_parent_id
721 recalculate_attributes_for(former_parent_id) if former_parent_id
722 end
722 end
723 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
723 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
724 end
724 end
725
725
726 def update_parent_attributes
726 def update_parent_attributes
727 recalculate_attributes_for(parent_id) if parent_id
727 recalculate_attributes_for(parent_id) if parent_id
728 end
728 end
729
729
730 def recalculate_attributes_for(issue_id)
730 def recalculate_attributes_for(issue_id)
731 if issue_id && p = Issue.find_by_id(issue_id)
731 if issue_id && p = Issue.find_by_id(issue_id)
732 # priority = highest priority of children
732 # priority = highest priority of children
733 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
733 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
734 p.priority = IssuePriority.find_by_position(priority_position)
734 p.priority = IssuePriority.find_by_position(priority_position)
735 end
735 end
736
736
737 # start/due dates = lowest/highest dates of children
737 # start/due dates = lowest/highest dates of children
738 p.start_date = p.children.minimum(:start_date)
738 p.start_date = p.children.minimum(:start_date)
739 p.due_date = p.children.maximum(:due_date)
739 p.due_date = p.children.maximum(:due_date)
740 if p.start_date && p.due_date && p.due_date < p.start_date
740 if p.start_date && p.due_date && p.due_date < p.start_date
741 p.start_date, p.due_date = p.due_date, p.start_date
741 p.start_date, p.due_date = p.due_date, p.start_date
742 end
742 end
743
743
744 # done ratio = weighted average ratio of leaves
744 # done ratio = weighted average ratio of leaves
745 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
745 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
746 leaves_count = p.leaves.count
746 leaves_count = p.leaves.count
747 if leaves_count > 0
747 if leaves_count > 0
748 average = p.leaves.average(:estimated_hours).to_f
748 average = p.leaves.average(:estimated_hours).to_f
749 if average == 0
749 if average == 0
750 average = 1
750 average = 1
751 end
751 end
752 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
752 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
753 progress = done / (average * leaves_count)
753 progress = done / (average * leaves_count)
754 p.done_ratio = progress.round
754 p.done_ratio = progress.round
755 end
755 end
756 end
756 end
757
757
758 # estimate = sum of leaves estimates
758 # estimate = sum of leaves estimates
759 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
759 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
760 p.estimated_hours = nil if p.estimated_hours == 0.0
760 p.estimated_hours = nil if p.estimated_hours == 0.0
761
761
762 # ancestors will be recursively updated
762 # ancestors will be recursively updated
763 p.save(false)
763 p.save(false)
764 end
764 end
765 end
765 end
766
766
767 # Update issues so their versions are not pointing to a
767 # Update issues so their versions are not pointing to a
768 # fixed_version that is not shared with the issue's project
768 # fixed_version that is not shared with the issue's project
769 def self.update_versions(conditions=nil)
769 def self.update_versions(conditions=nil)
770 # Only need to update issues with a fixed_version from
770 # Only need to update issues with a fixed_version from
771 # a different project and that is not systemwide shared
771 # a different project and that is not systemwide shared
772 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
772 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
773 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
773 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
774 " AND #{Version.table_name}.sharing <> 'system'",
774 " AND #{Version.table_name}.sharing <> 'system'",
775 conditions),
775 conditions),
776 :include => [:project, :fixed_version]
776 :include => [:project, :fixed_version]
777 ).each do |issue|
777 ).each do |issue|
778 next if issue.project.nil? || issue.fixed_version.nil?
778 next if issue.project.nil? || issue.fixed_version.nil?
779 unless issue.project.shared_versions.include?(issue.fixed_version)
779 unless issue.project.shared_versions.include?(issue.fixed_version)
780 issue.init_journal(User.current)
780 issue.init_journal(User.current)
781 issue.fixed_version = nil
781 issue.fixed_version = nil
782 issue.save
782 issue.save
783 end
783 end
784 end
784 end
785 end
785 end
786
786
787 # Callback on attachment deletion
787 # Callback on attachment deletion
788 def attachment_removed(obj)
788 def attachment_removed(obj)
789 journal = init_journal(User.current)
789 journal = init_journal(User.current)
790 journal.details << JournalDetail.new(:property => 'attachment',
790 journal.details << JournalDetail.new(:property => 'attachment',
791 :prop_key => obj.id,
791 :prop_key => obj.id,
792 :old_value => obj.filename)
792 :old_value => obj.filename)
793 journal.save
793 journal.save
794 end
794 end
795
795
796 # Default assignment based on category
796 # Default assignment based on category
797 def default_assign
797 def default_assign
798 if assigned_to.nil? && category && category.assigned_to
798 if assigned_to.nil? && category && category.assigned_to
799 self.assigned_to = category.assigned_to
799 self.assigned_to = category.assigned_to
800 end
800 end
801 end
801 end
802
802
803 # Updates start/due dates of following issues
803 # Updates start/due dates of following issues
804 def reschedule_following_issues
804 def reschedule_following_issues
805 if start_date_changed? || due_date_changed?
805 if start_date_changed? || due_date_changed?
806 relations_from.each do |relation|
806 relations_from.each do |relation|
807 relation.set_issue_to_dates
807 relation.set_issue_to_dates
808 end
808 end
809 end
809 end
810 end
810 end
811
811
812 # Closes duplicates if the issue is being closed
812 # Closes duplicates if the issue is being closed
813 def close_duplicates
813 def close_duplicates
814 if closing?
814 if closing?
815 duplicates.each do |duplicate|
815 duplicates.each do |duplicate|
816 # Reload is need in case the duplicate was updated by a previous duplicate
816 # Reload is need in case the duplicate was updated by a previous duplicate
817 duplicate.reload
817 duplicate.reload
818 # Don't re-close it if it's already closed
818 # Don't re-close it if it's already closed
819 next if duplicate.closed?
819 next if duplicate.closed?
820 # Same user and notes
820 # Same user and notes
821 if @current_journal
821 if @current_journal
822 duplicate.init_journal(@current_journal.user, @current_journal.notes)
822 duplicate.init_journal(@current_journal.user, @current_journal.notes)
823 end
823 end
824 duplicate.update_attribute :status, self.status
824 duplicate.update_attribute :status, self.status
825 end
825 end
826 end
826 end
827 end
827 end
828
828
829 # Saves the changes in a Journal
829 # Saves the changes in a Journal
830 # Called after_save
830 # Called after_save
831 def create_journal
831 def create_journal
832 if @current_journal
832 if @current_journal
833 # attributes changes
833 # attributes changes
834 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
834 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
835 @current_journal.details << JournalDetail.new(:property => 'attr',
835 @current_journal.details << JournalDetail.new(:property => 'attr',
836 :prop_key => c,
836 :prop_key => c,
837 :old_value => @issue_before_change.send(c),
837 :old_value => @issue_before_change.send(c),
838 :value => send(c)) unless send(c)==@issue_before_change.send(c)
838 :value => send(c)) unless send(c)==@issue_before_change.send(c)
839 }
839 }
840 # custom fields changes
840 # custom fields changes
841 custom_values.each {|c|
841 custom_values.each {|c|
842 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
842 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
843 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
843 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
844 @current_journal.details << JournalDetail.new(:property => 'cf',
844 @current_journal.details << JournalDetail.new(:property => 'cf',
845 :prop_key => c.custom_field_id,
845 :prop_key => c.custom_field_id,
846 :old_value => @custom_values_before_change[c.custom_field_id],
846 :old_value => @custom_values_before_change[c.custom_field_id],
847 :value => c.value)
847 :value => c.value)
848 }
848 }
849 @current_journal.save
849 @current_journal.save
850 # reset current journal
850 # reset current journal
851 init_journal @current_journal.user, @current_journal.notes
851 init_journal @current_journal.user, @current_journal.notes
852 end
852 end
853 end
853 end
854
854
855 # Query generator for selecting groups of issue counts for a project
855 # Query generator for selecting groups of issue counts for a project
856 # based on specific criteria
856 # based on specific criteria
857 #
857 #
858 # Options
858 # Options
859 # * project - Project to search in.
859 # * project - Project to search in.
860 # * field - String. Issue field to key off of in the grouping.
860 # * field - String. Issue field to key off of in the grouping.
861 # * joins - String. The table name to join against.
861 # * joins - String. The table name to join against.
862 def self.count_and_group_by(options)
862 def self.count_and_group_by(options)
863 project = options.delete(:project)
863 project = options.delete(:project)
864 select_field = options.delete(:field)
864 select_field = options.delete(:field)
865 joins = options.delete(:joins)
865 joins = options.delete(:joins)
866
866
867 where = "i.#{select_field}=j.id"
867 where = "i.#{select_field}=j.id"
868
868
869 ActiveRecord::Base.connection.select_all("select s.id as status_id,
869 ActiveRecord::Base.connection.select_all("select s.id as status_id,
870 s.is_closed as closed,
870 s.is_closed as closed,
871 j.id as #{select_field},
871 j.id as #{select_field},
872 count(i.id) as total
872 count(i.id) as total
873 from
873 from
874 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
874 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
875 where
875 where
876 i.status_id=s.id
876 i.status_id=s.id
877 and #{where}
877 and #{where}
878 and i.project_id=#{project.id}
878 and i.project_id=#{project.id}
879 group by s.id, s.is_closed, j.id")
879 group by s.id, s.is_closed, j.id")
880 end
880 end
881
881
882
882
883 end
883 end
General Comments 0
You need to be logged in to leave comments. Login now