##// END OF EJS Templates
Adds css classes to parent/child issues (#7986)....
Jean-Philippe Lang -
r5101:437c5658d732
parent child
Show More
@@ -1,881 +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.select {|c| tracker.custom_fields.include? c } : []
110 end
110 end
111
111
112 def copy_from(arg)
112 def copy_from(arg)
113 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
113 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
114 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
114 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
115 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
115 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
116 self.status = issue.status
116 self.status = issue.status
117 self
117 self
118 end
118 end
119
119
120 # Moves/copies an issue to a new project and tracker
120 # Moves/copies an issue to a new project and tracker
121 # Returns the moved/copied issue on success, false on failure
121 # Returns the moved/copied issue on success, false on failure
122 def move_to_project(*args)
122 def move_to_project(*args)
123 ret = Issue.transaction do
123 ret = Issue.transaction do
124 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
124 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
125 end || false
125 end || false
126 end
126 end
127
127
128 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
128 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
129 options ||= {}
129 options ||= {}
130 issue = options[:copy] ? self.class.new.copy_from(self) : self
130 issue = options[:copy] ? self.class.new.copy_from(self) : self
131
131
132 if new_project && issue.project_id != new_project.id
132 if new_project && issue.project_id != new_project.id
133 # delete issue relations
133 # delete issue relations
134 unless Setting.cross_project_issue_relations?
134 unless Setting.cross_project_issue_relations?
135 issue.relations_from.clear
135 issue.relations_from.clear
136 issue.relations_to.clear
136 issue.relations_to.clear
137 end
137 end
138 # issue is moved to another project
138 # issue is moved to another project
139 # reassign to the category with same name if any
139 # reassign to the category with same name if any
140 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
140 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
141 issue.category = new_category
141 issue.category = new_category
142 # Keep the fixed_version if it's still valid in the new_project
142 # Keep the fixed_version if it's still valid in the new_project
143 unless new_project.shared_versions.include?(issue.fixed_version)
143 unless new_project.shared_versions.include?(issue.fixed_version)
144 issue.fixed_version = nil
144 issue.fixed_version = nil
145 end
145 end
146 issue.project = new_project
146 issue.project = new_project
147 if issue.parent && issue.parent.project_id != issue.project_id
147 if issue.parent && issue.parent.project_id != issue.project_id
148 issue.parent_issue_id = nil
148 issue.parent_issue_id = nil
149 end
149 end
150 end
150 end
151 if new_tracker
151 if new_tracker
152 issue.tracker = new_tracker
152 issue.tracker = new_tracker
153 issue.reset_custom_values!
153 issue.reset_custom_values!
154 end
154 end
155 if options[:copy]
155 if options[:copy]
156 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
156 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
157 issue.status = if options[:attributes] && options[:attributes][:status_id]
157 issue.status = if options[:attributes] && options[:attributes][:status_id]
158 IssueStatus.find_by_id(options[:attributes][:status_id])
158 IssueStatus.find_by_id(options[:attributes][:status_id])
159 else
159 else
160 self.status
160 self.status
161 end
161 end
162 end
162 end
163 # Allow bulk setting of attributes on the issue
163 # Allow bulk setting of attributes on the issue
164 if options[:attributes]
164 if options[:attributes]
165 issue.attributes = options[:attributes]
165 issue.attributes = options[:attributes]
166 end
166 end
167 if issue.save
167 if issue.save
168 unless options[:copy]
168 unless options[:copy]
169 # Manually update project_id on related time entries
169 # Manually update project_id on related time entries
170 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
170 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
171
171
172 issue.children.each do |child|
172 issue.children.each do |child|
173 unless child.move_to_project_without_transaction(new_project)
173 unless child.move_to_project_without_transaction(new_project)
174 # Move failed and transaction was rollback'd
174 # Move failed and transaction was rollback'd
175 return false
175 return false
176 end
176 end
177 end
177 end
178 end
178 end
179 else
179 else
180 return false
180 return false
181 end
181 end
182 issue
182 issue
183 end
183 end
184
184
185 def status_id=(sid)
185 def status_id=(sid)
186 self.status = nil
186 self.status = nil
187 write_attribute(:status_id, sid)
187 write_attribute(:status_id, sid)
188 end
188 end
189
189
190 def priority_id=(pid)
190 def priority_id=(pid)
191 self.priority = nil
191 self.priority = nil
192 write_attribute(:priority_id, pid)
192 write_attribute(:priority_id, pid)
193 end
193 end
194
194
195 def tracker_id=(tid)
195 def tracker_id=(tid)
196 self.tracker = nil
196 self.tracker = nil
197 result = write_attribute(:tracker_id, tid)
197 result = write_attribute(:tracker_id, tid)
198 @custom_field_values = nil
198 @custom_field_values = nil
199 result
199 result
200 end
200 end
201
201
202 # Overrides attributes= so that tracker_id gets assigned first
202 # Overrides attributes= so that tracker_id gets assigned first
203 def attributes_with_tracker_first=(new_attributes, *args)
203 def attributes_with_tracker_first=(new_attributes, *args)
204 return if new_attributes.nil?
204 return if new_attributes.nil?
205 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
205 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
206 if new_tracker_id
206 if new_tracker_id
207 self.tracker_id = new_tracker_id
207 self.tracker_id = new_tracker_id
208 end
208 end
209 send :attributes_without_tracker_first=, new_attributes, *args
209 send :attributes_without_tracker_first=, new_attributes, *args
210 end
210 end
211 # Do not redefine alias chain on reload (see #4838)
211 # Do not redefine alias chain on reload (see #4838)
212 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
212 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
213
213
214 def estimated_hours=(h)
214 def estimated_hours=(h)
215 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
215 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
216 end
216 end
217
217
218 safe_attributes '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?
536 s << ' parent' unless leaf?
535 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
536 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
537 s
539 s
538 end
540 end
539
541
540 # 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
541 # Returns false if save fails
543 # Returns false if save fails
542 def save_issue_with_child_records(params, existing_time_entry=nil)
544 def save_issue_with_child_records(params, existing_time_entry=nil)
543 Issue.transaction do
545 Issue.transaction do
544 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)
545 @time_entry = existing_time_entry || TimeEntry.new
547 @time_entry = existing_time_entry || TimeEntry.new
546 @time_entry.project = project
548 @time_entry.project = project
547 @time_entry.issue = self
549 @time_entry.issue = self
548 @time_entry.user = User.current
550 @time_entry.user = User.current
549 @time_entry.spent_on = Date.today
551 @time_entry.spent_on = Date.today
550 @time_entry.attributes = params[:time_entry]
552 @time_entry.attributes = params[:time_entry]
551 self.time_entries << @time_entry
553 self.time_entries << @time_entry
552 end
554 end
553
555
554 if valid?
556 if valid?
555 attachments = Attachment.attach_files(self, params[:attachments])
557 attachments = Attachment.attach_files(self, params[:attachments])
556
558
557 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)}
558 # TODO: Rename hook
560 # TODO: Rename hook
559 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})
560 begin
562 begin
561 if save
563 if save
562 # TODO: Rename hook
564 # TODO: Rename hook
563 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})
564 else
566 else
565 raise ActiveRecord::Rollback
567 raise ActiveRecord::Rollback
566 end
568 end
567 rescue ActiveRecord::StaleObjectError
569 rescue ActiveRecord::StaleObjectError
568 attachments[:files].each(&:destroy)
570 attachments[:files].each(&:destroy)
569 errors.add_to_base l(:notice_locking_conflict)
571 errors.add_to_base l(:notice_locking_conflict)
570 raise ActiveRecord::Rollback
572 raise ActiveRecord::Rollback
571 end
573 end
572 end
574 end
573 end
575 end
574 end
576 end
575
577
576 # 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
577 def self.update_versions_from_sharing_change(version)
579 def self.update_versions_from_sharing_change(version)
578 # Update issues assigned to the version
580 # Update issues assigned to the version
579 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
581 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
580 end
582 end
581
583
582 # Unassigns issues from versions that are no longer shared
584 # Unassigns issues from versions that are no longer shared
583 # after +project+ was moved
585 # after +project+ was moved
584 def self.update_versions_from_hierarchy_change(project)
586 def self.update_versions_from_hierarchy_change(project)
585 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
587 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
586 # 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
587 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])
588 end
590 end
589
591
590 def parent_issue_id=(arg)
592 def parent_issue_id=(arg)
591 parent_issue_id = arg.blank? ? nil : arg.to_i
593 parent_issue_id = arg.blank? ? nil : arg.to_i
592 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)
593 @parent_issue.id
595 @parent_issue.id
594 else
596 else
595 @parent_issue = nil
597 @parent_issue = nil
596 nil
598 nil
597 end
599 end
598 end
600 end
599
601
600 def parent_issue_id
602 def parent_issue_id
601 if instance_variable_defined? :@parent_issue
603 if instance_variable_defined? :@parent_issue
602 @parent_issue.nil? ? nil : @parent_issue.id
604 @parent_issue.nil? ? nil : @parent_issue.id
603 else
605 else
604 parent_id
606 parent_id
605 end
607 end
606 end
608 end
607
609
608 # Extracted from the ReportsController.
610 # Extracted from the ReportsController.
609 def self.by_tracker(project)
611 def self.by_tracker(project)
610 count_and_group_by(:project => project,
612 count_and_group_by(:project => project,
611 :field => 'tracker_id',
613 :field => 'tracker_id',
612 :joins => Tracker.table_name)
614 :joins => Tracker.table_name)
613 end
615 end
614
616
615 def self.by_version(project)
617 def self.by_version(project)
616 count_and_group_by(:project => project,
618 count_and_group_by(:project => project,
617 :field => 'fixed_version_id',
619 :field => 'fixed_version_id',
618 :joins => Version.table_name)
620 :joins => Version.table_name)
619 end
621 end
620
622
621 def self.by_priority(project)
623 def self.by_priority(project)
622 count_and_group_by(:project => project,
624 count_and_group_by(:project => project,
623 :field => 'priority_id',
625 :field => 'priority_id',
624 :joins => IssuePriority.table_name)
626 :joins => IssuePriority.table_name)
625 end
627 end
626
628
627 def self.by_category(project)
629 def self.by_category(project)
628 count_and_group_by(:project => project,
630 count_and_group_by(:project => project,
629 :field => 'category_id',
631 :field => 'category_id',
630 :joins => IssueCategory.table_name)
632 :joins => IssueCategory.table_name)
631 end
633 end
632
634
633 def self.by_assigned_to(project)
635 def self.by_assigned_to(project)
634 count_and_group_by(:project => project,
636 count_and_group_by(:project => project,
635 :field => 'assigned_to_id',
637 :field => 'assigned_to_id',
636 :joins => User.table_name)
638 :joins => User.table_name)
637 end
639 end
638
640
639 def self.by_author(project)
641 def self.by_author(project)
640 count_and_group_by(:project => project,
642 count_and_group_by(:project => project,
641 :field => 'author_id',
643 :field => 'author_id',
642 :joins => User.table_name)
644 :joins => User.table_name)
643 end
645 end
644
646
645 def self.by_subproject(project)
647 def self.by_subproject(project)
646 ActiveRecord::Base.connection.select_all("select s.id as status_id,
648 ActiveRecord::Base.connection.select_all("select s.id as status_id,
647 s.is_closed as closed,
649 s.is_closed as closed,
648 i.project_id as project_id,
650 i.project_id as project_id,
649 count(i.id) as total
651 count(i.id) as total
650 from
652 from
651 #{Issue.table_name} i, #{IssueStatus.table_name} s
653 #{Issue.table_name} i, #{IssueStatus.table_name} s
652 where
654 where
653 i.status_id=s.id
655 i.status_id=s.id
654 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(',')})
655 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?
656 end
658 end
657 # End ReportsController extraction
659 # End ReportsController extraction
658
660
659 # 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
660 def self.allowed_target_projects_on_move
662 def self.allowed_target_projects_on_move
661 projects = []
663 projects = []
662 if User.current.admin?
664 if User.current.admin?
663 # admin is allowed to move issues to any active (visible) project
665 # admin is allowed to move issues to any active (visible) project
664 projects = Project.visible.all
666 projects = Project.visible.all
665 elsif User.current.logged?
667 elsif User.current.logged?
666 if Role.non_member.allowed_to?(:move_issues)
668 if Role.non_member.allowed_to?(:move_issues)
667 projects = Project.visible.all
669 projects = Project.visible.all
668 else
670 else
669 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)}}
670 end
672 end
671 end
673 end
672 projects
674 projects
673 end
675 end
674
676
675 private
677 private
676
678
677 def update_nested_set_attributes
679 def update_nested_set_attributes
678 if root_id.nil?
680 if root_id.nil?
679 # issue was just created
681 # issue was just created
680 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
682 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
681 set_default_left_and_right
683 set_default_left_and_right
682 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])
683 if @parent_issue
685 if @parent_issue
684 move_to_child_of(@parent_issue)
686 move_to_child_of(@parent_issue)
685 end
687 end
686 reload
688 reload
687 elsif parent_issue_id != parent_id
689 elsif parent_issue_id != parent_id
688 former_parent_id = parent_id
690 former_parent_id = parent_id
689 # moving an existing issue
691 # moving an existing issue
690 if @parent_issue && @parent_issue.root_id == root_id
692 if @parent_issue && @parent_issue.root_id == root_id
691 # inside the same tree
693 # inside the same tree
692 move_to_child_of(@parent_issue)
694 move_to_child_of(@parent_issue)
693 else
695 else
694 # to another tree
696 # to another tree
695 unless root?
697 unless root?
696 move_to_right_of(root)
698 move_to_right_of(root)
697 reload
699 reload
698 end
700 end
699 old_root_id = root_id
701 old_root_id = root_id
700 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
702 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
701 target_maxright = nested_set_scope.maximum(right_column_name) || 0
703 target_maxright = nested_set_scope.maximum(right_column_name) || 0
702 offset = target_maxright + 1 - lft
704 offset = target_maxright + 1 - lft
703 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}",
704 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
706 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
705 self[left_column_name] = lft + offset
707 self[left_column_name] = lft + offset
706 self[right_column_name] = rgt + offset
708 self[right_column_name] = rgt + offset
707 if @parent_issue
709 if @parent_issue
708 move_to_child_of(@parent_issue)
710 move_to_child_of(@parent_issue)
709 end
711 end
710 end
712 end
711 reload
713 reload
712 # delete invalid relations of all descendants
714 # delete invalid relations of all descendants
713 self_and_descendants.each do |issue|
715 self_and_descendants.each do |issue|
714 issue.relations.each do |relation|
716 issue.relations.each do |relation|
715 relation.destroy unless relation.valid?
717 relation.destroy unless relation.valid?
716 end
718 end
717 end
719 end
718 # update former parent
720 # update former parent
719 recalculate_attributes_for(former_parent_id) if former_parent_id
721 recalculate_attributes_for(former_parent_id) if former_parent_id
720 end
722 end
721 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
723 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
722 end
724 end
723
725
724 def update_parent_attributes
726 def update_parent_attributes
725 recalculate_attributes_for(parent_id) if parent_id
727 recalculate_attributes_for(parent_id) if parent_id
726 end
728 end
727
729
728 def recalculate_attributes_for(issue_id)
730 def recalculate_attributes_for(issue_id)
729 if issue_id && p = Issue.find_by_id(issue_id)
731 if issue_id && p = Issue.find_by_id(issue_id)
730 # priority = highest priority of children
732 # priority = highest priority of children
731 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)
732 p.priority = IssuePriority.find_by_position(priority_position)
734 p.priority = IssuePriority.find_by_position(priority_position)
733 end
735 end
734
736
735 # start/due dates = lowest/highest dates of children
737 # start/due dates = lowest/highest dates of children
736 p.start_date = p.children.minimum(:start_date)
738 p.start_date = p.children.minimum(:start_date)
737 p.due_date = p.children.maximum(:due_date)
739 p.due_date = p.children.maximum(:due_date)
738 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
739 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
740 end
742 end
741
743
742 # done ratio = weighted average ratio of leaves
744 # done ratio = weighted average ratio of leaves
743 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
744 leaves_count = p.leaves.count
746 leaves_count = p.leaves.count
745 if leaves_count > 0
747 if leaves_count > 0
746 average = p.leaves.average(:estimated_hours).to_f
748 average = p.leaves.average(:estimated_hours).to_f
747 if average == 0
749 if average == 0
748 average = 1
750 average = 1
749 end
751 end
750 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
751 progress = done / (average * leaves_count)
753 progress = done / (average * leaves_count)
752 p.done_ratio = progress.round
754 p.done_ratio = progress.round
753 end
755 end
754 end
756 end
755
757
756 # estimate = sum of leaves estimates
758 # estimate = sum of leaves estimates
757 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
759 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
758 p.estimated_hours = nil if p.estimated_hours == 0.0
760 p.estimated_hours = nil if p.estimated_hours == 0.0
759
761
760 # ancestors will be recursively updated
762 # ancestors will be recursively updated
761 p.save(false)
763 p.save(false)
762 end
764 end
763 end
765 end
764
766
765 # Update issues so their versions are not pointing to a
767 # Update issues so their versions are not pointing to a
766 # fixed_version that is not shared with the issue's project
768 # fixed_version that is not shared with the issue's project
767 def self.update_versions(conditions=nil)
769 def self.update_versions(conditions=nil)
768 # Only need to update issues with a fixed_version from
770 # Only need to update issues with a fixed_version from
769 # a different project and that is not systemwide shared
771 # a different project and that is not systemwide shared
770 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" +
771 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
773 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
772 " AND #{Version.table_name}.sharing <> 'system'",
774 " AND #{Version.table_name}.sharing <> 'system'",
773 conditions),
775 conditions),
774 :include => [:project, :fixed_version]
776 :include => [:project, :fixed_version]
775 ).each do |issue|
777 ).each do |issue|
776 next if issue.project.nil? || issue.fixed_version.nil?
778 next if issue.project.nil? || issue.fixed_version.nil?
777 unless issue.project.shared_versions.include?(issue.fixed_version)
779 unless issue.project.shared_versions.include?(issue.fixed_version)
778 issue.init_journal(User.current)
780 issue.init_journal(User.current)
779 issue.fixed_version = nil
781 issue.fixed_version = nil
780 issue.save
782 issue.save
781 end
783 end
782 end
784 end
783 end
785 end
784
786
785 # Callback on attachment deletion
787 # Callback on attachment deletion
786 def attachment_removed(obj)
788 def attachment_removed(obj)
787 journal = init_journal(User.current)
789 journal = init_journal(User.current)
788 journal.details << JournalDetail.new(:property => 'attachment',
790 journal.details << JournalDetail.new(:property => 'attachment',
789 :prop_key => obj.id,
791 :prop_key => obj.id,
790 :old_value => obj.filename)
792 :old_value => obj.filename)
791 journal.save
793 journal.save
792 end
794 end
793
795
794 # Default assignment based on category
796 # Default assignment based on category
795 def default_assign
797 def default_assign
796 if assigned_to.nil? && category && category.assigned_to
798 if assigned_to.nil? && category && category.assigned_to
797 self.assigned_to = category.assigned_to
799 self.assigned_to = category.assigned_to
798 end
800 end
799 end
801 end
800
802
801 # Updates start/due dates of following issues
803 # Updates start/due dates of following issues
802 def reschedule_following_issues
804 def reschedule_following_issues
803 if start_date_changed? || due_date_changed?
805 if start_date_changed? || due_date_changed?
804 relations_from.each do |relation|
806 relations_from.each do |relation|
805 relation.set_issue_to_dates
807 relation.set_issue_to_dates
806 end
808 end
807 end
809 end
808 end
810 end
809
811
810 # Closes duplicates if the issue is being closed
812 # Closes duplicates if the issue is being closed
811 def close_duplicates
813 def close_duplicates
812 if closing?
814 if closing?
813 duplicates.each do |duplicate|
815 duplicates.each do |duplicate|
814 # 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
815 duplicate.reload
817 duplicate.reload
816 # Don't re-close it if it's already closed
818 # Don't re-close it if it's already closed
817 next if duplicate.closed?
819 next if duplicate.closed?
818 # Same user and notes
820 # Same user and notes
819 if @current_journal
821 if @current_journal
820 duplicate.init_journal(@current_journal.user, @current_journal.notes)
822 duplicate.init_journal(@current_journal.user, @current_journal.notes)
821 end
823 end
822 duplicate.update_attribute :status, self.status
824 duplicate.update_attribute :status, self.status
823 end
825 end
824 end
826 end
825 end
827 end
826
828
827 # Saves the changes in a Journal
829 # Saves the changes in a Journal
828 # Called after_save
830 # Called after_save
829 def create_journal
831 def create_journal
830 if @current_journal
832 if @current_journal
831 # attributes changes
833 # attributes changes
832 (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|
833 @current_journal.details << JournalDetail.new(:property => 'attr',
835 @current_journal.details << JournalDetail.new(:property => 'attr',
834 :prop_key => c,
836 :prop_key => c,
835 :old_value => @issue_before_change.send(c),
837 :old_value => @issue_before_change.send(c),
836 :value => send(c)) unless send(c)==@issue_before_change.send(c)
838 :value => send(c)) unless send(c)==@issue_before_change.send(c)
837 }
839 }
838 # custom fields changes
840 # custom fields changes
839 custom_values.each {|c|
841 custom_values.each {|c|
840 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 ||
841 (@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?))
842 @current_journal.details << JournalDetail.new(:property => 'cf',
844 @current_journal.details << JournalDetail.new(:property => 'cf',
843 :prop_key => c.custom_field_id,
845 :prop_key => c.custom_field_id,
844 :old_value => @custom_values_before_change[c.custom_field_id],
846 :old_value => @custom_values_before_change[c.custom_field_id],
845 :value => c.value)
847 :value => c.value)
846 }
848 }
847 @current_journal.save
849 @current_journal.save
848 # reset current journal
850 # reset current journal
849 init_journal @current_journal.user, @current_journal.notes
851 init_journal @current_journal.user, @current_journal.notes
850 end
852 end
851 end
853 end
852
854
853 # Query generator for selecting groups of issue counts for a project
855 # Query generator for selecting groups of issue counts for a project
854 # based on specific criteria
856 # based on specific criteria
855 #
857 #
856 # Options
858 # Options
857 # * project - Project to search in.
859 # * project - Project to search in.
858 # * field - String. Issue field to key off of in the grouping.
860 # * field - String. Issue field to key off of in the grouping.
859 # * joins - String. The table name to join against.
861 # * joins - String. The table name to join against.
860 def self.count_and_group_by(options)
862 def self.count_and_group_by(options)
861 project = options.delete(:project)
863 project = options.delete(:project)
862 select_field = options.delete(:field)
864 select_field = options.delete(:field)
863 joins = options.delete(:joins)
865 joins = options.delete(:joins)
864
866
865 where = "i.#{select_field}=j.id"
867 where = "i.#{select_field}=j.id"
866
868
867 ActiveRecord::Base.connection.select_all("select s.id as status_id,
869 ActiveRecord::Base.connection.select_all("select s.id as status_id,
868 s.is_closed as closed,
870 s.is_closed as closed,
869 j.id as #{select_field},
871 j.id as #{select_field},
870 count(i.id) as total
872 count(i.id) as total
871 from
873 from
872 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
874 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
873 where
875 where
874 i.status_id=s.id
876 i.status_id=s.id
875 and #{where}
877 and #{where}
876 and i.project_id=#{project.id}
878 and i.project_id=#{project.id}
877 group by s.id, s.is_closed, j.id")
879 group by s.id, s.is_closed, j.id")
878 end
880 end
879
881
880
882
881 end
883 end
General Comments 0
You need to be logged in to leave comments. Login now