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