##// END OF EJS Templates
Rails4: replace deprecated Relation#update_all at Issue model...
Toshi MARUYAMA -
r12253:599ac20570fb
parent child
Show More
@@ -1,1562 +1,1563
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 include Redmine::Utils::DateCalculation
20 include Redmine::Utils::DateCalculation
21 include Redmine::I18n
21 include Redmine::I18n
22
22
23 belongs_to :project
23 belongs_to :project
24 belongs_to :tracker
24 belongs_to :tracker
25 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
25 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
26 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
26 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
27 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
27 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
28 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
28 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
29 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
29 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
30 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
30 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
31
31
32 has_many :journals, :as => :journalized, :dependent => :destroy
32 has_many :journals, :as => :journalized, :dependent => :destroy
33 has_many :visible_journals,
33 has_many :visible_journals,
34 :class_name => 'Journal',
34 :class_name => 'Journal',
35 :as => :journalized,
35 :as => :journalized,
36 :conditions => Proc.new {
36 :conditions => Proc.new {
37 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
37 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
38 },
38 },
39 :readonly => true
39 :readonly => true
40
40
41 has_many :time_entries, :dependent => :destroy
41 has_many :time_entries, :dependent => :destroy
42 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
42 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
43
43
44 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
45 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
46
46
47 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
47 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
48 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
48 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
49 acts_as_customizable
49 acts_as_customizable
50 acts_as_watchable
50 acts_as_watchable
51 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
51 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
52 :include => [:project, :visible_journals],
52 :include => [:project, :visible_journals],
53 # sort by id so that limited eager loading doesn't break with postgresql
53 # sort by id so that limited eager loading doesn't break with postgresql
54 :order_column => "#{table_name}.id"
54 :order_column => "#{table_name}.id"
55 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
55 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
56 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
56 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
57 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
57 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
58
58
59 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
59 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
60 :author_key => :author_id
60 :author_key => :author_id
61
61
62 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
62 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
63
63
64 attr_reader :current_journal
64 attr_reader :current_journal
65 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
65 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
66
66
67 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
67 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
68
68
69 validates_length_of :subject, :maximum => 255
69 validates_length_of :subject, :maximum => 255
70 validates_inclusion_of :done_ratio, :in => 0..100
70 validates_inclusion_of :done_ratio, :in => 0..100
71 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
71 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
72 validates :start_date, :date => true
72 validates :start_date, :date => true
73 validates :due_date, :date => true
73 validates :due_date, :date => true
74 validate :validate_issue, :validate_required_fields
74 validate :validate_issue, :validate_required_fields
75
75
76 scope :visible, lambda {|*args|
76 scope :visible, lambda {|*args|
77 includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
77 includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
78 }
78 }
79
79
80 scope :open, lambda {|*args|
80 scope :open, lambda {|*args|
81 is_closed = args.size > 0 ? !args.first : false
81 is_closed = args.size > 0 ? !args.first : false
82 includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
82 includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
83 }
83 }
84
84
85 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
85 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
86 scope :on_active_project, lambda {
86 scope :on_active_project, lambda {
87 includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
87 includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
88 }
88 }
89 scope :fixed_version, lambda {|versions|
89 scope :fixed_version, lambda {|versions|
90 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
90 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
91 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
91 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
92 }
92 }
93
93
94 before_create :default_assign
94 before_create :default_assign
95 before_save :close_duplicates, :update_done_ratio_from_issue_status,
95 before_save :close_duplicates, :update_done_ratio_from_issue_status,
96 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
96 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
97 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
97 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
98 after_save :reschedule_following_issues, :update_nested_set_attributes,
98 after_save :reschedule_following_issues, :update_nested_set_attributes,
99 :update_parent_attributes, :create_journal
99 :update_parent_attributes, :create_journal
100 # Should be after_create but would be called before previous after_save callbacks
100 # Should be after_create but would be called before previous after_save callbacks
101 after_save :after_create_from_copy
101 after_save :after_create_from_copy
102 after_destroy :update_parent_attributes
102 after_destroy :update_parent_attributes
103 after_create :send_notification
103 after_create :send_notification
104 # Keep it at the end of after_save callbacks
104 # Keep it at the end of after_save callbacks
105 after_save :clear_assigned_to_was
105 after_save :clear_assigned_to_was
106
106
107 # Returns a SQL conditions string used to find all issues visible by the specified user
107 # Returns a SQL conditions string used to find all issues visible by the specified user
108 def self.visible_condition(user, options={})
108 def self.visible_condition(user, options={})
109 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
109 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
110 if user.logged?
110 if user.logged?
111 case role.issues_visibility
111 case role.issues_visibility
112 when 'all'
112 when 'all'
113 nil
113 nil
114 when 'default'
114 when 'default'
115 user_ids = [user.id] + user.groups.map(&:id).compact
115 user_ids = [user.id] + user.groups.map(&:id).compact
116 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
116 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
117 when 'own'
117 when 'own'
118 user_ids = [user.id] + user.groups.map(&:id).compact
118 user_ids = [user.id] + user.groups.map(&:id).compact
119 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
119 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
120 else
120 else
121 '1=0'
121 '1=0'
122 end
122 end
123 else
123 else
124 "(#{table_name}.is_private = #{connection.quoted_false})"
124 "(#{table_name}.is_private = #{connection.quoted_false})"
125 end
125 end
126 end
126 end
127 end
127 end
128
128
129 # Returns true if usr or current user is allowed to view the issue
129 # Returns true if usr or current user is allowed to view the issue
130 def visible?(usr=nil)
130 def visible?(usr=nil)
131 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
131 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
132 if user.logged?
132 if user.logged?
133 case role.issues_visibility
133 case role.issues_visibility
134 when 'all'
134 when 'all'
135 true
135 true
136 when 'default'
136 when 'default'
137 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
137 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
138 when 'own'
138 when 'own'
139 self.author == user || user.is_or_belongs_to?(assigned_to)
139 self.author == user || user.is_or_belongs_to?(assigned_to)
140 else
140 else
141 false
141 false
142 end
142 end
143 else
143 else
144 !self.is_private?
144 !self.is_private?
145 end
145 end
146 end
146 end
147 end
147 end
148
148
149 # Returns true if user or current user is allowed to edit or add a note to the issue
149 # Returns true if user or current user is allowed to edit or add a note to the issue
150 def editable?(user=User.current)
150 def editable?(user=User.current)
151 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
151 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
152 end
152 end
153
153
154 def initialize(attributes=nil, *args)
154 def initialize(attributes=nil, *args)
155 super
155 super
156 if new_record?
156 if new_record?
157 # set default values for new records only
157 # set default values for new records only
158 self.status ||= IssueStatus.default
158 self.status ||= IssueStatus.default
159 self.priority ||= IssuePriority.default
159 self.priority ||= IssuePriority.default
160 self.watcher_user_ids = []
160 self.watcher_user_ids = []
161 end
161 end
162 end
162 end
163
163
164 def create_or_update
164 def create_or_update
165 super
165 super
166 ensure
166 ensure
167 @status_was = nil
167 @status_was = nil
168 end
168 end
169 private :create_or_update
169 private :create_or_update
170
170
171 # AR#Persistence#destroy would raise and RecordNotFound exception
171 # AR#Persistence#destroy would raise and RecordNotFound exception
172 # if the issue was already deleted or updated (non matching lock_version).
172 # if the issue was already deleted or updated (non matching lock_version).
173 # This is a problem when bulk deleting issues or deleting a project
173 # This is a problem when bulk deleting issues or deleting a project
174 # (because an issue may already be deleted if its parent was deleted
174 # (because an issue may already be deleted if its parent was deleted
175 # first).
175 # first).
176 # The issue is reloaded by the nested_set before being deleted so
176 # The issue is reloaded by the nested_set before being deleted so
177 # the lock_version condition should not be an issue but we handle it.
177 # the lock_version condition should not be an issue but we handle it.
178 def destroy
178 def destroy
179 super
179 super
180 rescue ActiveRecord::RecordNotFound
180 rescue ActiveRecord::RecordNotFound
181 # Stale or already deleted
181 # Stale or already deleted
182 begin
182 begin
183 reload
183 reload
184 rescue ActiveRecord::RecordNotFound
184 rescue ActiveRecord::RecordNotFound
185 # The issue was actually already deleted
185 # The issue was actually already deleted
186 @destroyed = true
186 @destroyed = true
187 return freeze
187 return freeze
188 end
188 end
189 # The issue was stale, retry to destroy
189 # The issue was stale, retry to destroy
190 super
190 super
191 end
191 end
192
192
193 alias :base_reload :reload
193 alias :base_reload :reload
194 def reload(*args)
194 def reload(*args)
195 @workflow_rule_by_attribute = nil
195 @workflow_rule_by_attribute = nil
196 @assignable_versions = nil
196 @assignable_versions = nil
197 @relations = nil
197 @relations = nil
198 base_reload(*args)
198 base_reload(*args)
199 end
199 end
200
200
201 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
201 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
202 def available_custom_fields
202 def available_custom_fields
203 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
203 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
204 end
204 end
205
205
206 def visible_custom_field_values(user=nil)
206 def visible_custom_field_values(user=nil)
207 user_real = user || User.current
207 user_real = user || User.current
208 custom_field_values.select do |value|
208 custom_field_values.select do |value|
209 value.custom_field.visible_by?(project, user_real)
209 value.custom_field.visible_by?(project, user_real)
210 end
210 end
211 end
211 end
212
212
213 # Copies attributes from another issue, arg can be an id or an Issue
213 # Copies attributes from another issue, arg can be an id or an Issue
214 def copy_from(arg, options={})
214 def copy_from(arg, options={})
215 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
215 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
216 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
216 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
217 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
217 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
218 self.status = issue.status
218 self.status = issue.status
219 self.author = User.current
219 self.author = User.current
220 unless options[:attachments] == false
220 unless options[:attachments] == false
221 self.attachments = issue.attachments.map do |attachement|
221 self.attachments = issue.attachments.map do |attachement|
222 attachement.copy(:container => self)
222 attachement.copy(:container => self)
223 end
223 end
224 end
224 end
225 @copied_from = issue
225 @copied_from = issue
226 @copy_options = options
226 @copy_options = options
227 self
227 self
228 end
228 end
229
229
230 # Returns an unsaved copy of the issue
230 # Returns an unsaved copy of the issue
231 def copy(attributes=nil, copy_options={})
231 def copy(attributes=nil, copy_options={})
232 copy = self.class.new.copy_from(self, copy_options)
232 copy = self.class.new.copy_from(self, copy_options)
233 copy.attributes = attributes if attributes
233 copy.attributes = attributes if attributes
234 copy
234 copy
235 end
235 end
236
236
237 # Returns true if the issue is a copy
237 # Returns true if the issue is a copy
238 def copy?
238 def copy?
239 @copied_from.present?
239 @copied_from.present?
240 end
240 end
241
241
242 # Moves/copies an issue to a new project and tracker
242 # Moves/copies an issue to a new project and tracker
243 # Returns the moved/copied issue on success, false on failure
243 # Returns the moved/copied issue on success, false on failure
244 def move_to_project(new_project, new_tracker=nil, options={})
244 def move_to_project(new_project, new_tracker=nil, options={})
245 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
245 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
246
246
247 if options[:copy]
247 if options[:copy]
248 issue = self.copy
248 issue = self.copy
249 else
249 else
250 issue = self
250 issue = self
251 end
251 end
252
252
253 issue.init_journal(User.current, options[:notes])
253 issue.init_journal(User.current, options[:notes])
254
254
255 # Preserve previous behaviour
255 # Preserve previous behaviour
256 # #move_to_project doesn't change tracker automatically
256 # #move_to_project doesn't change tracker automatically
257 issue.send :project=, new_project, true
257 issue.send :project=, new_project, true
258 if new_tracker
258 if new_tracker
259 issue.tracker = new_tracker
259 issue.tracker = new_tracker
260 end
260 end
261 # Allow bulk setting of attributes on the issue
261 # Allow bulk setting of attributes on the issue
262 if options[:attributes]
262 if options[:attributes]
263 issue.attributes = options[:attributes]
263 issue.attributes = options[:attributes]
264 end
264 end
265
265
266 issue.save ? issue : false
266 issue.save ? issue : false
267 end
267 end
268
268
269 def status_id=(sid)
269 def status_id=(sid)
270 self.status = nil
270 self.status = nil
271 result = write_attribute(:status_id, sid)
271 result = write_attribute(:status_id, sid)
272 @workflow_rule_by_attribute = nil
272 @workflow_rule_by_attribute = nil
273 result
273 result
274 end
274 end
275
275
276 def priority_id=(pid)
276 def priority_id=(pid)
277 self.priority = nil
277 self.priority = nil
278 write_attribute(:priority_id, pid)
278 write_attribute(:priority_id, pid)
279 end
279 end
280
280
281 def category_id=(cid)
281 def category_id=(cid)
282 self.category = nil
282 self.category = nil
283 write_attribute(:category_id, cid)
283 write_attribute(:category_id, cid)
284 end
284 end
285
285
286 def fixed_version_id=(vid)
286 def fixed_version_id=(vid)
287 self.fixed_version = nil
287 self.fixed_version = nil
288 write_attribute(:fixed_version_id, vid)
288 write_attribute(:fixed_version_id, vid)
289 end
289 end
290
290
291 def tracker_id=(tid)
291 def tracker_id=(tid)
292 self.tracker = nil
292 self.tracker = nil
293 result = write_attribute(:tracker_id, tid)
293 result = write_attribute(:tracker_id, tid)
294 @custom_field_values = nil
294 @custom_field_values = nil
295 @workflow_rule_by_attribute = nil
295 @workflow_rule_by_attribute = nil
296 result
296 result
297 end
297 end
298
298
299 def project_id=(project_id)
299 def project_id=(project_id)
300 if project_id.to_s != self.project_id.to_s
300 if project_id.to_s != self.project_id.to_s
301 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
301 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
302 end
302 end
303 end
303 end
304
304
305 def project=(project, keep_tracker=false)
305 def project=(project, keep_tracker=false)
306 project_was = self.project
306 project_was = self.project
307 write_attribute(:project_id, project ? project.id : nil)
307 write_attribute(:project_id, project ? project.id : nil)
308 association_instance_set('project', project)
308 association_instance_set('project', project)
309 if project_was && project && project_was != project
309 if project_was && project && project_was != project
310 @assignable_versions = nil
310 @assignable_versions = nil
311
311
312 unless keep_tracker || project.trackers.include?(tracker)
312 unless keep_tracker || project.trackers.include?(tracker)
313 self.tracker = project.trackers.first
313 self.tracker = project.trackers.first
314 end
314 end
315 # Reassign to the category with same name if any
315 # Reassign to the category with same name if any
316 if category
316 if category
317 self.category = project.issue_categories.find_by_name(category.name)
317 self.category = project.issue_categories.find_by_name(category.name)
318 end
318 end
319 # Keep the fixed_version if it's still valid in the new_project
319 # Keep the fixed_version if it's still valid in the new_project
320 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
320 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
321 self.fixed_version = nil
321 self.fixed_version = nil
322 end
322 end
323 # Clear the parent task if it's no longer valid
323 # Clear the parent task if it's no longer valid
324 unless valid_parent_project?
324 unless valid_parent_project?
325 self.parent_issue_id = nil
325 self.parent_issue_id = nil
326 end
326 end
327 @custom_field_values = nil
327 @custom_field_values = nil
328 end
328 end
329 end
329 end
330
330
331 def description=(arg)
331 def description=(arg)
332 if arg.is_a?(String)
332 if arg.is_a?(String)
333 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
333 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
334 end
334 end
335 write_attribute(:description, arg)
335 write_attribute(:description, arg)
336 end
336 end
337
337
338 # Overrides assign_attributes so that project and tracker get assigned first
338 # Overrides assign_attributes so that project and tracker get assigned first
339 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
339 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
340 return if new_attributes.nil?
340 return if new_attributes.nil?
341 attrs = new_attributes.dup
341 attrs = new_attributes.dup
342 attrs.stringify_keys!
342 attrs.stringify_keys!
343
343
344 %w(project project_id tracker tracker_id).each do |attr|
344 %w(project project_id tracker tracker_id).each do |attr|
345 if attrs.has_key?(attr)
345 if attrs.has_key?(attr)
346 send "#{attr}=", attrs.delete(attr)
346 send "#{attr}=", attrs.delete(attr)
347 end
347 end
348 end
348 end
349 send :assign_attributes_without_project_and_tracker_first, attrs, *args
349 send :assign_attributes_without_project_and_tracker_first, attrs, *args
350 end
350 end
351 # Do not redefine alias chain on reload (see #4838)
351 # Do not redefine alias chain on reload (see #4838)
352 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
352 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
353
353
354 def estimated_hours=(h)
354 def estimated_hours=(h)
355 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
355 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
356 end
356 end
357
357
358 safe_attributes 'project_id',
358 safe_attributes 'project_id',
359 :if => lambda {|issue, user|
359 :if => lambda {|issue, user|
360 if issue.new_record?
360 if issue.new_record?
361 issue.copy?
361 issue.copy?
362 elsif user.allowed_to?(:move_issues, issue.project)
362 elsif user.allowed_to?(:move_issues, issue.project)
363 Issue.allowed_target_projects_on_move.count > 1
363 Issue.allowed_target_projects_on_move.count > 1
364 end
364 end
365 }
365 }
366
366
367 safe_attributes 'tracker_id',
367 safe_attributes 'tracker_id',
368 'status_id',
368 'status_id',
369 'category_id',
369 'category_id',
370 'assigned_to_id',
370 'assigned_to_id',
371 'priority_id',
371 'priority_id',
372 'fixed_version_id',
372 'fixed_version_id',
373 'subject',
373 'subject',
374 'description',
374 'description',
375 'start_date',
375 'start_date',
376 'due_date',
376 'due_date',
377 'done_ratio',
377 'done_ratio',
378 'estimated_hours',
378 'estimated_hours',
379 'custom_field_values',
379 'custom_field_values',
380 'custom_fields',
380 'custom_fields',
381 'lock_version',
381 'lock_version',
382 'notes',
382 'notes',
383 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
383 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
384
384
385 safe_attributes 'status_id',
385 safe_attributes 'status_id',
386 'assigned_to_id',
386 'assigned_to_id',
387 'fixed_version_id',
387 'fixed_version_id',
388 'done_ratio',
388 'done_ratio',
389 'lock_version',
389 'lock_version',
390 'notes',
390 'notes',
391 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
391 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
392
392
393 safe_attributes 'notes',
393 safe_attributes 'notes',
394 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
394 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
395
395
396 safe_attributes 'private_notes',
396 safe_attributes 'private_notes',
397 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
397 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
398
398
399 safe_attributes 'watcher_user_ids',
399 safe_attributes 'watcher_user_ids',
400 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
400 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
401
401
402 safe_attributes 'is_private',
402 safe_attributes 'is_private',
403 :if => lambda {|issue, user|
403 :if => lambda {|issue, user|
404 user.allowed_to?(:set_issues_private, issue.project) ||
404 user.allowed_to?(:set_issues_private, issue.project) ||
405 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
405 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
406 }
406 }
407
407
408 safe_attributes 'parent_issue_id',
408 safe_attributes 'parent_issue_id',
409 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
409 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
410 user.allowed_to?(:manage_subtasks, issue.project)}
410 user.allowed_to?(:manage_subtasks, issue.project)}
411
411
412 def safe_attribute_names(user=nil)
412 def safe_attribute_names(user=nil)
413 names = super
413 names = super
414 names -= disabled_core_fields
414 names -= disabled_core_fields
415 names -= read_only_attribute_names(user)
415 names -= read_only_attribute_names(user)
416 names
416 names
417 end
417 end
418
418
419 # Safely sets attributes
419 # Safely sets attributes
420 # Should be called from controllers instead of #attributes=
420 # Should be called from controllers instead of #attributes=
421 # attr_accessible is too rough because we still want things like
421 # attr_accessible is too rough because we still want things like
422 # Issue.new(:project => foo) to work
422 # Issue.new(:project => foo) to work
423 def safe_attributes=(attrs, user=User.current)
423 def safe_attributes=(attrs, user=User.current)
424 return unless attrs.is_a?(Hash)
424 return unless attrs.is_a?(Hash)
425
425
426 attrs = attrs.dup
426 attrs = attrs.dup
427
427
428 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
428 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
429 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
429 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
430 if allowed_target_projects(user).where(:id => p.to_i).exists?
430 if allowed_target_projects(user).where(:id => p.to_i).exists?
431 self.project_id = p
431 self.project_id = p
432 end
432 end
433 end
433 end
434
434
435 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
435 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
436 self.tracker_id = t
436 self.tracker_id = t
437 end
437 end
438
438
439 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
439 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
440 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
440 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
441 self.status_id = s
441 self.status_id = s
442 end
442 end
443 end
443 end
444
444
445 attrs = delete_unsafe_attributes(attrs, user)
445 attrs = delete_unsafe_attributes(attrs, user)
446 return if attrs.empty?
446 return if attrs.empty?
447
447
448 unless leaf?
448 unless leaf?
449 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
449 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
450 end
450 end
451
451
452 if attrs['parent_issue_id'].present?
452 if attrs['parent_issue_id'].present?
453 s = attrs['parent_issue_id'].to_s
453 s = attrs['parent_issue_id'].to_s
454 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
454 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
455 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
455 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
456 end
456 end
457 end
457 end
458
458
459 if attrs['custom_field_values'].present?
459 if attrs['custom_field_values'].present?
460 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
460 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
461 # TODO: use #select when ruby1.8 support is dropped
461 # TODO: use #select when ruby1.8 support is dropped
462 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| !editable_custom_field_ids.include?(k.to_s)}
462 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| !editable_custom_field_ids.include?(k.to_s)}
463 end
463 end
464
464
465 if attrs['custom_fields'].present?
465 if attrs['custom_fields'].present?
466 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
466 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
467 # TODO: use #select when ruby1.8 support is dropped
467 # TODO: use #select when ruby1.8 support is dropped
468 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| !editable_custom_field_ids.include?(c['id'].to_s)}
468 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| !editable_custom_field_ids.include?(c['id'].to_s)}
469 end
469 end
470
470
471 # mass-assignment security bypass
471 # mass-assignment security bypass
472 assign_attributes attrs, :without_protection => true
472 assign_attributes attrs, :without_protection => true
473 end
473 end
474
474
475 def disabled_core_fields
475 def disabled_core_fields
476 tracker ? tracker.disabled_core_fields : []
476 tracker ? tracker.disabled_core_fields : []
477 end
477 end
478
478
479 # Returns the custom_field_values that can be edited by the given user
479 # Returns the custom_field_values that can be edited by the given user
480 def editable_custom_field_values(user=nil)
480 def editable_custom_field_values(user=nil)
481 visible_custom_field_values(user).reject do |value|
481 visible_custom_field_values(user).reject do |value|
482 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
482 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
483 end
483 end
484 end
484 end
485
485
486 # Returns the names of attributes that are read-only for user or the current user
486 # Returns the names of attributes that are read-only for user or the current user
487 # For users with multiple roles, the read-only fields are the intersection of
487 # For users with multiple roles, the read-only fields are the intersection of
488 # read-only fields of each role
488 # read-only fields of each role
489 # The result is an array of strings where sustom fields are represented with their ids
489 # The result is an array of strings where sustom fields are represented with their ids
490 #
490 #
491 # Examples:
491 # Examples:
492 # issue.read_only_attribute_names # => ['due_date', '2']
492 # issue.read_only_attribute_names # => ['due_date', '2']
493 # issue.read_only_attribute_names(user) # => []
493 # issue.read_only_attribute_names(user) # => []
494 def read_only_attribute_names(user=nil)
494 def read_only_attribute_names(user=nil)
495 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
495 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
496 end
496 end
497
497
498 # Returns the names of required attributes for user or the current user
498 # Returns the names of required attributes for user or the current user
499 # For users with multiple roles, the required fields are the intersection of
499 # For users with multiple roles, the required fields are the intersection of
500 # required fields of each role
500 # required fields of each role
501 # The result is an array of strings where sustom fields are represented with their ids
501 # The result is an array of strings where sustom fields are represented with their ids
502 #
502 #
503 # Examples:
503 # Examples:
504 # issue.required_attribute_names # => ['due_date', '2']
504 # issue.required_attribute_names # => ['due_date', '2']
505 # issue.required_attribute_names(user) # => []
505 # issue.required_attribute_names(user) # => []
506 def required_attribute_names(user=nil)
506 def required_attribute_names(user=nil)
507 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
507 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
508 end
508 end
509
509
510 # Returns true if the attribute is required for user
510 # Returns true if the attribute is required for user
511 def required_attribute?(name, user=nil)
511 def required_attribute?(name, user=nil)
512 required_attribute_names(user).include?(name.to_s)
512 required_attribute_names(user).include?(name.to_s)
513 end
513 end
514
514
515 # Returns a hash of the workflow rule by attribute for the given user
515 # Returns a hash of the workflow rule by attribute for the given user
516 #
516 #
517 # Examples:
517 # Examples:
518 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
518 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
519 def workflow_rule_by_attribute(user=nil)
519 def workflow_rule_by_attribute(user=nil)
520 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
520 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
521
521
522 user_real = user || User.current
522 user_real = user || User.current
523 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
523 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
524 return {} if roles.empty?
524 return {} if roles.empty?
525
525
526 result = {}
526 result = {}
527 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
527 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
528 if workflow_permissions.any?
528 if workflow_permissions.any?
529 workflow_rules = workflow_permissions.inject({}) do |h, wp|
529 workflow_rules = workflow_permissions.inject({}) do |h, wp|
530 h[wp.field_name] ||= []
530 h[wp.field_name] ||= []
531 h[wp.field_name] << wp.rule
531 h[wp.field_name] << wp.rule
532 h
532 h
533 end
533 end
534 workflow_rules.each do |attr, rules|
534 workflow_rules.each do |attr, rules|
535 next if rules.size < roles.size
535 next if rules.size < roles.size
536 uniq_rules = rules.uniq
536 uniq_rules = rules.uniq
537 if uniq_rules.size == 1
537 if uniq_rules.size == 1
538 result[attr] = uniq_rules.first
538 result[attr] = uniq_rules.first
539 else
539 else
540 result[attr] = 'required'
540 result[attr] = 'required'
541 end
541 end
542 end
542 end
543 end
543 end
544 @workflow_rule_by_attribute = result if user.nil?
544 @workflow_rule_by_attribute = result if user.nil?
545 result
545 result
546 end
546 end
547 private :workflow_rule_by_attribute
547 private :workflow_rule_by_attribute
548
548
549 def done_ratio
549 def done_ratio
550 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
550 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
551 status.default_done_ratio
551 status.default_done_ratio
552 else
552 else
553 read_attribute(:done_ratio)
553 read_attribute(:done_ratio)
554 end
554 end
555 end
555 end
556
556
557 def self.use_status_for_done_ratio?
557 def self.use_status_for_done_ratio?
558 Setting.issue_done_ratio == 'issue_status'
558 Setting.issue_done_ratio == 'issue_status'
559 end
559 end
560
560
561 def self.use_field_for_done_ratio?
561 def self.use_field_for_done_ratio?
562 Setting.issue_done_ratio == 'issue_field'
562 Setting.issue_done_ratio == 'issue_field'
563 end
563 end
564
564
565 def validate_issue
565 def validate_issue
566 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
566 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
567 errors.add :due_date, :greater_than_start_date
567 errors.add :due_date, :greater_than_start_date
568 end
568 end
569
569
570 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
570 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
571 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
571 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
572 end
572 end
573
573
574 if fixed_version
574 if fixed_version
575 if !assignable_versions.include?(fixed_version)
575 if !assignable_versions.include?(fixed_version)
576 errors.add :fixed_version_id, :inclusion
576 errors.add :fixed_version_id, :inclusion
577 elsif reopened? && fixed_version.closed?
577 elsif reopened? && fixed_version.closed?
578 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
578 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
579 end
579 end
580 end
580 end
581
581
582 # Checks that the issue can not be added/moved to a disabled tracker
582 # Checks that the issue can not be added/moved to a disabled tracker
583 if project && (tracker_id_changed? || project_id_changed?)
583 if project && (tracker_id_changed? || project_id_changed?)
584 unless project.trackers.include?(tracker)
584 unless project.trackers.include?(tracker)
585 errors.add :tracker_id, :inclusion
585 errors.add :tracker_id, :inclusion
586 end
586 end
587 end
587 end
588
588
589 # Checks parent issue assignment
589 # Checks parent issue assignment
590 if @invalid_parent_issue_id.present?
590 if @invalid_parent_issue_id.present?
591 errors.add :parent_issue_id, :invalid
591 errors.add :parent_issue_id, :invalid
592 elsif @parent_issue
592 elsif @parent_issue
593 if !valid_parent_project?(@parent_issue)
593 if !valid_parent_project?(@parent_issue)
594 errors.add :parent_issue_id, :invalid
594 errors.add :parent_issue_id, :invalid
595 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
595 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
596 errors.add :parent_issue_id, :invalid
596 errors.add :parent_issue_id, :invalid
597 elsif !new_record?
597 elsif !new_record?
598 # moving an existing issue
598 # moving an existing issue
599 if @parent_issue.root_id != root_id
599 if @parent_issue.root_id != root_id
600 # we can always move to another tree
600 # we can always move to another tree
601 elsif move_possible?(@parent_issue)
601 elsif move_possible?(@parent_issue)
602 # move accepted inside tree
602 # move accepted inside tree
603 else
603 else
604 errors.add :parent_issue_id, :invalid
604 errors.add :parent_issue_id, :invalid
605 end
605 end
606 end
606 end
607 end
607 end
608 end
608 end
609
609
610 # Validates the issue against additional workflow requirements
610 # Validates the issue against additional workflow requirements
611 def validate_required_fields
611 def validate_required_fields
612 user = new_record? ? author : current_journal.try(:user)
612 user = new_record? ? author : current_journal.try(:user)
613
613
614 required_attribute_names(user).each do |attribute|
614 required_attribute_names(user).each do |attribute|
615 if attribute =~ /^\d+$/
615 if attribute =~ /^\d+$/
616 attribute = attribute.to_i
616 attribute = attribute.to_i
617 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
617 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
618 if v && v.value.blank?
618 if v && v.value.blank?
619 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
619 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
620 end
620 end
621 else
621 else
622 if respond_to?(attribute) && send(attribute).blank?
622 if respond_to?(attribute) && send(attribute).blank?
623 errors.add attribute, :blank
623 errors.add attribute, :blank
624 end
624 end
625 end
625 end
626 end
626 end
627 end
627 end
628
628
629 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
629 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
630 # even if the user turns off the setting later
630 # even if the user turns off the setting later
631 def update_done_ratio_from_issue_status
631 def update_done_ratio_from_issue_status
632 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
632 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
633 self.done_ratio = status.default_done_ratio
633 self.done_ratio = status.default_done_ratio
634 end
634 end
635 end
635 end
636
636
637 def init_journal(user, notes = "")
637 def init_journal(user, notes = "")
638 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
638 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
639 if new_record?
639 if new_record?
640 @current_journal.notify = false
640 @current_journal.notify = false
641 else
641 else
642 @attributes_before_change = attributes.dup
642 @attributes_before_change = attributes.dup
643 @custom_values_before_change = {}
643 @custom_values_before_change = {}
644 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
644 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
645 end
645 end
646 @current_journal
646 @current_journal
647 end
647 end
648
648
649 # Returns the id of the last journal or nil
649 # Returns the id of the last journal or nil
650 def last_journal_id
650 def last_journal_id
651 if new_record?
651 if new_record?
652 nil
652 nil
653 else
653 else
654 journals.maximum(:id)
654 journals.maximum(:id)
655 end
655 end
656 end
656 end
657
657
658 # Returns a scope for journals that have an id greater than journal_id
658 # Returns a scope for journals that have an id greater than journal_id
659 def journals_after(journal_id)
659 def journals_after(journal_id)
660 scope = journals.reorder("#{Journal.table_name}.id ASC")
660 scope = journals.reorder("#{Journal.table_name}.id ASC")
661 if journal_id.present?
661 if journal_id.present?
662 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
662 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
663 end
663 end
664 scope
664 scope
665 end
665 end
666
666
667 # Returns the initial status of the issue
667 # Returns the initial status of the issue
668 # Returns nil for a new issue
668 # Returns nil for a new issue
669 def status_was
669 def status_was
670 if status_id_was && status_id_was.to_i > 0
670 if status_id_was && status_id_was.to_i > 0
671 @status_was ||= IssueStatus.find_by_id(status_id_was)
671 @status_was ||= IssueStatus.find_by_id(status_id_was)
672 end
672 end
673 end
673 end
674
674
675 # Return true if the issue is closed, otherwise false
675 # Return true if the issue is closed, otherwise false
676 def closed?
676 def closed?
677 self.status.is_closed?
677 self.status.is_closed?
678 end
678 end
679
679
680 # Return true if the issue is being reopened
680 # Return true if the issue is being reopened
681 def reopened?
681 def reopened?
682 if !new_record? && status_id_changed?
682 if !new_record? && status_id_changed?
683 status_was = IssueStatus.find_by_id(status_id_was)
683 status_was = IssueStatus.find_by_id(status_id_was)
684 status_new = IssueStatus.find_by_id(status_id)
684 status_new = IssueStatus.find_by_id(status_id)
685 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
685 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
686 return true
686 return true
687 end
687 end
688 end
688 end
689 false
689 false
690 end
690 end
691
691
692 # Return true if the issue is being closed
692 # Return true if the issue is being closed
693 def closing?
693 def closing?
694 if !new_record? && status_id_changed?
694 if !new_record? && status_id_changed?
695 if status_was && status && !status_was.is_closed? && status.is_closed?
695 if status_was && status && !status_was.is_closed? && status.is_closed?
696 return true
696 return true
697 end
697 end
698 end
698 end
699 false
699 false
700 end
700 end
701
701
702 # Returns true if the issue is overdue
702 # Returns true if the issue is overdue
703 def overdue?
703 def overdue?
704 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
704 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
705 end
705 end
706
706
707 # Is the amount of work done less than it should for the due date
707 # Is the amount of work done less than it should for the due date
708 def behind_schedule?
708 def behind_schedule?
709 return false if start_date.nil? || due_date.nil?
709 return false if start_date.nil? || due_date.nil?
710 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
710 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
711 return done_date <= Date.today
711 return done_date <= Date.today
712 end
712 end
713
713
714 # Does this issue have children?
714 # Does this issue have children?
715 def children?
715 def children?
716 !leaf?
716 !leaf?
717 end
717 end
718
718
719 # Users the issue can be assigned to
719 # Users the issue can be assigned to
720 def assignable_users
720 def assignable_users
721 users = project.assignable_users
721 users = project.assignable_users
722 users << author if author
722 users << author if author
723 users << assigned_to if assigned_to
723 users << assigned_to if assigned_to
724 users.uniq.sort
724 users.uniq.sort
725 end
725 end
726
726
727 # Versions that the issue can be assigned to
727 # Versions that the issue can be assigned to
728 def assignable_versions
728 def assignable_versions
729 return @assignable_versions if @assignable_versions
729 return @assignable_versions if @assignable_versions
730
730
731 versions = project.shared_versions.open.all
731 versions = project.shared_versions.open.all
732 if fixed_version
732 if fixed_version
733 if fixed_version_id_changed?
733 if fixed_version_id_changed?
734 # nothing to do
734 # nothing to do
735 elsif project_id_changed?
735 elsif project_id_changed?
736 if project.shared_versions.include?(fixed_version)
736 if project.shared_versions.include?(fixed_version)
737 versions << fixed_version
737 versions << fixed_version
738 end
738 end
739 else
739 else
740 versions << fixed_version
740 versions << fixed_version
741 end
741 end
742 end
742 end
743 @assignable_versions = versions.uniq.sort
743 @assignable_versions = versions.uniq.sort
744 end
744 end
745
745
746 # Returns true if this issue is blocked by another issue that is still open
746 # Returns true if this issue is blocked by another issue that is still open
747 def blocked?
747 def blocked?
748 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
748 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
749 end
749 end
750
750
751 # Returns an array of statuses that user is able to apply
751 # Returns an array of statuses that user is able to apply
752 def new_statuses_allowed_to(user=User.current, include_default=false)
752 def new_statuses_allowed_to(user=User.current, include_default=false)
753 if new_record? && @copied_from
753 if new_record? && @copied_from
754 [IssueStatus.default, @copied_from.status].compact.uniq.sort
754 [IssueStatus.default, @copied_from.status].compact.uniq.sort
755 else
755 else
756 initial_status = nil
756 initial_status = nil
757 if new_record?
757 if new_record?
758 initial_status = IssueStatus.default
758 initial_status = IssueStatus.default
759 elsif status_id_was
759 elsif status_id_was
760 initial_status = IssueStatus.find_by_id(status_id_was)
760 initial_status = IssueStatus.find_by_id(status_id_was)
761 end
761 end
762 initial_status ||= status
762 initial_status ||= status
763
763
764 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
764 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
765 assignee_transitions_allowed = initial_assigned_to_id.present? &&
765 assignee_transitions_allowed = initial_assigned_to_id.present? &&
766 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
766 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
767
767
768 statuses = initial_status.find_new_statuses_allowed_to(
768 statuses = initial_status.find_new_statuses_allowed_to(
769 user.admin ? Role.all : user.roles_for_project(project),
769 user.admin ? Role.all : user.roles_for_project(project),
770 tracker,
770 tracker,
771 author == user,
771 author == user,
772 assignee_transitions_allowed
772 assignee_transitions_allowed
773 )
773 )
774 statuses << initial_status unless statuses.empty?
774 statuses << initial_status unless statuses.empty?
775 statuses << IssueStatus.default if include_default
775 statuses << IssueStatus.default if include_default
776 statuses = statuses.compact.uniq.sort
776 statuses = statuses.compact.uniq.sort
777 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
777 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
778 end
778 end
779 end
779 end
780
780
781 # Returns the previous assignee if changed
781 # Returns the previous assignee if changed
782 def assigned_to_was
782 def assigned_to_was
783 # assigned_to_id_was is reset before after_save callbacks
783 # assigned_to_id_was is reset before after_save callbacks
784 user_id = @previous_assigned_to_id || assigned_to_id_was
784 user_id = @previous_assigned_to_id || assigned_to_id_was
785 if user_id && user_id != assigned_to_id
785 if user_id && user_id != assigned_to_id
786 @assigned_to_was ||= User.find_by_id(user_id)
786 @assigned_to_was ||= User.find_by_id(user_id)
787 end
787 end
788 end
788 end
789
789
790 # Returns the users that should be notified
790 # Returns the users that should be notified
791 def notified_users
791 def notified_users
792 notified = []
792 notified = []
793 # Author and assignee are always notified unless they have been
793 # Author and assignee are always notified unless they have been
794 # locked or don't want to be notified
794 # locked or don't want to be notified
795 notified << author if author
795 notified << author if author
796 if assigned_to
796 if assigned_to
797 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
797 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
798 end
798 end
799 if assigned_to_was
799 if assigned_to_was
800 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
800 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
801 end
801 end
802 notified = notified.select {|u| u.active? && u.notify_about?(self)}
802 notified = notified.select {|u| u.active? && u.notify_about?(self)}
803
803
804 notified += project.notified_users
804 notified += project.notified_users
805 notified.uniq!
805 notified.uniq!
806 # Remove users that can not view the issue
806 # Remove users that can not view the issue
807 notified.reject! {|user| !visible?(user)}
807 notified.reject! {|user| !visible?(user)}
808 notified
808 notified
809 end
809 end
810
810
811 # Returns the email addresses that should be notified
811 # Returns the email addresses that should be notified
812 def recipients
812 def recipients
813 notified_users.collect(&:mail)
813 notified_users.collect(&:mail)
814 end
814 end
815
815
816 def each_notification(users, &block)
816 def each_notification(users, &block)
817 if users.any?
817 if users.any?
818 if custom_field_values.detect {|value| !value.custom_field.visible?}
818 if custom_field_values.detect {|value| !value.custom_field.visible?}
819 users_by_custom_field_visibility = users.group_by do |user|
819 users_by_custom_field_visibility = users.group_by do |user|
820 visible_custom_field_values(user).map(&:custom_field_id).sort
820 visible_custom_field_values(user).map(&:custom_field_id).sort
821 end
821 end
822 users_by_custom_field_visibility.values.each do |users|
822 users_by_custom_field_visibility.values.each do |users|
823 yield(users)
823 yield(users)
824 end
824 end
825 else
825 else
826 yield(users)
826 yield(users)
827 end
827 end
828 end
828 end
829 end
829 end
830
830
831 # Returns the number of hours spent on this issue
831 # Returns the number of hours spent on this issue
832 def spent_hours
832 def spent_hours
833 @spent_hours ||= time_entries.sum(:hours) || 0
833 @spent_hours ||= time_entries.sum(:hours) || 0
834 end
834 end
835
835
836 # Returns the total number of hours spent on this issue and its descendants
836 # Returns the total number of hours spent on this issue and its descendants
837 #
837 #
838 # Example:
838 # Example:
839 # spent_hours => 0.0
839 # spent_hours => 0.0
840 # spent_hours => 50.2
840 # spent_hours => 50.2
841 def total_spent_hours
841 def total_spent_hours
842 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
842 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
843 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
843 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
844 end
844 end
845
845
846 def relations
846 def relations
847 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
847 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
848 end
848 end
849
849
850 # Preloads relations for a collection of issues
850 # Preloads relations for a collection of issues
851 def self.load_relations(issues)
851 def self.load_relations(issues)
852 if issues.any?
852 if issues.any?
853 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
853 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
854 issues.each do |issue|
854 issues.each do |issue|
855 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
855 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
856 end
856 end
857 end
857 end
858 end
858 end
859
859
860 # Preloads visible spent time for a collection of issues
860 # Preloads visible spent time for a collection of issues
861 def self.load_visible_spent_hours(issues, user=User.current)
861 def self.load_visible_spent_hours(issues, user=User.current)
862 if issues.any?
862 if issues.any?
863 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
863 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
864 issues.each do |issue|
864 issues.each do |issue|
865 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
865 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
866 end
866 end
867 end
867 end
868 end
868 end
869
869
870 # Preloads visible relations for a collection of issues
870 # Preloads visible relations for a collection of issues
871 def self.load_visible_relations(issues, user=User.current)
871 def self.load_visible_relations(issues, user=User.current)
872 if issues.any?
872 if issues.any?
873 issue_ids = issues.map(&:id)
873 issue_ids = issues.map(&:id)
874 # Relations with issue_from in given issues and visible issue_to
874 # Relations with issue_from in given issues and visible issue_to
875 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
875 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
876 # Relations with issue_to in given issues and visible issue_from
876 # Relations with issue_to in given issues and visible issue_from
877 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
877 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
878
878
879 issues.each do |issue|
879 issues.each do |issue|
880 relations =
880 relations =
881 relations_from.select {|relation| relation.issue_from_id == issue.id} +
881 relations_from.select {|relation| relation.issue_from_id == issue.id} +
882 relations_to.select {|relation| relation.issue_to_id == issue.id}
882 relations_to.select {|relation| relation.issue_to_id == issue.id}
883
883
884 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
884 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
885 end
885 end
886 end
886 end
887 end
887 end
888
888
889 # Finds an issue relation given its id.
889 # Finds an issue relation given its id.
890 def find_relation(relation_id)
890 def find_relation(relation_id)
891 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
891 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
892 end
892 end
893
893
894 # Returns all the other issues that depend on the issue
894 # Returns all the other issues that depend on the issue
895 # The algorithm is a modified breadth first search (bfs)
895 # The algorithm is a modified breadth first search (bfs)
896 def all_dependent_issues(except=[])
896 def all_dependent_issues(except=[])
897 # The found dependencies
897 # The found dependencies
898 dependencies = []
898 dependencies = []
899
899
900 # The visited flag for every node (issue) used by the breadth first search
900 # The visited flag for every node (issue) used by the breadth first search
901 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
901 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
902
902
903 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
903 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
904 # the issue when it is processed.
904 # the issue when it is processed.
905
905
906 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
906 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
907 # but its children will not be added to the queue when it is processed.
907 # but its children will not be added to the queue when it is processed.
908
908
909 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
909 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
910 # the queue, but its children have not been added.
910 # the queue, but its children have not been added.
911
911
912 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
912 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
913 # the children still need to be processed.
913 # the children still need to be processed.
914
914
915 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
915 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
916 # added as dependent issues. It needs no further processing.
916 # added as dependent issues. It needs no further processing.
917
917
918 issue_status = Hash.new(eNOT_DISCOVERED)
918 issue_status = Hash.new(eNOT_DISCOVERED)
919
919
920 # The queue
920 # The queue
921 queue = []
921 queue = []
922
922
923 # Initialize the bfs, add start node (self) to the queue
923 # Initialize the bfs, add start node (self) to the queue
924 queue << self
924 queue << self
925 issue_status[self] = ePROCESS_ALL
925 issue_status[self] = ePROCESS_ALL
926
926
927 while (!queue.empty?) do
927 while (!queue.empty?) do
928 current_issue = queue.shift
928 current_issue = queue.shift
929 current_issue_status = issue_status[current_issue]
929 current_issue_status = issue_status[current_issue]
930 dependencies << current_issue
930 dependencies << current_issue
931
931
932 # Add parent to queue, if not already in it.
932 # Add parent to queue, if not already in it.
933 parent = current_issue.parent
933 parent = current_issue.parent
934 parent_status = issue_status[parent]
934 parent_status = issue_status[parent]
935
935
936 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
936 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
937 queue << parent
937 queue << parent
938 issue_status[parent] = ePROCESS_RELATIONS_ONLY
938 issue_status[parent] = ePROCESS_RELATIONS_ONLY
939 end
939 end
940
940
941 # Add children to queue, but only if they are not already in it and
941 # Add children to queue, but only if they are not already in it and
942 # the children of the current node need to be processed.
942 # the children of the current node need to be processed.
943 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
943 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
944 current_issue.children.each do |child|
944 current_issue.children.each do |child|
945 next if except.include?(child)
945 next if except.include?(child)
946
946
947 if (issue_status[child] == eNOT_DISCOVERED)
947 if (issue_status[child] == eNOT_DISCOVERED)
948 queue << child
948 queue << child
949 issue_status[child] = ePROCESS_ALL
949 issue_status[child] = ePROCESS_ALL
950 elsif (issue_status[child] == eRELATIONS_PROCESSED)
950 elsif (issue_status[child] == eRELATIONS_PROCESSED)
951 queue << child
951 queue << child
952 issue_status[child] = ePROCESS_CHILDREN_ONLY
952 issue_status[child] = ePROCESS_CHILDREN_ONLY
953 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
953 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
954 queue << child
954 queue << child
955 issue_status[child] = ePROCESS_ALL
955 issue_status[child] = ePROCESS_ALL
956 end
956 end
957 end
957 end
958 end
958 end
959
959
960 # Add related issues to the queue, if they are not already in it.
960 # Add related issues to the queue, if they are not already in it.
961 current_issue.relations_from.map(&:issue_to).each do |related_issue|
961 current_issue.relations_from.map(&:issue_to).each do |related_issue|
962 next if except.include?(related_issue)
962 next if except.include?(related_issue)
963
963
964 if (issue_status[related_issue] == eNOT_DISCOVERED)
964 if (issue_status[related_issue] == eNOT_DISCOVERED)
965 queue << related_issue
965 queue << related_issue
966 issue_status[related_issue] = ePROCESS_ALL
966 issue_status[related_issue] = ePROCESS_ALL
967 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
967 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
968 queue << related_issue
968 queue << related_issue
969 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
969 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
970 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
970 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
971 queue << related_issue
971 queue << related_issue
972 issue_status[related_issue] = ePROCESS_ALL
972 issue_status[related_issue] = ePROCESS_ALL
973 end
973 end
974 end
974 end
975
975
976 # Set new status for current issue
976 # Set new status for current issue
977 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
977 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
978 issue_status[current_issue] = eALL_PROCESSED
978 issue_status[current_issue] = eALL_PROCESSED
979 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
979 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
980 issue_status[current_issue] = eRELATIONS_PROCESSED
980 issue_status[current_issue] = eRELATIONS_PROCESSED
981 end
981 end
982 end # while
982 end # while
983
983
984 # Remove the issues from the "except" parameter from the result array
984 # Remove the issues from the "except" parameter from the result array
985 dependencies -= except
985 dependencies -= except
986 dependencies.delete(self)
986 dependencies.delete(self)
987
987
988 dependencies
988 dependencies
989 end
989 end
990
990
991 # Returns an array of issues that duplicate this one
991 # Returns an array of issues that duplicate this one
992 def duplicates
992 def duplicates
993 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
993 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
994 end
994 end
995
995
996 # Returns the due date or the target due date if any
996 # Returns the due date or the target due date if any
997 # Used on gantt chart
997 # Used on gantt chart
998 def due_before
998 def due_before
999 due_date || (fixed_version ? fixed_version.effective_date : nil)
999 due_date || (fixed_version ? fixed_version.effective_date : nil)
1000 end
1000 end
1001
1001
1002 # Returns the time scheduled for this issue.
1002 # Returns the time scheduled for this issue.
1003 #
1003 #
1004 # Example:
1004 # Example:
1005 # Start Date: 2/26/09, End Date: 3/04/09
1005 # Start Date: 2/26/09, End Date: 3/04/09
1006 # duration => 6
1006 # duration => 6
1007 def duration
1007 def duration
1008 (start_date && due_date) ? due_date - start_date : 0
1008 (start_date && due_date) ? due_date - start_date : 0
1009 end
1009 end
1010
1010
1011 # Returns the duration in working days
1011 # Returns the duration in working days
1012 def working_duration
1012 def working_duration
1013 (start_date && due_date) ? working_days(start_date, due_date) : 0
1013 (start_date && due_date) ? working_days(start_date, due_date) : 0
1014 end
1014 end
1015
1015
1016 def soonest_start(reload=false)
1016 def soonest_start(reload=false)
1017 @soonest_start = nil if reload
1017 @soonest_start = nil if reload
1018 @soonest_start ||= (
1018 @soonest_start ||= (
1019 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1019 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1020 [(@parent_issue || parent).try(:soonest_start)]
1020 [(@parent_issue || parent).try(:soonest_start)]
1021 ).compact.max
1021 ).compact.max
1022 end
1022 end
1023
1023
1024 # Sets start_date on the given date or the next working day
1024 # Sets start_date on the given date or the next working day
1025 # and changes due_date to keep the same working duration.
1025 # and changes due_date to keep the same working duration.
1026 def reschedule_on(date)
1026 def reschedule_on(date)
1027 wd = working_duration
1027 wd = working_duration
1028 date = next_working_date(date)
1028 date = next_working_date(date)
1029 self.start_date = date
1029 self.start_date = date
1030 self.due_date = add_working_days(date, wd)
1030 self.due_date = add_working_days(date, wd)
1031 end
1031 end
1032
1032
1033 # Reschedules the issue on the given date or the next working day and saves the record.
1033 # Reschedules the issue on the given date or the next working day and saves the record.
1034 # If the issue is a parent task, this is done by rescheduling its subtasks.
1034 # If the issue is a parent task, this is done by rescheduling its subtasks.
1035 def reschedule_on!(date)
1035 def reschedule_on!(date)
1036 return if date.nil?
1036 return if date.nil?
1037 if leaf?
1037 if leaf?
1038 if start_date.nil? || start_date != date
1038 if start_date.nil? || start_date != date
1039 if start_date && start_date > date
1039 if start_date && start_date > date
1040 # Issue can not be moved earlier than its soonest start date
1040 # Issue can not be moved earlier than its soonest start date
1041 date = [soonest_start(true), date].compact.max
1041 date = [soonest_start(true), date].compact.max
1042 end
1042 end
1043 reschedule_on(date)
1043 reschedule_on(date)
1044 begin
1044 begin
1045 save
1045 save
1046 rescue ActiveRecord::StaleObjectError
1046 rescue ActiveRecord::StaleObjectError
1047 reload
1047 reload
1048 reschedule_on(date)
1048 reschedule_on(date)
1049 save
1049 save
1050 end
1050 end
1051 end
1051 end
1052 else
1052 else
1053 leaves.each do |leaf|
1053 leaves.each do |leaf|
1054 if leaf.start_date
1054 if leaf.start_date
1055 # Only move subtask if it starts at the same date as the parent
1055 # Only move subtask if it starts at the same date as the parent
1056 # or if it starts before the given date
1056 # or if it starts before the given date
1057 if start_date == leaf.start_date || date > leaf.start_date
1057 if start_date == leaf.start_date || date > leaf.start_date
1058 leaf.reschedule_on!(date)
1058 leaf.reschedule_on!(date)
1059 end
1059 end
1060 else
1060 else
1061 leaf.reschedule_on!(date)
1061 leaf.reschedule_on!(date)
1062 end
1062 end
1063 end
1063 end
1064 end
1064 end
1065 end
1065 end
1066
1066
1067 def <=>(issue)
1067 def <=>(issue)
1068 if issue.nil?
1068 if issue.nil?
1069 -1
1069 -1
1070 elsif root_id != issue.root_id
1070 elsif root_id != issue.root_id
1071 (root_id || 0) <=> (issue.root_id || 0)
1071 (root_id || 0) <=> (issue.root_id || 0)
1072 else
1072 else
1073 (lft || 0) <=> (issue.lft || 0)
1073 (lft || 0) <=> (issue.lft || 0)
1074 end
1074 end
1075 end
1075 end
1076
1076
1077 def to_s
1077 def to_s
1078 "#{tracker} ##{id}: #{subject}"
1078 "#{tracker} ##{id}: #{subject}"
1079 end
1079 end
1080
1080
1081 # Returns a string of css classes that apply to the issue
1081 # Returns a string of css classes that apply to the issue
1082 def css_classes(user=User.current)
1082 def css_classes(user=User.current)
1083 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1083 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1084 s << ' closed' if closed?
1084 s << ' closed' if closed?
1085 s << ' overdue' if overdue?
1085 s << ' overdue' if overdue?
1086 s << ' child' if child?
1086 s << ' child' if child?
1087 s << ' parent' unless leaf?
1087 s << ' parent' unless leaf?
1088 s << ' private' if is_private?
1088 s << ' private' if is_private?
1089 if user.logged?
1089 if user.logged?
1090 s << ' created-by-me' if author_id == user.id
1090 s << ' created-by-me' if author_id == user.id
1091 s << ' assigned-to-me' if assigned_to_id == user.id
1091 s << ' assigned-to-me' if assigned_to_id == user.id
1092 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id = assigned_to_id}
1092 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id = assigned_to_id}
1093 end
1093 end
1094 s
1094 s
1095 end
1095 end
1096
1096
1097 # Unassigns issues from +version+ if it's no longer shared with issue's project
1097 # Unassigns issues from +version+ if it's no longer shared with issue's project
1098 def self.update_versions_from_sharing_change(version)
1098 def self.update_versions_from_sharing_change(version)
1099 # Update issues assigned to the version
1099 # Update issues assigned to the version
1100 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1100 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1101 end
1101 end
1102
1102
1103 # Unassigns issues from versions that are no longer shared
1103 # Unassigns issues from versions that are no longer shared
1104 # after +project+ was moved
1104 # after +project+ was moved
1105 def self.update_versions_from_hierarchy_change(project)
1105 def self.update_versions_from_hierarchy_change(project)
1106 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1106 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1107 # Update issues of the moved projects and issues assigned to a version of a moved project
1107 # Update issues of the moved projects and issues assigned to a version of a moved project
1108 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
1108 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
1109 end
1109 end
1110
1110
1111 def parent_issue_id=(arg)
1111 def parent_issue_id=(arg)
1112 s = arg.to_s.strip.presence
1112 s = arg.to_s.strip.presence
1113 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1113 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1114 @parent_issue.id
1114 @parent_issue.id
1115 @invalid_parent_issue_id = nil
1115 @invalid_parent_issue_id = nil
1116 elsif s.blank?
1116 elsif s.blank?
1117 @parent_issue = nil
1117 @parent_issue = nil
1118 @invalid_parent_issue_id = nil
1118 @invalid_parent_issue_id = nil
1119 else
1119 else
1120 @parent_issue = nil
1120 @parent_issue = nil
1121 @invalid_parent_issue_id = arg
1121 @invalid_parent_issue_id = arg
1122 end
1122 end
1123 end
1123 end
1124
1124
1125 def parent_issue_id
1125 def parent_issue_id
1126 if @invalid_parent_issue_id
1126 if @invalid_parent_issue_id
1127 @invalid_parent_issue_id
1127 @invalid_parent_issue_id
1128 elsif instance_variable_defined? :@parent_issue
1128 elsif instance_variable_defined? :@parent_issue
1129 @parent_issue.nil? ? nil : @parent_issue.id
1129 @parent_issue.nil? ? nil : @parent_issue.id
1130 else
1130 else
1131 parent_id
1131 parent_id
1132 end
1132 end
1133 end
1133 end
1134
1134
1135 # Returns true if issue's project is a valid
1135 # Returns true if issue's project is a valid
1136 # parent issue project
1136 # parent issue project
1137 def valid_parent_project?(issue=parent)
1137 def valid_parent_project?(issue=parent)
1138 return true if issue.nil? || issue.project_id == project_id
1138 return true if issue.nil? || issue.project_id == project_id
1139
1139
1140 case Setting.cross_project_subtasks
1140 case Setting.cross_project_subtasks
1141 when 'system'
1141 when 'system'
1142 true
1142 true
1143 when 'tree'
1143 when 'tree'
1144 issue.project.root == project.root
1144 issue.project.root == project.root
1145 when 'hierarchy'
1145 when 'hierarchy'
1146 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1146 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1147 when 'descendants'
1147 when 'descendants'
1148 issue.project.is_or_is_ancestor_of?(project)
1148 issue.project.is_or_is_ancestor_of?(project)
1149 else
1149 else
1150 false
1150 false
1151 end
1151 end
1152 end
1152 end
1153
1153
1154 # Extracted from the ReportsController.
1154 # Extracted from the ReportsController.
1155 def self.by_tracker(project)
1155 def self.by_tracker(project)
1156 count_and_group_by(:project => project,
1156 count_and_group_by(:project => project,
1157 :field => 'tracker_id',
1157 :field => 'tracker_id',
1158 :joins => Tracker.table_name)
1158 :joins => Tracker.table_name)
1159 end
1159 end
1160
1160
1161 def self.by_version(project)
1161 def self.by_version(project)
1162 count_and_group_by(:project => project,
1162 count_and_group_by(:project => project,
1163 :field => 'fixed_version_id',
1163 :field => 'fixed_version_id',
1164 :joins => Version.table_name)
1164 :joins => Version.table_name)
1165 end
1165 end
1166
1166
1167 def self.by_priority(project)
1167 def self.by_priority(project)
1168 count_and_group_by(:project => project,
1168 count_and_group_by(:project => project,
1169 :field => 'priority_id',
1169 :field => 'priority_id',
1170 :joins => IssuePriority.table_name)
1170 :joins => IssuePriority.table_name)
1171 end
1171 end
1172
1172
1173 def self.by_category(project)
1173 def self.by_category(project)
1174 count_and_group_by(:project => project,
1174 count_and_group_by(:project => project,
1175 :field => 'category_id',
1175 :field => 'category_id',
1176 :joins => IssueCategory.table_name)
1176 :joins => IssueCategory.table_name)
1177 end
1177 end
1178
1178
1179 def self.by_assigned_to(project)
1179 def self.by_assigned_to(project)
1180 count_and_group_by(:project => project,
1180 count_and_group_by(:project => project,
1181 :field => 'assigned_to_id',
1181 :field => 'assigned_to_id',
1182 :joins => User.table_name)
1182 :joins => User.table_name)
1183 end
1183 end
1184
1184
1185 def self.by_author(project)
1185 def self.by_author(project)
1186 count_and_group_by(:project => project,
1186 count_and_group_by(:project => project,
1187 :field => 'author_id',
1187 :field => 'author_id',
1188 :joins => User.table_name)
1188 :joins => User.table_name)
1189 end
1189 end
1190
1190
1191 def self.by_subproject(project)
1191 def self.by_subproject(project)
1192 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1192 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1193 s.is_closed as closed,
1193 s.is_closed as closed,
1194 #{Issue.table_name}.project_id as project_id,
1194 #{Issue.table_name}.project_id as project_id,
1195 count(#{Issue.table_name}.id) as total
1195 count(#{Issue.table_name}.id) as total
1196 from
1196 from
1197 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1197 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1198 where
1198 where
1199 #{Issue.table_name}.status_id=s.id
1199 #{Issue.table_name}.status_id=s.id
1200 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1200 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1201 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1201 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1202 and #{Issue.table_name}.project_id <> #{project.id}
1202 and #{Issue.table_name}.project_id <> #{project.id}
1203 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1203 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1204 end
1204 end
1205 # End ReportsController extraction
1205 # End ReportsController extraction
1206
1206
1207 # Returns a scope of projects that user can assign the issue to
1207 # Returns a scope of projects that user can assign the issue to
1208 def allowed_target_projects(user=User.current)
1208 def allowed_target_projects(user=User.current)
1209 if new_record?
1209 if new_record?
1210 Project.where(Project.allowed_to_condition(user, :add_issues))
1210 Project.where(Project.allowed_to_condition(user, :add_issues))
1211 else
1211 else
1212 self.class.allowed_target_projects_on_move(user)
1212 self.class.allowed_target_projects_on_move(user)
1213 end
1213 end
1214 end
1214 end
1215
1215
1216 # Returns a scope of projects that user can move issues to
1216 # Returns a scope of projects that user can move issues to
1217 def self.allowed_target_projects_on_move(user=User.current)
1217 def self.allowed_target_projects_on_move(user=User.current)
1218 Project.where(Project.allowed_to_condition(user, :move_issues))
1218 Project.where(Project.allowed_to_condition(user, :move_issues))
1219 end
1219 end
1220
1220
1221 private
1221 private
1222
1222
1223 def after_project_change
1223 def after_project_change
1224 # Update project_id on related time entries
1224 # Update project_id on related time entries
1225 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
1225 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
1226
1226
1227 # Delete issue relations
1227 # Delete issue relations
1228 unless Setting.cross_project_issue_relations?
1228 unless Setting.cross_project_issue_relations?
1229 relations_from.clear
1229 relations_from.clear
1230 relations_to.clear
1230 relations_to.clear
1231 end
1231 end
1232
1232
1233 # Move subtasks that were in the same project
1233 # Move subtasks that were in the same project
1234 children.each do |child|
1234 children.each do |child|
1235 next unless child.project_id == project_id_was
1235 next unless child.project_id == project_id_was
1236 # Change project and keep project
1236 # Change project and keep project
1237 child.send :project=, project, true
1237 child.send :project=, project, true
1238 unless child.save
1238 unless child.save
1239 raise ActiveRecord::Rollback
1239 raise ActiveRecord::Rollback
1240 end
1240 end
1241 end
1241 end
1242 end
1242 end
1243
1243
1244 # Callback for after the creation of an issue by copy
1244 # Callback for after the creation of an issue by copy
1245 # * adds a "copied to" relation with the copied issue
1245 # * adds a "copied to" relation with the copied issue
1246 # * copies subtasks from the copied issue
1246 # * copies subtasks from the copied issue
1247 def after_create_from_copy
1247 def after_create_from_copy
1248 return unless copy? && !@after_create_from_copy_handled
1248 return unless copy? && !@after_create_from_copy_handled
1249
1249
1250 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1250 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1251 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1251 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1252 unless relation.save
1252 unless relation.save
1253 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1253 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1254 end
1254 end
1255 end
1255 end
1256
1256
1257 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1257 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1258 copy_options = (@copy_options || {}).merge(:subtasks => false)
1258 copy_options = (@copy_options || {}).merge(:subtasks => false)
1259 copied_issue_ids = {@copied_from.id => self.id}
1259 copied_issue_ids = {@copied_from.id => self.id}
1260 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1260 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1261 # Do not copy self when copying an issue as a descendant of the copied issue
1261 # Do not copy self when copying an issue as a descendant of the copied issue
1262 next if child == self
1262 next if child == self
1263 # Do not copy subtasks of issues that were not copied
1263 # Do not copy subtasks of issues that were not copied
1264 next unless copied_issue_ids[child.parent_id]
1264 next unless copied_issue_ids[child.parent_id]
1265 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1265 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1266 unless child.visible?
1266 unless child.visible?
1267 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1267 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1268 next
1268 next
1269 end
1269 end
1270 copy = Issue.new.copy_from(child, copy_options)
1270 copy = Issue.new.copy_from(child, copy_options)
1271 copy.author = author
1271 copy.author = author
1272 copy.project = project
1272 copy.project = project
1273 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1273 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1274 unless copy.save
1274 unless copy.save
1275 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1275 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1276 next
1276 next
1277 end
1277 end
1278 copied_issue_ids[child.id] = copy.id
1278 copied_issue_ids[child.id] = copy.id
1279 end
1279 end
1280 end
1280 end
1281 @after_create_from_copy_handled = true
1281 @after_create_from_copy_handled = true
1282 end
1282 end
1283
1283
1284 def update_nested_set_attributes
1284 def update_nested_set_attributes
1285 if root_id.nil?
1285 if root_id.nil?
1286 # issue was just created
1286 # issue was just created
1287 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1287 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1288 set_default_left_and_right
1288 set_default_left_and_right
1289 Issue.update_all(["root_id = ?, lft = ?, rgt = ?", root_id, lft, rgt], ["id = ?", id])
1289 Issue.where(["id = ?", id]).
1290 update_all(["root_id = ?, lft = ?, rgt = ?", root_id, lft, rgt])
1290 if @parent_issue
1291 if @parent_issue
1291 move_to_child_of(@parent_issue)
1292 move_to_child_of(@parent_issue)
1292 end
1293 end
1293 elsif parent_issue_id != parent_id
1294 elsif parent_issue_id != parent_id
1294 update_nested_set_attributes_on_parent_change
1295 update_nested_set_attributes_on_parent_change
1295 end
1296 end
1296 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1297 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1297 end
1298 end
1298
1299
1299 # Updates the nested set for when an existing issue is moved
1300 # Updates the nested set for when an existing issue is moved
1300 def update_nested_set_attributes_on_parent_change
1301 def update_nested_set_attributes_on_parent_change
1301 former_parent_id = parent_id
1302 former_parent_id = parent_id
1302 # moving an existing issue
1303 # moving an existing issue
1303 if @parent_issue && @parent_issue.root_id == root_id
1304 if @parent_issue && @parent_issue.root_id == root_id
1304 # inside the same tree
1305 # inside the same tree
1305 move_to_child_of(@parent_issue)
1306 move_to_child_of(@parent_issue)
1306 else
1307 else
1307 # to another tree
1308 # to another tree
1308 unless root?
1309 unless root?
1309 move_to_right_of(root)
1310 move_to_right_of(root)
1310 end
1311 end
1311 old_root_id = root_id
1312 old_root_id = root_id
1312 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1313 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1313 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1314 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1314 offset = target_maxright + 1 - lft
1315 offset = target_maxright + 1 - lft
1315 Issue.update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset],
1316 Issue.update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset],
1316 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1317 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1317 self[left_column_name] = lft + offset
1318 self[left_column_name] = lft + offset
1318 self[right_column_name] = rgt + offset
1319 self[right_column_name] = rgt + offset
1319 if @parent_issue
1320 if @parent_issue
1320 move_to_child_of(@parent_issue)
1321 move_to_child_of(@parent_issue)
1321 end
1322 end
1322 end
1323 end
1323 # delete invalid relations of all descendants
1324 # delete invalid relations of all descendants
1324 self_and_descendants.each do |issue|
1325 self_and_descendants.each do |issue|
1325 issue.relations.each do |relation|
1326 issue.relations.each do |relation|
1326 relation.destroy unless relation.valid?
1327 relation.destroy unless relation.valid?
1327 end
1328 end
1328 end
1329 end
1329 # update former parent
1330 # update former parent
1330 recalculate_attributes_for(former_parent_id) if former_parent_id
1331 recalculate_attributes_for(former_parent_id) if former_parent_id
1331 end
1332 end
1332
1333
1333 def update_parent_attributes
1334 def update_parent_attributes
1334 recalculate_attributes_for(parent_id) if parent_id
1335 recalculate_attributes_for(parent_id) if parent_id
1335 end
1336 end
1336
1337
1337 def recalculate_attributes_for(issue_id)
1338 def recalculate_attributes_for(issue_id)
1338 if issue_id && p = Issue.find_by_id(issue_id)
1339 if issue_id && p = Issue.find_by_id(issue_id)
1339 # priority = highest priority of children
1340 # priority = highest priority of children
1340 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1341 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1341 p.priority = IssuePriority.find_by_position(priority_position)
1342 p.priority = IssuePriority.find_by_position(priority_position)
1342 end
1343 end
1343
1344
1344 # start/due dates = lowest/highest dates of children
1345 # start/due dates = lowest/highest dates of children
1345 p.start_date = p.children.minimum(:start_date)
1346 p.start_date = p.children.minimum(:start_date)
1346 p.due_date = p.children.maximum(:due_date)
1347 p.due_date = p.children.maximum(:due_date)
1347 if p.start_date && p.due_date && p.due_date < p.start_date
1348 if p.start_date && p.due_date && p.due_date < p.start_date
1348 p.start_date, p.due_date = p.due_date, p.start_date
1349 p.start_date, p.due_date = p.due_date, p.start_date
1349 end
1350 end
1350
1351
1351 # done ratio = weighted average ratio of leaves
1352 # done ratio = weighted average ratio of leaves
1352 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1353 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1353 leaves_count = p.leaves.count
1354 leaves_count = p.leaves.count
1354 if leaves_count > 0
1355 if leaves_count > 0
1355 average = p.leaves.average(:estimated_hours).to_f
1356 average = p.leaves.average(:estimated_hours).to_f
1356 if average == 0
1357 if average == 0
1357 average = 1
1358 average = 1
1358 end
1359 end
1359 done = p.leaves.sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1360 done = p.leaves.sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1360 "* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1361 "* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1361 progress = done / (average * leaves_count)
1362 progress = done / (average * leaves_count)
1362 p.done_ratio = progress.round
1363 p.done_ratio = progress.round
1363 end
1364 end
1364 end
1365 end
1365
1366
1366 # estimate = sum of leaves estimates
1367 # estimate = sum of leaves estimates
1367 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1368 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1368 p.estimated_hours = nil if p.estimated_hours == 0.0
1369 p.estimated_hours = nil if p.estimated_hours == 0.0
1369
1370
1370 # ancestors will be recursively updated
1371 # ancestors will be recursively updated
1371 p.save(:validate => false)
1372 p.save(:validate => false)
1372 end
1373 end
1373 end
1374 end
1374
1375
1375 # Update issues so their versions are not pointing to a
1376 # Update issues so their versions are not pointing to a
1376 # fixed_version that is not shared with the issue's project
1377 # fixed_version that is not shared with the issue's project
1377 def self.update_versions(conditions=nil)
1378 def self.update_versions(conditions=nil)
1378 # Only need to update issues with a fixed_version from
1379 # Only need to update issues with a fixed_version from
1379 # a different project and that is not systemwide shared
1380 # a different project and that is not systemwide shared
1380 Issue.includes(:project, :fixed_version).
1381 Issue.includes(:project, :fixed_version).
1381 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1382 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1382 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1383 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1383 " AND #{Version.table_name}.sharing <> 'system'").
1384 " AND #{Version.table_name}.sharing <> 'system'").
1384 where(conditions).each do |issue|
1385 where(conditions).each do |issue|
1385 next if issue.project.nil? || issue.fixed_version.nil?
1386 next if issue.project.nil? || issue.fixed_version.nil?
1386 unless issue.project.shared_versions.include?(issue.fixed_version)
1387 unless issue.project.shared_versions.include?(issue.fixed_version)
1387 issue.init_journal(User.current)
1388 issue.init_journal(User.current)
1388 issue.fixed_version = nil
1389 issue.fixed_version = nil
1389 issue.save
1390 issue.save
1390 end
1391 end
1391 end
1392 end
1392 end
1393 end
1393
1394
1394 # Callback on file attachment
1395 # Callback on file attachment
1395 def attachment_added(obj)
1396 def attachment_added(obj)
1396 if @current_journal && !obj.new_record?
1397 if @current_journal && !obj.new_record?
1397 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1398 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1398 end
1399 end
1399 end
1400 end
1400
1401
1401 # Callback on attachment deletion
1402 # Callback on attachment deletion
1402 def attachment_removed(obj)
1403 def attachment_removed(obj)
1403 if @current_journal && !obj.new_record?
1404 if @current_journal && !obj.new_record?
1404 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1405 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1405 @current_journal.save
1406 @current_journal.save
1406 end
1407 end
1407 end
1408 end
1408
1409
1409 # Default assignment based on category
1410 # Default assignment based on category
1410 def default_assign
1411 def default_assign
1411 if assigned_to.nil? && category && category.assigned_to
1412 if assigned_to.nil? && category && category.assigned_to
1412 self.assigned_to = category.assigned_to
1413 self.assigned_to = category.assigned_to
1413 end
1414 end
1414 end
1415 end
1415
1416
1416 # Updates start/due dates of following issues
1417 # Updates start/due dates of following issues
1417 def reschedule_following_issues
1418 def reschedule_following_issues
1418 if start_date_changed? || due_date_changed?
1419 if start_date_changed? || due_date_changed?
1419 relations_from.each do |relation|
1420 relations_from.each do |relation|
1420 relation.set_issue_to_dates
1421 relation.set_issue_to_dates
1421 end
1422 end
1422 end
1423 end
1423 end
1424 end
1424
1425
1425 # Closes duplicates if the issue is being closed
1426 # Closes duplicates if the issue is being closed
1426 def close_duplicates
1427 def close_duplicates
1427 if closing?
1428 if closing?
1428 duplicates.each do |duplicate|
1429 duplicates.each do |duplicate|
1429 # Reload is need in case the duplicate was updated by a previous duplicate
1430 # Reload is need in case the duplicate was updated by a previous duplicate
1430 duplicate.reload
1431 duplicate.reload
1431 # Don't re-close it if it's already closed
1432 # Don't re-close it if it's already closed
1432 next if duplicate.closed?
1433 next if duplicate.closed?
1433 # Same user and notes
1434 # Same user and notes
1434 if @current_journal
1435 if @current_journal
1435 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1436 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1436 end
1437 end
1437 duplicate.update_attribute :status, self.status
1438 duplicate.update_attribute :status, self.status
1438 end
1439 end
1439 end
1440 end
1440 end
1441 end
1441
1442
1442 # Make sure updated_on is updated when adding a note and set updated_on now
1443 # Make sure updated_on is updated when adding a note and set updated_on now
1443 # so we can set closed_on with the same value on closing
1444 # so we can set closed_on with the same value on closing
1444 def force_updated_on_change
1445 def force_updated_on_change
1445 if @current_journal || changed?
1446 if @current_journal || changed?
1446 self.updated_on = current_time_from_proper_timezone
1447 self.updated_on = current_time_from_proper_timezone
1447 if new_record?
1448 if new_record?
1448 self.created_on = updated_on
1449 self.created_on = updated_on
1449 end
1450 end
1450 end
1451 end
1451 end
1452 end
1452
1453
1453 # Callback for setting closed_on when the issue is closed.
1454 # Callback for setting closed_on when the issue is closed.
1454 # The closed_on attribute stores the time of the last closing
1455 # The closed_on attribute stores the time of the last closing
1455 # and is preserved when the issue is reopened.
1456 # and is preserved when the issue is reopened.
1456 def update_closed_on
1457 def update_closed_on
1457 if closing? || (new_record? && closed?)
1458 if closing? || (new_record? && closed?)
1458 self.closed_on = updated_on
1459 self.closed_on = updated_on
1459 end
1460 end
1460 end
1461 end
1461
1462
1462 # Saves the changes in a Journal
1463 # Saves the changes in a Journal
1463 # Called after_save
1464 # Called after_save
1464 def create_journal
1465 def create_journal
1465 if @current_journal
1466 if @current_journal
1466 # attributes changes
1467 # attributes changes
1467 if @attributes_before_change
1468 if @attributes_before_change
1468 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1469 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1469 before = @attributes_before_change[c]
1470 before = @attributes_before_change[c]
1470 after = send(c)
1471 after = send(c)
1471 next if before == after || (before.blank? && after.blank?)
1472 next if before == after || (before.blank? && after.blank?)
1472 @current_journal.details << JournalDetail.new(:property => 'attr',
1473 @current_journal.details << JournalDetail.new(:property => 'attr',
1473 :prop_key => c,
1474 :prop_key => c,
1474 :old_value => before,
1475 :old_value => before,
1475 :value => after)
1476 :value => after)
1476 }
1477 }
1477 end
1478 end
1478 if @custom_values_before_change
1479 if @custom_values_before_change
1479 # custom fields changes
1480 # custom fields changes
1480 custom_field_values.each {|c|
1481 custom_field_values.each {|c|
1481 before = @custom_values_before_change[c.custom_field_id]
1482 before = @custom_values_before_change[c.custom_field_id]
1482 after = c.value
1483 after = c.value
1483 next if before == after || (before.blank? && after.blank?)
1484 next if before == after || (before.blank? && after.blank?)
1484
1485
1485 if before.is_a?(Array) || after.is_a?(Array)
1486 if before.is_a?(Array) || after.is_a?(Array)
1486 before = [before] unless before.is_a?(Array)
1487 before = [before] unless before.is_a?(Array)
1487 after = [after] unless after.is_a?(Array)
1488 after = [after] unless after.is_a?(Array)
1488
1489
1489 # values removed
1490 # values removed
1490 (before - after).reject(&:blank?).each do |value|
1491 (before - after).reject(&:blank?).each do |value|
1491 @current_journal.details << JournalDetail.new(:property => 'cf',
1492 @current_journal.details << JournalDetail.new(:property => 'cf',
1492 :prop_key => c.custom_field_id,
1493 :prop_key => c.custom_field_id,
1493 :old_value => value,
1494 :old_value => value,
1494 :value => nil)
1495 :value => nil)
1495 end
1496 end
1496 # values added
1497 # values added
1497 (after - before).reject(&:blank?).each do |value|
1498 (after - before).reject(&:blank?).each do |value|
1498 @current_journal.details << JournalDetail.new(:property => 'cf',
1499 @current_journal.details << JournalDetail.new(:property => 'cf',
1499 :prop_key => c.custom_field_id,
1500 :prop_key => c.custom_field_id,
1500 :old_value => nil,
1501 :old_value => nil,
1501 :value => value)
1502 :value => value)
1502 end
1503 end
1503 else
1504 else
1504 @current_journal.details << JournalDetail.new(:property => 'cf',
1505 @current_journal.details << JournalDetail.new(:property => 'cf',
1505 :prop_key => c.custom_field_id,
1506 :prop_key => c.custom_field_id,
1506 :old_value => before,
1507 :old_value => before,
1507 :value => after)
1508 :value => after)
1508 end
1509 end
1509 }
1510 }
1510 end
1511 end
1511 @current_journal.save
1512 @current_journal.save
1512 # reset current journal
1513 # reset current journal
1513 init_journal @current_journal.user, @current_journal.notes
1514 init_journal @current_journal.user, @current_journal.notes
1514 end
1515 end
1515 end
1516 end
1516
1517
1517 def send_notification
1518 def send_notification
1518 if Setting.notified_events.include?('issue_added')
1519 if Setting.notified_events.include?('issue_added')
1519 Mailer.deliver_issue_add(self)
1520 Mailer.deliver_issue_add(self)
1520 end
1521 end
1521 end
1522 end
1522
1523
1523 # Stores the previous assignee so we can still have access
1524 # Stores the previous assignee so we can still have access
1524 # to it during after_save callbacks (assigned_to_id_was is reset)
1525 # to it during after_save callbacks (assigned_to_id_was is reset)
1525 def set_assigned_to_was
1526 def set_assigned_to_was
1526 @previous_assigned_to_id = assigned_to_id_was
1527 @previous_assigned_to_id = assigned_to_id_was
1527 end
1528 end
1528
1529
1529 # Clears the previous assignee at the end of after_save callbacks
1530 # Clears the previous assignee at the end of after_save callbacks
1530 def clear_assigned_to_was
1531 def clear_assigned_to_was
1531 @assigned_to_was = nil
1532 @assigned_to_was = nil
1532 @previous_assigned_to_id = nil
1533 @previous_assigned_to_id = nil
1533 end
1534 end
1534
1535
1535 # Query generator for selecting groups of issue counts for a project
1536 # Query generator for selecting groups of issue counts for a project
1536 # based on specific criteria
1537 # based on specific criteria
1537 #
1538 #
1538 # Options
1539 # Options
1539 # * project - Project to search in.
1540 # * project - Project to search in.
1540 # * field - String. Issue field to key off of in the grouping.
1541 # * field - String. Issue field to key off of in the grouping.
1541 # * joins - String. The table name to join against.
1542 # * joins - String. The table name to join against.
1542 def self.count_and_group_by(options)
1543 def self.count_and_group_by(options)
1543 project = options.delete(:project)
1544 project = options.delete(:project)
1544 select_field = options.delete(:field)
1545 select_field = options.delete(:field)
1545 joins = options.delete(:joins)
1546 joins = options.delete(:joins)
1546
1547
1547 where = "#{Issue.table_name}.#{select_field}=j.id"
1548 where = "#{Issue.table_name}.#{select_field}=j.id"
1548
1549
1549 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1550 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1550 s.is_closed as closed,
1551 s.is_closed as closed,
1551 j.id as #{select_field},
1552 j.id as #{select_field},
1552 count(#{Issue.table_name}.id) as total
1553 count(#{Issue.table_name}.id) as total
1553 from
1554 from
1554 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1555 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1555 where
1556 where
1556 #{Issue.table_name}.status_id=s.id
1557 #{Issue.table_name}.status_id=s.id
1557 and #{where}
1558 and #{where}
1558 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1559 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1559 and #{visible_condition(User.current, :project => project)}
1560 and #{visible_condition(User.current, :project => project)}
1560 group by s.id, s.is_closed, j.id")
1561 group by s.id, s.is_closed, j.id")
1561 end
1562 end
1562 end
1563 end
General Comments 0
You need to be logged in to leave comments. Login now