##// END OF EJS Templates
Fixed test failure with SQLServer introduced by r13461....
Jean-Philippe Lang -
r13086:51508f67d0cc
parent child
Show More
@@ -1,1571 +1,1572
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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) : []
203 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
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 custom fields that can be edited by the given user
486 # Returns the custom fields that can be edited by the given user
487 def editable_custom_fields(user=nil)
487 def editable_custom_fields(user=nil)
488 editable_custom_field_values(user).map(&:custom_field).uniq
488 editable_custom_field_values(user).map(&:custom_field).uniq
489 end
489 end
490
490
491 # Returns the names of attributes that are read-only for user or the current user
491 # Returns the names of attributes that are read-only for user or the current user
492 # For users with multiple roles, the read-only fields are the intersection of
492 # For users with multiple roles, the read-only fields are the intersection of
493 # read-only fields of each role
493 # read-only fields of each role
494 # The result is an array of strings where sustom fields are represented with their ids
494 # The result is an array of strings where sustom fields are represented with their ids
495 #
495 #
496 # Examples:
496 # Examples:
497 # issue.read_only_attribute_names # => ['due_date', '2']
497 # issue.read_only_attribute_names # => ['due_date', '2']
498 # issue.read_only_attribute_names(user) # => []
498 # issue.read_only_attribute_names(user) # => []
499 def read_only_attribute_names(user=nil)
499 def read_only_attribute_names(user=nil)
500 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
500 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
501 end
501 end
502
502
503 # Returns the names of required attributes for user or the current user
503 # Returns the names of required attributes for user or the current user
504 # For users with multiple roles, the required fields are the intersection of
504 # For users with multiple roles, the required fields are the intersection of
505 # required fields of each role
505 # required fields of each role
506 # The result is an array of strings where sustom fields are represented with their ids
506 # The result is an array of strings where sustom fields are represented with their ids
507 #
507 #
508 # Examples:
508 # Examples:
509 # issue.required_attribute_names # => ['due_date', '2']
509 # issue.required_attribute_names # => ['due_date', '2']
510 # issue.required_attribute_names(user) # => []
510 # issue.required_attribute_names(user) # => []
511 def required_attribute_names(user=nil)
511 def required_attribute_names(user=nil)
512 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
512 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
513 end
513 end
514
514
515 # Returns true if the attribute is required for user
515 # Returns true if the attribute is required for user
516 def required_attribute?(name, user=nil)
516 def required_attribute?(name, user=nil)
517 required_attribute_names(user).include?(name.to_s)
517 required_attribute_names(user).include?(name.to_s)
518 end
518 end
519
519
520 # Returns a hash of the workflow rule by attribute for the given user
520 # Returns a hash of the workflow rule by attribute for the given user
521 #
521 #
522 # Examples:
522 # Examples:
523 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
523 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
524 def workflow_rule_by_attribute(user=nil)
524 def workflow_rule_by_attribute(user=nil)
525 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
525 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
526
526
527 user_real = user || User.current
527 user_real = user || User.current
528 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
528 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
529 return {} if roles.empty?
529 return {} if roles.empty?
530
530
531 result = {}
531 result = {}
532 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id))
532 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id))
533 if workflow_permissions.any?
533 if workflow_permissions.any?
534 workflow_rules = workflow_permissions.inject({}) do |h, wp|
534 workflow_rules = workflow_permissions.inject({}) do |h, wp|
535 h[wp.field_name] ||= []
535 h[wp.field_name] ||= []
536 h[wp.field_name] << wp.rule
536 h[wp.field_name] << wp.rule
537 h
537 h
538 end
538 end
539 workflow_rules.each do |attr, rules|
539 workflow_rules.each do |attr, rules|
540 next if rules.size < roles.size
540 next if rules.size < roles.size
541 uniq_rules = rules.uniq
541 uniq_rules = rules.uniq
542 if uniq_rules.size == 1
542 if uniq_rules.size == 1
543 result[attr] = uniq_rules.first
543 result[attr] = uniq_rules.first
544 else
544 else
545 result[attr] = 'required'
545 result[attr] = 'required'
546 end
546 end
547 end
547 end
548 end
548 end
549 @workflow_rule_by_attribute = result if user.nil?
549 @workflow_rule_by_attribute = result if user.nil?
550 result
550 result
551 end
551 end
552 private :workflow_rule_by_attribute
552 private :workflow_rule_by_attribute
553
553
554 def done_ratio
554 def done_ratio
555 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
555 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
556 status.default_done_ratio
556 status.default_done_ratio
557 else
557 else
558 read_attribute(:done_ratio)
558 read_attribute(:done_ratio)
559 end
559 end
560 end
560 end
561
561
562 def self.use_status_for_done_ratio?
562 def self.use_status_for_done_ratio?
563 Setting.issue_done_ratio == 'issue_status'
563 Setting.issue_done_ratio == 'issue_status'
564 end
564 end
565
565
566 def self.use_field_for_done_ratio?
566 def self.use_field_for_done_ratio?
567 Setting.issue_done_ratio == 'issue_field'
567 Setting.issue_done_ratio == 'issue_field'
568 end
568 end
569
569
570 def validate_issue
570 def validate_issue
571 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
571 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
572 errors.add :due_date, :greater_than_start_date
572 errors.add :due_date, :greater_than_start_date
573 end
573 end
574
574
575 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
575 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
576 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
576 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
577 end
577 end
578
578
579 if fixed_version
579 if fixed_version
580 if !assignable_versions.include?(fixed_version)
580 if !assignable_versions.include?(fixed_version)
581 errors.add :fixed_version_id, :inclusion
581 errors.add :fixed_version_id, :inclusion
582 elsif reopened? && fixed_version.closed?
582 elsif reopened? && fixed_version.closed?
583 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
583 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
584 end
584 end
585 end
585 end
586
586
587 # Checks that the issue can not be added/moved to a disabled tracker
587 # Checks that the issue can not be added/moved to a disabled tracker
588 if project && (tracker_id_changed? || project_id_changed?)
588 if project && (tracker_id_changed? || project_id_changed?)
589 unless project.trackers.include?(tracker)
589 unless project.trackers.include?(tracker)
590 errors.add :tracker_id, :inclusion
590 errors.add :tracker_id, :inclusion
591 end
591 end
592 end
592 end
593
593
594 # Checks parent issue assignment
594 # Checks parent issue assignment
595 if @invalid_parent_issue_id.present?
595 if @invalid_parent_issue_id.present?
596 errors.add :parent_issue_id, :invalid
596 errors.add :parent_issue_id, :invalid
597 elsif @parent_issue
597 elsif @parent_issue
598 if !valid_parent_project?(@parent_issue)
598 if !valid_parent_project?(@parent_issue)
599 errors.add :parent_issue_id, :invalid
599 errors.add :parent_issue_id, :invalid
600 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
600 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
601 errors.add :parent_issue_id, :invalid
601 errors.add :parent_issue_id, :invalid
602 elsif !new_record?
602 elsif !new_record?
603 # moving an existing issue
603 # moving an existing issue
604 if @parent_issue.root_id != root_id
604 if @parent_issue.root_id != root_id
605 # we can always move to another tree
605 # we can always move to another tree
606 elsif move_possible?(@parent_issue)
606 elsif move_possible?(@parent_issue)
607 # move accepted inside tree
607 # move accepted inside tree
608 else
608 else
609 errors.add :parent_issue_id, :invalid
609 errors.add :parent_issue_id, :invalid
610 end
610 end
611 end
611 end
612 end
612 end
613 end
613 end
614
614
615 # Validates the issue against additional workflow requirements
615 # Validates the issue against additional workflow requirements
616 def validate_required_fields
616 def validate_required_fields
617 user = new_record? ? author : current_journal.try(:user)
617 user = new_record? ? author : current_journal.try(:user)
618
618
619 required_attribute_names(user).each do |attribute|
619 required_attribute_names(user).each do |attribute|
620 if attribute =~ /^\d+$/
620 if attribute =~ /^\d+$/
621 attribute = attribute.to_i
621 attribute = attribute.to_i
622 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
622 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
623 if v && v.value.blank?
623 if v && v.value.blank?
624 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
624 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
625 end
625 end
626 else
626 else
627 if respond_to?(attribute) && send(attribute).blank?
627 if respond_to?(attribute) && send(attribute).blank?
628 errors.add attribute, :blank
628 errors.add attribute, :blank
629 end
629 end
630 end
630 end
631 end
631 end
632 end
632 end
633
633
634 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
634 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
635 # even if the user turns off the setting later
635 # even if the user turns off the setting later
636 def update_done_ratio_from_issue_status
636 def update_done_ratio_from_issue_status
637 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
637 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
638 self.done_ratio = status.default_done_ratio
638 self.done_ratio = status.default_done_ratio
639 end
639 end
640 end
640 end
641
641
642 def init_journal(user, notes = "")
642 def init_journal(user, notes = "")
643 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
643 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
644 if new_record?
644 if new_record?
645 @current_journal.notify = false
645 @current_journal.notify = false
646 else
646 else
647 @attributes_before_change = attributes.dup
647 @attributes_before_change = attributes.dup
648 @custom_values_before_change = {}
648 @custom_values_before_change = {}
649 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
649 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
650 end
650 end
651 @current_journal
651 @current_journal
652 end
652 end
653
653
654 # Returns the id of the last journal or nil
654 # Returns the id of the last journal or nil
655 def last_journal_id
655 def last_journal_id
656 if new_record?
656 if new_record?
657 nil
657 nil
658 else
658 else
659 journals.maximum(:id)
659 journals.maximum(:id)
660 end
660 end
661 end
661 end
662
662
663 # Returns a scope for journals that have an id greater than journal_id
663 # Returns a scope for journals that have an id greater than journal_id
664 def journals_after(journal_id)
664 def journals_after(journal_id)
665 scope = journals.reorder("#{Journal.table_name}.id ASC")
665 scope = journals.reorder("#{Journal.table_name}.id ASC")
666 if journal_id.present?
666 if journal_id.present?
667 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
667 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
668 end
668 end
669 scope
669 scope
670 end
670 end
671
671
672 # Returns the initial status of the issue
672 # Returns the initial status of the issue
673 # Returns nil for a new issue
673 # Returns nil for a new issue
674 def status_was
674 def status_was
675 if status_id_was && status_id_was.to_i > 0
675 if status_id_was && status_id_was.to_i > 0
676 @status_was ||= IssueStatus.find_by_id(status_id_was)
676 @status_was ||= IssueStatus.find_by_id(status_id_was)
677 end
677 end
678 end
678 end
679
679
680 # Return true if the issue is closed, otherwise false
680 # Return true if the issue is closed, otherwise false
681 def closed?
681 def closed?
682 self.status.is_closed?
682 self.status.is_closed?
683 end
683 end
684
684
685 # Return true if the issue is being reopened
685 # Return true if the issue is being reopened
686 def reopened?
686 def reopened?
687 if !new_record? && status_id_changed?
687 if !new_record? && status_id_changed?
688 status_was = IssueStatus.find_by_id(status_id_was)
688 status_was = IssueStatus.find_by_id(status_id_was)
689 status_new = IssueStatus.find_by_id(status_id)
689 status_new = IssueStatus.find_by_id(status_id)
690 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
690 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
691 return true
691 return true
692 end
692 end
693 end
693 end
694 false
694 false
695 end
695 end
696
696
697 # Return true if the issue is being closed
697 # Return true if the issue is being closed
698 def closing?
698 def closing?
699 if !new_record? && status_id_changed?
699 if !new_record? && status_id_changed?
700 if status_was && status && !status_was.is_closed? && status.is_closed?
700 if status_was && status && !status_was.is_closed? && status.is_closed?
701 return true
701 return true
702 end
702 end
703 end
703 end
704 false
704 false
705 end
705 end
706
706
707 # Returns true if the issue is overdue
707 # Returns true if the issue is overdue
708 def overdue?
708 def overdue?
709 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
709 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
710 end
710 end
711
711
712 # Is the amount of work done less than it should for the due date
712 # Is the amount of work done less than it should for the due date
713 def behind_schedule?
713 def behind_schedule?
714 return false if start_date.nil? || due_date.nil?
714 return false if start_date.nil? || due_date.nil?
715 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
715 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
716 return done_date <= Date.today
716 return done_date <= Date.today
717 end
717 end
718
718
719 # Does this issue have children?
719 # Does this issue have children?
720 def children?
720 def children?
721 !leaf?
721 !leaf?
722 end
722 end
723
723
724 # Users the issue can be assigned to
724 # Users the issue can be assigned to
725 def assignable_users
725 def assignable_users
726 users = project.assignable_users
726 users = project.assignable_users
727 users << author if author
727 users << author if author
728 users << assigned_to if assigned_to
728 users << assigned_to if assigned_to
729 users.uniq.sort
729 users.uniq.sort
730 end
730 end
731
731
732 # Versions that the issue can be assigned to
732 # Versions that the issue can be assigned to
733 def assignable_versions
733 def assignable_versions
734 return @assignable_versions if @assignable_versions
734 return @assignable_versions if @assignable_versions
735
735
736 versions = project.shared_versions.open.all
736 versions = project.shared_versions.open.all
737 if fixed_version
737 if fixed_version
738 if fixed_version_id_changed?
738 if fixed_version_id_changed?
739 # nothing to do
739 # nothing to do
740 elsif project_id_changed?
740 elsif project_id_changed?
741 if project.shared_versions.include?(fixed_version)
741 if project.shared_versions.include?(fixed_version)
742 versions << fixed_version
742 versions << fixed_version
743 end
743 end
744 else
744 else
745 versions << fixed_version
745 versions << fixed_version
746 end
746 end
747 end
747 end
748 @assignable_versions = versions.uniq.sort
748 @assignable_versions = versions.uniq.sort
749 end
749 end
750
750
751 # Returns true if this issue is blocked by another issue that is still open
751 # Returns true if this issue is blocked by another issue that is still open
752 def blocked?
752 def blocked?
753 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
753 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
754 end
754 end
755
755
756 # Returns an array of statuses that user is able to apply
756 # Returns an array of statuses that user is able to apply
757 def new_statuses_allowed_to(user=User.current, include_default=false)
757 def new_statuses_allowed_to(user=User.current, include_default=false)
758 if new_record? && @copied_from
758 if new_record? && @copied_from
759 [IssueStatus.default, @copied_from.status].compact.uniq.sort
759 [IssueStatus.default, @copied_from.status].compact.uniq.sort
760 else
760 else
761 initial_status = nil
761 initial_status = nil
762 if new_record?
762 if new_record?
763 initial_status = IssueStatus.default
763 initial_status = IssueStatus.default
764 elsif status_id_was
764 elsif status_id_was
765 initial_status = IssueStatus.find_by_id(status_id_was)
765 initial_status = IssueStatus.find_by_id(status_id_was)
766 end
766 end
767 initial_status ||= status
767 initial_status ||= status
768
768
769 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
769 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
770 assignee_transitions_allowed = initial_assigned_to_id.present? &&
770 assignee_transitions_allowed = initial_assigned_to_id.present? &&
771 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
771 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
772
772
773 statuses = initial_status.find_new_statuses_allowed_to(
773 statuses = initial_status.find_new_statuses_allowed_to(
774 user.admin ? Role.all : user.roles_for_project(project),
774 user.admin ? Role.all : user.roles_for_project(project),
775 tracker,
775 tracker,
776 author == user,
776 author == user,
777 assignee_transitions_allowed
777 assignee_transitions_allowed
778 )
778 )
779 statuses << initial_status unless statuses.empty?
779 statuses << initial_status unless statuses.empty?
780 statuses << IssueStatus.default if include_default
780 statuses << IssueStatus.default if include_default
781 statuses = statuses.compact.uniq.sort
781 statuses = statuses.compact.uniq.sort
782 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
782 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
783 end
783 end
784 end
784 end
785
785
786 # Returns the previous assignee if changed
786 # Returns the previous assignee if changed
787 def assigned_to_was
787 def assigned_to_was
788 # assigned_to_id_was is reset before after_save callbacks
788 # assigned_to_id_was is reset before after_save callbacks
789 user_id = @previous_assigned_to_id || assigned_to_id_was
789 user_id = @previous_assigned_to_id || assigned_to_id_was
790 if user_id && user_id != assigned_to_id
790 if user_id && user_id != assigned_to_id
791 @assigned_to_was ||= User.find_by_id(user_id)
791 @assigned_to_was ||= User.find_by_id(user_id)
792 end
792 end
793 end
793 end
794
794
795 # Returns the users that should be notified
795 # Returns the users that should be notified
796 def notified_users
796 def notified_users
797 notified = []
797 notified = []
798 # Author and assignee are always notified unless they have been
798 # Author and assignee are always notified unless they have been
799 # locked or don't want to be notified
799 # locked or don't want to be notified
800 notified << author if author
800 notified << author if author
801 if assigned_to
801 if assigned_to
802 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
802 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
803 end
803 end
804 if assigned_to_was
804 if assigned_to_was
805 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
805 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
806 end
806 end
807 notified = notified.select {|u| u.active? && u.notify_about?(self)}
807 notified = notified.select {|u| u.active? && u.notify_about?(self)}
808
808
809 notified += project.notified_users
809 notified += project.notified_users
810 notified.uniq!
810 notified.uniq!
811 # Remove users that can not view the issue
811 # Remove users that can not view the issue
812 notified.reject! {|user| !visible?(user)}
812 notified.reject! {|user| !visible?(user)}
813 notified
813 notified
814 end
814 end
815
815
816 # Returns the email addresses that should be notified
816 # Returns the email addresses that should be notified
817 def recipients
817 def recipients
818 notified_users.collect(&:mail)
818 notified_users.collect(&:mail)
819 end
819 end
820
820
821 def each_notification(users, &block)
821 def each_notification(users, &block)
822 if users.any?
822 if users.any?
823 if custom_field_values.detect {|value| !value.custom_field.visible?}
823 if custom_field_values.detect {|value| !value.custom_field.visible?}
824 users_by_custom_field_visibility = users.group_by do |user|
824 users_by_custom_field_visibility = users.group_by do |user|
825 visible_custom_field_values(user).map(&:custom_field_id).sort
825 visible_custom_field_values(user).map(&:custom_field_id).sort
826 end
826 end
827 users_by_custom_field_visibility.values.each do |users|
827 users_by_custom_field_visibility.values.each do |users|
828 yield(users)
828 yield(users)
829 end
829 end
830 else
830 else
831 yield(users)
831 yield(users)
832 end
832 end
833 end
833 end
834 end
834 end
835
835
836 # Returns the number of hours spent on this issue
836 # Returns the number of hours spent on this issue
837 def spent_hours
837 def spent_hours
838 @spent_hours ||= time_entries.sum(:hours) || 0
838 @spent_hours ||= time_entries.sum(:hours) || 0
839 end
839 end
840
840
841 # Returns the total number of hours spent on this issue and its descendants
841 # Returns the total number of hours spent on this issue and its descendants
842 #
842 #
843 # Example:
843 # Example:
844 # spent_hours => 0.0
844 # spent_hours => 0.0
845 # spent_hours => 50.2
845 # spent_hours => 50.2
846 def total_spent_hours
846 def total_spent_hours
847 @total_spent_hours ||=
847 @total_spent_hours ||=
848 self_and_descendants.
848 self_and_descendants.
849 joins("LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").
849 joins("LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").
850 sum("#{TimeEntry.table_name}.hours").to_f || 0.0
850 sum("#{TimeEntry.table_name}.hours").to_f || 0.0
851 end
851 end
852
852
853 def relations
853 def relations
854 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
854 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
855 end
855 end
856
856
857 # Preloads relations for a collection of issues
857 # Preloads relations for a collection of issues
858 def self.load_relations(issues)
858 def self.load_relations(issues)
859 if issues.any?
859 if issues.any?
860 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
860 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
861 issues.each do |issue|
861 issues.each do |issue|
862 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
862 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
863 end
863 end
864 end
864 end
865 end
865 end
866
866
867 # Preloads visible spent time for a collection of issues
867 # Preloads visible spent time for a collection of issues
868 def self.load_visible_spent_hours(issues, user=User.current)
868 def self.load_visible_spent_hours(issues, user=User.current)
869 if issues.any?
869 if issues.any?
870 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
870 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
871 issues.each do |issue|
871 issues.each do |issue|
872 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
872 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
873 end
873 end
874 end
874 end
875 end
875 end
876
876
877 # Preloads visible relations for a collection of issues
877 # Preloads visible relations for a collection of issues
878 def self.load_visible_relations(issues, user=User.current)
878 def self.load_visible_relations(issues, user=User.current)
879 if issues.any?
879 if issues.any?
880 issue_ids = issues.map(&:id)
880 issue_ids = issues.map(&:id)
881 # Relations with issue_from in given issues and visible issue_to
881 # Relations with issue_from in given issues and visible issue_to
882 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
882 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
883 # Relations with issue_to in given issues and visible issue_from
883 # Relations with issue_to in given issues and visible issue_from
884 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
884 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
885
885
886 issues.each do |issue|
886 issues.each do |issue|
887 relations =
887 relations =
888 relations_from.select {|relation| relation.issue_from_id == issue.id} +
888 relations_from.select {|relation| relation.issue_from_id == issue.id} +
889 relations_to.select {|relation| relation.issue_to_id == issue.id}
889 relations_to.select {|relation| relation.issue_to_id == issue.id}
890
890
891 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
891 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
892 end
892 end
893 end
893 end
894 end
894 end
895
895
896 # Finds an issue relation given its id.
896 # Finds an issue relation given its id.
897 def find_relation(relation_id)
897 def find_relation(relation_id)
898 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
898 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
899 end
899 end
900
900
901 # Returns all the other issues that depend on the issue
901 # Returns all the other issues that depend on the issue
902 # The algorithm is a modified breadth first search (bfs)
902 # The algorithm is a modified breadth first search (bfs)
903 def all_dependent_issues(except=[])
903 def all_dependent_issues(except=[])
904 # The found dependencies
904 # The found dependencies
905 dependencies = []
905 dependencies = []
906
906
907 # The visited flag for every node (issue) used by the breadth first search
907 # The visited flag for every node (issue) used by the breadth first search
908 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
908 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
909
909
910 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
910 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
911 # the issue when it is processed.
911 # the issue when it is processed.
912
912
913 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
913 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
914 # but its children will not be added to the queue when it is processed.
914 # but its children will not be added to the queue when it is processed.
915
915
916 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
916 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
917 # the queue, but its children have not been added.
917 # the queue, but its children have not been added.
918
918
919 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
919 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
920 # the children still need to be processed.
920 # the children still need to be processed.
921
921
922 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
922 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
923 # added as dependent issues. It needs no further processing.
923 # added as dependent issues. It needs no further processing.
924
924
925 issue_status = Hash.new(eNOT_DISCOVERED)
925 issue_status = Hash.new(eNOT_DISCOVERED)
926
926
927 # The queue
927 # The queue
928 queue = []
928 queue = []
929
929
930 # Initialize the bfs, add start node (self) to the queue
930 # Initialize the bfs, add start node (self) to the queue
931 queue << self
931 queue << self
932 issue_status[self] = ePROCESS_ALL
932 issue_status[self] = ePROCESS_ALL
933
933
934 while (!queue.empty?) do
934 while (!queue.empty?) do
935 current_issue = queue.shift
935 current_issue = queue.shift
936 current_issue_status = issue_status[current_issue]
936 current_issue_status = issue_status[current_issue]
937 dependencies << current_issue
937 dependencies << current_issue
938
938
939 # Add parent to queue, if not already in it.
939 # Add parent to queue, if not already in it.
940 parent = current_issue.parent
940 parent = current_issue.parent
941 parent_status = issue_status[parent]
941 parent_status = issue_status[parent]
942
942
943 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
943 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
944 queue << parent
944 queue << parent
945 issue_status[parent] = ePROCESS_RELATIONS_ONLY
945 issue_status[parent] = ePROCESS_RELATIONS_ONLY
946 end
946 end
947
947
948 # Add children to queue, but only if they are not already in it and
948 # Add children to queue, but only if they are not already in it and
949 # the children of the current node need to be processed.
949 # the children of the current node need to be processed.
950 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
950 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
951 current_issue.children.each do |child|
951 current_issue.children.each do |child|
952 next if except.include?(child)
952 next if except.include?(child)
953
953
954 if (issue_status[child] == eNOT_DISCOVERED)
954 if (issue_status[child] == eNOT_DISCOVERED)
955 queue << child
955 queue << child
956 issue_status[child] = ePROCESS_ALL
956 issue_status[child] = ePROCESS_ALL
957 elsif (issue_status[child] == eRELATIONS_PROCESSED)
957 elsif (issue_status[child] == eRELATIONS_PROCESSED)
958 queue << child
958 queue << child
959 issue_status[child] = ePROCESS_CHILDREN_ONLY
959 issue_status[child] = ePROCESS_CHILDREN_ONLY
960 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
960 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
961 queue << child
961 queue << child
962 issue_status[child] = ePROCESS_ALL
962 issue_status[child] = ePROCESS_ALL
963 end
963 end
964 end
964 end
965 end
965 end
966
966
967 # Add related issues to the queue, if they are not already in it.
967 # Add related issues to the queue, if they are not already in it.
968 current_issue.relations_from.map(&:issue_to).each do |related_issue|
968 current_issue.relations_from.map(&:issue_to).each do |related_issue|
969 next if except.include?(related_issue)
969 next if except.include?(related_issue)
970
970
971 if (issue_status[related_issue] == eNOT_DISCOVERED)
971 if (issue_status[related_issue] == eNOT_DISCOVERED)
972 queue << related_issue
972 queue << related_issue
973 issue_status[related_issue] = ePROCESS_ALL
973 issue_status[related_issue] = ePROCESS_ALL
974 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
974 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
975 queue << related_issue
975 queue << related_issue
976 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
976 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
977 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
977 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
978 queue << related_issue
978 queue << related_issue
979 issue_status[related_issue] = ePROCESS_ALL
979 issue_status[related_issue] = ePROCESS_ALL
980 end
980 end
981 end
981 end
982
982
983 # Set new status for current issue
983 # Set new status for current issue
984 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
984 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
985 issue_status[current_issue] = eALL_PROCESSED
985 issue_status[current_issue] = eALL_PROCESSED
986 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
986 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
987 issue_status[current_issue] = eRELATIONS_PROCESSED
987 issue_status[current_issue] = eRELATIONS_PROCESSED
988 end
988 end
989 end # while
989 end # while
990
990
991 # Remove the issues from the "except" parameter from the result array
991 # Remove the issues from the "except" parameter from the result array
992 dependencies -= except
992 dependencies -= except
993 dependencies.delete(self)
993 dependencies.delete(self)
994
994
995 dependencies
995 dependencies
996 end
996 end
997
997
998 # Returns an array of issues that duplicate this one
998 # Returns an array of issues that duplicate this one
999 def duplicates
999 def duplicates
1000 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1000 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1001 end
1001 end
1002
1002
1003 # Returns the due date or the target due date if any
1003 # Returns the due date or the target due date if any
1004 # Used on gantt chart
1004 # Used on gantt chart
1005 def due_before
1005 def due_before
1006 due_date || (fixed_version ? fixed_version.effective_date : nil)
1006 due_date || (fixed_version ? fixed_version.effective_date : nil)
1007 end
1007 end
1008
1008
1009 # Returns the time scheduled for this issue.
1009 # Returns the time scheduled for this issue.
1010 #
1010 #
1011 # Example:
1011 # Example:
1012 # Start Date: 2/26/09, End Date: 3/04/09
1012 # Start Date: 2/26/09, End Date: 3/04/09
1013 # duration => 6
1013 # duration => 6
1014 def duration
1014 def duration
1015 (start_date && due_date) ? due_date - start_date : 0
1015 (start_date && due_date) ? due_date - start_date : 0
1016 end
1016 end
1017
1017
1018 # Returns the duration in working days
1018 # Returns the duration in working days
1019 def working_duration
1019 def working_duration
1020 (start_date && due_date) ? working_days(start_date, due_date) : 0
1020 (start_date && due_date) ? working_days(start_date, due_date) : 0
1021 end
1021 end
1022
1022
1023 def soonest_start(reload=false)
1023 def soonest_start(reload=false)
1024 @soonest_start = nil if reload
1024 @soonest_start = nil if reload
1025 @soonest_start ||= (
1025 @soonest_start ||= (
1026 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1026 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1027 [(@parent_issue || parent).try(:soonest_start)]
1027 [(@parent_issue || parent).try(:soonest_start)]
1028 ).compact.max
1028 ).compact.max
1029 end
1029 end
1030
1030
1031 # Sets start_date on the given date or the next working day
1031 # Sets start_date on the given date or the next working day
1032 # and changes due_date to keep the same working duration.
1032 # and changes due_date to keep the same working duration.
1033 def reschedule_on(date)
1033 def reschedule_on(date)
1034 wd = working_duration
1034 wd = working_duration
1035 date = next_working_date(date)
1035 date = next_working_date(date)
1036 self.start_date = date
1036 self.start_date = date
1037 self.due_date = add_working_days(date, wd)
1037 self.due_date = add_working_days(date, wd)
1038 end
1038 end
1039
1039
1040 # Reschedules the issue on the given date or the next working day and saves the record.
1040 # Reschedules the issue on the given date or the next working day and saves the record.
1041 # If the issue is a parent task, this is done by rescheduling its subtasks.
1041 # If the issue is a parent task, this is done by rescheduling its subtasks.
1042 def reschedule_on!(date)
1042 def reschedule_on!(date)
1043 return if date.nil?
1043 return if date.nil?
1044 if leaf?
1044 if leaf?
1045 if start_date.nil? || start_date != date
1045 if start_date.nil? || start_date != date
1046 if start_date && start_date > date
1046 if start_date && start_date > date
1047 # Issue can not be moved earlier than its soonest start date
1047 # Issue can not be moved earlier than its soonest start date
1048 date = [soonest_start(true), date].compact.max
1048 date = [soonest_start(true), date].compact.max
1049 end
1049 end
1050 reschedule_on(date)
1050 reschedule_on(date)
1051 begin
1051 begin
1052 save
1052 save
1053 rescue ActiveRecord::StaleObjectError
1053 rescue ActiveRecord::StaleObjectError
1054 reload
1054 reload
1055 reschedule_on(date)
1055 reschedule_on(date)
1056 save
1056 save
1057 end
1057 end
1058 end
1058 end
1059 else
1059 else
1060 leaves.each do |leaf|
1060 leaves.each do |leaf|
1061 if leaf.start_date
1061 if leaf.start_date
1062 # Only move subtask if it starts at the same date as the parent
1062 # Only move subtask if it starts at the same date as the parent
1063 # or if it starts before the given date
1063 # or if it starts before the given date
1064 if start_date == leaf.start_date || date > leaf.start_date
1064 if start_date == leaf.start_date || date > leaf.start_date
1065 leaf.reschedule_on!(date)
1065 leaf.reschedule_on!(date)
1066 end
1066 end
1067 else
1067 else
1068 leaf.reschedule_on!(date)
1068 leaf.reschedule_on!(date)
1069 end
1069 end
1070 end
1070 end
1071 end
1071 end
1072 end
1072 end
1073
1073
1074 def <=>(issue)
1074 def <=>(issue)
1075 if issue.nil?
1075 if issue.nil?
1076 -1
1076 -1
1077 elsif root_id != issue.root_id
1077 elsif root_id != issue.root_id
1078 (root_id || 0) <=> (issue.root_id || 0)
1078 (root_id || 0) <=> (issue.root_id || 0)
1079 else
1079 else
1080 (lft || 0) <=> (issue.lft || 0)
1080 (lft || 0) <=> (issue.lft || 0)
1081 end
1081 end
1082 end
1082 end
1083
1083
1084 def to_s
1084 def to_s
1085 "#{tracker} ##{id}: #{subject}"
1085 "#{tracker} ##{id}: #{subject}"
1086 end
1086 end
1087
1087
1088 # Returns a string of css classes that apply to the issue
1088 # Returns a string of css classes that apply to the issue
1089 def css_classes(user=User.current)
1089 def css_classes(user=User.current)
1090 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1090 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1091 s << ' closed' if closed?
1091 s << ' closed' if closed?
1092 s << ' overdue' if overdue?
1092 s << ' overdue' if overdue?
1093 s << ' child' if child?
1093 s << ' child' if child?
1094 s << ' parent' unless leaf?
1094 s << ' parent' unless leaf?
1095 s << ' private' if is_private?
1095 s << ' private' if is_private?
1096 if user.logged?
1096 if user.logged?
1097 s << ' created-by-me' if author_id == user.id
1097 s << ' created-by-me' if author_id == user.id
1098 s << ' assigned-to-me' if assigned_to_id == user.id
1098 s << ' assigned-to-me' if assigned_to_id == user.id
1099 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1099 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1100 end
1100 end
1101 s
1101 s
1102 end
1102 end
1103
1103
1104 # Unassigns issues from +version+ if it's no longer shared with issue's project
1104 # Unassigns issues from +version+ if it's no longer shared with issue's project
1105 def self.update_versions_from_sharing_change(version)
1105 def self.update_versions_from_sharing_change(version)
1106 # Update issues assigned to the version
1106 # Update issues assigned to the version
1107 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1107 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1108 end
1108 end
1109
1109
1110 # Unassigns issues from versions that are no longer shared
1110 # Unassigns issues from versions that are no longer shared
1111 # after +project+ was moved
1111 # after +project+ was moved
1112 def self.update_versions_from_hierarchy_change(project)
1112 def self.update_versions_from_hierarchy_change(project)
1113 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1113 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1114 # Update issues of the moved projects and issues assigned to a version of a moved project
1114 # Update issues of the moved projects and issues assigned to a version of a moved project
1115 Issue.update_versions(
1115 Issue.update_versions(
1116 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1116 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1117 moved_project_ids, moved_project_ids]
1117 moved_project_ids, moved_project_ids]
1118 )
1118 )
1119 end
1119 end
1120
1120
1121 def parent_issue_id=(arg)
1121 def parent_issue_id=(arg)
1122 s = arg.to_s.strip.presence
1122 s = arg.to_s.strip.presence
1123 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1123 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1124 @invalid_parent_issue_id = nil
1124 @invalid_parent_issue_id = nil
1125 elsif s.blank?
1125 elsif s.blank?
1126 @parent_issue = nil
1126 @parent_issue = nil
1127 @invalid_parent_issue_id = nil
1127 @invalid_parent_issue_id = nil
1128 else
1128 else
1129 @parent_issue = nil
1129 @parent_issue = nil
1130 @invalid_parent_issue_id = arg
1130 @invalid_parent_issue_id = arg
1131 end
1131 end
1132 end
1132 end
1133
1133
1134 def parent_issue_id
1134 def parent_issue_id
1135 if @invalid_parent_issue_id
1135 if @invalid_parent_issue_id
1136 @invalid_parent_issue_id
1136 @invalid_parent_issue_id
1137 elsif instance_variable_defined? :@parent_issue
1137 elsif instance_variable_defined? :@parent_issue
1138 @parent_issue.nil? ? nil : @parent_issue.id
1138 @parent_issue.nil? ? nil : @parent_issue.id
1139 else
1139 else
1140 parent_id
1140 parent_id
1141 end
1141 end
1142 end
1142 end
1143
1143
1144 # Returns true if issue's project is a valid
1144 # Returns true if issue's project is a valid
1145 # parent issue project
1145 # parent issue project
1146 def valid_parent_project?(issue=parent)
1146 def valid_parent_project?(issue=parent)
1147 return true if issue.nil? || issue.project_id == project_id
1147 return true if issue.nil? || issue.project_id == project_id
1148
1148
1149 case Setting.cross_project_subtasks
1149 case Setting.cross_project_subtasks
1150 when 'system'
1150 when 'system'
1151 true
1151 true
1152 when 'tree'
1152 when 'tree'
1153 issue.project.root == project.root
1153 issue.project.root == project.root
1154 when 'hierarchy'
1154 when 'hierarchy'
1155 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1155 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1156 when 'descendants'
1156 when 'descendants'
1157 issue.project.is_or_is_ancestor_of?(project)
1157 issue.project.is_or_is_ancestor_of?(project)
1158 else
1158 else
1159 false
1159 false
1160 end
1160 end
1161 end
1161 end
1162
1162
1163 # Returns an issue scope based on project and scope
1163 # Returns an issue scope based on project and scope
1164 def self.cross_project_scope(project, scope=nil)
1164 def self.cross_project_scope(project, scope=nil)
1165 if project.nil?
1165 if project.nil?
1166 return Issue
1166 return Issue
1167 end
1167 end
1168 case scope
1168 case scope
1169 when 'all', 'system'
1169 when 'all', 'system'
1170 Issue
1170 Issue
1171 when 'tree'
1171 when 'tree'
1172 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1172 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1173 :lft => project.root.lft, :rgt => project.root.rgt)
1173 :lft => project.root.lft, :rgt => project.root.rgt)
1174 when 'hierarchy'
1174 when 'hierarchy'
1175 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1175 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1176 :lft => project.lft, :rgt => project.rgt)
1176 :lft => project.lft, :rgt => project.rgt)
1177 when 'descendants'
1177 when 'descendants'
1178 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1178 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1179 :lft => project.lft, :rgt => project.rgt)
1179 :lft => project.lft, :rgt => project.rgt)
1180 else
1180 else
1181 Issue.where(:project_id => project.id)
1181 Issue.where(:project_id => project.id)
1182 end
1182 end
1183 end
1183 end
1184
1184
1185 def self.by_tracker(project)
1185 def self.by_tracker(project)
1186 count_and_group_by(:project => project, :association => :tracker)
1186 count_and_group_by(:project => project, :association => :tracker)
1187 end
1187 end
1188
1188
1189 def self.by_version(project)
1189 def self.by_version(project)
1190 count_and_group_by(:project => project, :association => :fixed_version)
1190 count_and_group_by(:project => project, :association => :fixed_version)
1191 end
1191 end
1192
1192
1193 def self.by_priority(project)
1193 def self.by_priority(project)
1194 count_and_group_by(:project => project, :association => :priority)
1194 count_and_group_by(:project => project, :association => :priority)
1195 end
1195 end
1196
1196
1197 def self.by_category(project)
1197 def self.by_category(project)
1198 count_and_group_by(:project => project, :association => :category)
1198 count_and_group_by(:project => project, :association => :category)
1199 end
1199 end
1200
1200
1201 def self.by_assigned_to(project)
1201 def self.by_assigned_to(project)
1202 count_and_group_by(:project => project, :association => :assigned_to)
1202 count_and_group_by(:project => project, :association => :assigned_to)
1203 end
1203 end
1204
1204
1205 def self.by_author(project)
1205 def self.by_author(project)
1206 count_and_group_by(:project => project, :association => :author)
1206 count_and_group_by(:project => project, :association => :author)
1207 end
1207 end
1208
1208
1209 def self.by_subproject(project)
1209 def self.by_subproject(project)
1210 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1210 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1211 r.reject {|r| r["project_id"] == project.id.to_s}
1211 r.reject {|r| r["project_id"] == project.id.to_s}
1212 end
1212 end
1213
1213
1214 # Query generator for selecting groups of issue counts for a project
1214 # Query generator for selecting groups of issue counts for a project
1215 # based on specific criteria
1215 # based on specific criteria
1216 #
1216 #
1217 # Options
1217 # Options
1218 # * project - Project to search in.
1218 # * project - Project to search in.
1219 # * with_subprojects - Includes subprojects issues if set to true.
1219 # * with_subprojects - Includes subprojects issues if set to true.
1220 # * association - Symbol. Association for grouping.
1220 # * association - Symbol. Association for grouping.
1221 def self.count_and_group_by(options)
1221 def self.count_and_group_by(options)
1222 assoc = reflect_on_association(options[:association])
1222 assoc = reflect_on_association(options[:association])
1223 select_field = assoc.foreign_key
1223 select_field = assoc.foreign_key
1224
1224
1225 Issue.
1225 Issue.
1226 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1226 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1227 joins(:status, assoc.name).
1227 joins(:status, assoc.name).
1228 group(:status_id, :is_closed, select_field).
1228 group(:status_id, :is_closed, select_field).
1229 count.
1229 count.
1230 map do |columns, total|
1230 map do |columns, total|
1231 status_id, is_closed, field_value = columns
1231 status_id, is_closed, field_value = columns
1232 is_closed = ['t', 'true', true].include?(is_closed)
1232 {
1233 {
1233 "status_id" => status_id.to_s,
1234 "status_id" => status_id.to_s,
1234 "closed" => is_closed.to_s,
1235 "closed" => is_closed,
1235 select_field => field_value.to_s,
1236 select_field => field_value.to_s,
1236 "total" => total.to_s
1237 "total" => total.to_s
1237 }
1238 }
1238 end
1239 end
1239 end
1240 end
1240
1241
1241 # Returns a scope of projects that user can assign the issue to
1242 # Returns a scope of projects that user can assign the issue to
1242 def allowed_target_projects(user=User.current)
1243 def allowed_target_projects(user=User.current)
1243 if new_record?
1244 if new_record?
1244 Project.where(Project.allowed_to_condition(user, :add_issues))
1245 Project.where(Project.allowed_to_condition(user, :add_issues))
1245 else
1246 else
1246 self.class.allowed_target_projects_on_move(user)
1247 self.class.allowed_target_projects_on_move(user)
1247 end
1248 end
1248 end
1249 end
1249
1250
1250 # Returns a scope of projects that user can move issues to
1251 # Returns a scope of projects that user can move issues to
1251 def self.allowed_target_projects_on_move(user=User.current)
1252 def self.allowed_target_projects_on_move(user=User.current)
1252 Project.where(Project.allowed_to_condition(user, :move_issues))
1253 Project.where(Project.allowed_to_condition(user, :move_issues))
1253 end
1254 end
1254
1255
1255 private
1256 private
1256
1257
1257 def after_project_change
1258 def after_project_change
1258 # Update project_id on related time entries
1259 # Update project_id on related time entries
1259 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1260 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1260
1261
1261 # Delete issue relations
1262 # Delete issue relations
1262 unless Setting.cross_project_issue_relations?
1263 unless Setting.cross_project_issue_relations?
1263 relations_from.clear
1264 relations_from.clear
1264 relations_to.clear
1265 relations_to.clear
1265 end
1266 end
1266
1267
1267 # Move subtasks that were in the same project
1268 # Move subtasks that were in the same project
1268 children.each do |child|
1269 children.each do |child|
1269 next unless child.project_id == project_id_was
1270 next unless child.project_id == project_id_was
1270 # Change project and keep project
1271 # Change project and keep project
1271 child.send :project=, project, true
1272 child.send :project=, project, true
1272 unless child.save
1273 unless child.save
1273 raise ActiveRecord::Rollback
1274 raise ActiveRecord::Rollback
1274 end
1275 end
1275 end
1276 end
1276 end
1277 end
1277
1278
1278 # Callback for after the creation of an issue by copy
1279 # Callback for after the creation of an issue by copy
1279 # * adds a "copied to" relation with the copied issue
1280 # * adds a "copied to" relation with the copied issue
1280 # * copies subtasks from the copied issue
1281 # * copies subtasks from the copied issue
1281 def after_create_from_copy
1282 def after_create_from_copy
1282 return unless copy? && !@after_create_from_copy_handled
1283 return unless copy? && !@after_create_from_copy_handled
1283
1284
1284 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1285 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1285 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1286 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1286 unless relation.save
1287 unless relation.save
1287 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1288 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1288 end
1289 end
1289 end
1290 end
1290
1291
1291 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1292 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1292 copy_options = (@copy_options || {}).merge(:subtasks => false)
1293 copy_options = (@copy_options || {}).merge(:subtasks => false)
1293 copied_issue_ids = {@copied_from.id => self.id}
1294 copied_issue_ids = {@copied_from.id => self.id}
1294 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1295 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1295 # Do not copy self when copying an issue as a descendant of the copied issue
1296 # Do not copy self when copying an issue as a descendant of the copied issue
1296 next if child == self
1297 next if child == self
1297 # Do not copy subtasks of issues that were not copied
1298 # Do not copy subtasks of issues that were not copied
1298 next unless copied_issue_ids[child.parent_id]
1299 next unless copied_issue_ids[child.parent_id]
1299 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1300 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1300 unless child.visible?
1301 unless child.visible?
1301 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1302 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1302 next
1303 next
1303 end
1304 end
1304 copy = Issue.new.copy_from(child, copy_options)
1305 copy = Issue.new.copy_from(child, copy_options)
1305 copy.author = author
1306 copy.author = author
1306 copy.project = project
1307 copy.project = project
1307 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1308 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1308 unless copy.save
1309 unless copy.save
1309 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
1310 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
1310 next
1311 next
1311 end
1312 end
1312 copied_issue_ids[child.id] = copy.id
1313 copied_issue_ids[child.id] = copy.id
1313 end
1314 end
1314 end
1315 end
1315 @after_create_from_copy_handled = true
1316 @after_create_from_copy_handled = true
1316 end
1317 end
1317
1318
1318 def update_nested_set_attributes
1319 def update_nested_set_attributes
1319 if root_id.nil?
1320 if root_id.nil?
1320 # issue was just created
1321 # issue was just created
1321 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1322 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1322 Issue.where(["id = ?", id]).update_all(["root_id = ?", root_id])
1323 Issue.where(["id = ?", id]).update_all(["root_id = ?", root_id])
1323 if @parent_issue
1324 if @parent_issue
1324 move_to_child_of(@parent_issue)
1325 move_to_child_of(@parent_issue)
1325 end
1326 end
1326 elsif parent_issue_id != parent_id
1327 elsif parent_issue_id != parent_id
1327 update_nested_set_attributes_on_parent_change
1328 update_nested_set_attributes_on_parent_change
1328 end
1329 end
1329 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1330 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1330 end
1331 end
1331
1332
1332 # Updates the nested set for when an existing issue is moved
1333 # Updates the nested set for when an existing issue is moved
1333 def update_nested_set_attributes_on_parent_change
1334 def update_nested_set_attributes_on_parent_change
1334 former_parent_id = parent_id
1335 former_parent_id = parent_id
1335 # moving an existing issue
1336 # moving an existing issue
1336 if @parent_issue && @parent_issue.root_id == root_id
1337 if @parent_issue && @parent_issue.root_id == root_id
1337 # inside the same tree
1338 # inside the same tree
1338 move_to_child_of(@parent_issue)
1339 move_to_child_of(@parent_issue)
1339 else
1340 else
1340 # to another tree
1341 # to another tree
1341 unless root?
1342 unless root?
1342 move_to_right_of(root)
1343 move_to_right_of(root)
1343 end
1344 end
1344 old_root_id = root_id
1345 old_root_id = root_id
1345 in_tenacious_transaction do
1346 in_tenacious_transaction do
1346 @parent_issue.reload_nested_set if @parent_issue
1347 @parent_issue.reload_nested_set if @parent_issue
1347 self.reload_nested_set
1348 self.reload_nested_set
1348 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1349 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1349 cond = ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]
1350 cond = ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]
1350 self.class.base_class.select('id').lock(true).where(cond)
1351 self.class.base_class.select('id').lock(true).where(cond)
1351 offset = right_most_bound + 1 - lft
1352 offset = right_most_bound + 1 - lft
1352 Issue.where(cond).
1353 Issue.where(cond).
1353 update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset])
1354 update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset])
1354 end
1355 end
1355 if @parent_issue
1356 if @parent_issue
1356 move_to_child_of(@parent_issue)
1357 move_to_child_of(@parent_issue)
1357 end
1358 end
1358 end
1359 end
1359 # delete invalid relations of all descendants
1360 # delete invalid relations of all descendants
1360 self_and_descendants.each do |issue|
1361 self_and_descendants.each do |issue|
1361 issue.relations.each do |relation|
1362 issue.relations.each do |relation|
1362 relation.destroy unless relation.valid?
1363 relation.destroy unless relation.valid?
1363 end
1364 end
1364 end
1365 end
1365 # update former parent
1366 # update former parent
1366 recalculate_attributes_for(former_parent_id) if former_parent_id
1367 recalculate_attributes_for(former_parent_id) if former_parent_id
1367 end
1368 end
1368
1369
1369 def update_parent_attributes
1370 def update_parent_attributes
1370 recalculate_attributes_for(parent_id) if parent_id
1371 recalculate_attributes_for(parent_id) if parent_id
1371 end
1372 end
1372
1373
1373 def recalculate_attributes_for(issue_id)
1374 def recalculate_attributes_for(issue_id)
1374 if issue_id && p = Issue.find_by_id(issue_id)
1375 if issue_id && p = Issue.find_by_id(issue_id)
1375 # priority = highest priority of children
1376 # priority = highest priority of children
1376 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1377 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1377 p.priority = IssuePriority.find_by_position(priority_position)
1378 p.priority = IssuePriority.find_by_position(priority_position)
1378 end
1379 end
1379
1380
1380 # start/due dates = lowest/highest dates of children
1381 # start/due dates = lowest/highest dates of children
1381 p.start_date = p.children.minimum(:start_date)
1382 p.start_date = p.children.minimum(:start_date)
1382 p.due_date = p.children.maximum(:due_date)
1383 p.due_date = p.children.maximum(:due_date)
1383 if p.start_date && p.due_date && p.due_date < p.start_date
1384 if p.start_date && p.due_date && p.due_date < p.start_date
1384 p.start_date, p.due_date = p.due_date, p.start_date
1385 p.start_date, p.due_date = p.due_date, p.start_date
1385 end
1386 end
1386
1387
1387 # done ratio = weighted average ratio of leaves
1388 # done ratio = weighted average ratio of leaves
1388 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1389 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1389 leaves_count = p.leaves.count
1390 leaves_count = p.leaves.count
1390 if leaves_count > 0
1391 if leaves_count > 0
1391 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1392 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1392 if average == 0
1393 if average == 0
1393 average = 1
1394 average = 1
1394 end
1395 end
1395 done = p.leaves.joins(:status).
1396 done = p.leaves.joins(:status).
1396 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1397 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1397 "* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1398 "* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1398 progress = done / (average * leaves_count)
1399 progress = done / (average * leaves_count)
1399 p.done_ratio = progress.round
1400 p.done_ratio = progress.round
1400 end
1401 end
1401 end
1402 end
1402
1403
1403 # estimate = sum of leaves estimates
1404 # estimate = sum of leaves estimates
1404 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1405 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1405 p.estimated_hours = nil if p.estimated_hours == 0.0
1406 p.estimated_hours = nil if p.estimated_hours == 0.0
1406
1407
1407 # ancestors will be recursively updated
1408 # ancestors will be recursively updated
1408 p.save(:validate => false)
1409 p.save(:validate => false)
1409 end
1410 end
1410 end
1411 end
1411
1412
1412 # Update issues so their versions are not pointing to a
1413 # Update issues so their versions are not pointing to a
1413 # fixed_version that is not shared with the issue's project
1414 # fixed_version that is not shared with the issue's project
1414 def self.update_versions(conditions=nil)
1415 def self.update_versions(conditions=nil)
1415 # Only need to update issues with a fixed_version from
1416 # Only need to update issues with a fixed_version from
1416 # a different project and that is not systemwide shared
1417 # a different project and that is not systemwide shared
1417 Issue.includes(:project, :fixed_version).
1418 Issue.includes(:project, :fixed_version).
1418 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1419 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1419 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1420 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1420 " AND #{Version.table_name}.sharing <> 'system'").
1421 " AND #{Version.table_name}.sharing <> 'system'").
1421 where(conditions).each do |issue|
1422 where(conditions).each do |issue|
1422 next if issue.project.nil? || issue.fixed_version.nil?
1423 next if issue.project.nil? || issue.fixed_version.nil?
1423 unless issue.project.shared_versions.include?(issue.fixed_version)
1424 unless issue.project.shared_versions.include?(issue.fixed_version)
1424 issue.init_journal(User.current)
1425 issue.init_journal(User.current)
1425 issue.fixed_version = nil
1426 issue.fixed_version = nil
1426 issue.save
1427 issue.save
1427 end
1428 end
1428 end
1429 end
1429 end
1430 end
1430
1431
1431 # Callback on file attachment
1432 # Callback on file attachment
1432 def attachment_added(obj)
1433 def attachment_added(obj)
1433 if @current_journal && !obj.new_record?
1434 if @current_journal && !obj.new_record?
1434 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1435 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1435 end
1436 end
1436 end
1437 end
1437
1438
1438 # Callback on attachment deletion
1439 # Callback on attachment deletion
1439 def attachment_removed(obj)
1440 def attachment_removed(obj)
1440 if @current_journal && !obj.new_record?
1441 if @current_journal && !obj.new_record?
1441 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1442 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1442 @current_journal.save
1443 @current_journal.save
1443 end
1444 end
1444 end
1445 end
1445
1446
1446 # Default assignment based on category
1447 # Default assignment based on category
1447 def default_assign
1448 def default_assign
1448 if assigned_to.nil? && category && category.assigned_to
1449 if assigned_to.nil? && category && category.assigned_to
1449 self.assigned_to = category.assigned_to
1450 self.assigned_to = category.assigned_to
1450 end
1451 end
1451 end
1452 end
1452
1453
1453 # Updates start/due dates of following issues
1454 # Updates start/due dates of following issues
1454 def reschedule_following_issues
1455 def reschedule_following_issues
1455 if start_date_changed? || due_date_changed?
1456 if start_date_changed? || due_date_changed?
1456 relations_from.each do |relation|
1457 relations_from.each do |relation|
1457 relation.set_issue_to_dates
1458 relation.set_issue_to_dates
1458 end
1459 end
1459 end
1460 end
1460 end
1461 end
1461
1462
1462 # Closes duplicates if the issue is being closed
1463 # Closes duplicates if the issue is being closed
1463 def close_duplicates
1464 def close_duplicates
1464 if closing?
1465 if closing?
1465 duplicates.each do |duplicate|
1466 duplicates.each do |duplicate|
1466 # Reload is needed in case the duplicate was updated by a previous duplicate
1467 # Reload is needed in case the duplicate was updated by a previous duplicate
1467 duplicate.reload
1468 duplicate.reload
1468 # Don't re-close it if it's already closed
1469 # Don't re-close it if it's already closed
1469 next if duplicate.closed?
1470 next if duplicate.closed?
1470 # Same user and notes
1471 # Same user and notes
1471 if @current_journal
1472 if @current_journal
1472 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1473 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1473 end
1474 end
1474 duplicate.update_attribute :status, self.status
1475 duplicate.update_attribute :status, self.status
1475 end
1476 end
1476 end
1477 end
1477 end
1478 end
1478
1479
1479 # Make sure updated_on is updated when adding a note and set updated_on now
1480 # Make sure updated_on is updated when adding a note and set updated_on now
1480 # so we can set closed_on with the same value on closing
1481 # so we can set closed_on with the same value on closing
1481 def force_updated_on_change
1482 def force_updated_on_change
1482 if @current_journal || changed?
1483 if @current_journal || changed?
1483 self.updated_on = current_time_from_proper_timezone
1484 self.updated_on = current_time_from_proper_timezone
1484 if new_record?
1485 if new_record?
1485 self.created_on = updated_on
1486 self.created_on = updated_on
1486 end
1487 end
1487 end
1488 end
1488 end
1489 end
1489
1490
1490 # Callback for setting closed_on when the issue is closed.
1491 # Callback for setting closed_on when the issue is closed.
1491 # The closed_on attribute stores the time of the last closing
1492 # The closed_on attribute stores the time of the last closing
1492 # and is preserved when the issue is reopened.
1493 # and is preserved when the issue is reopened.
1493 def update_closed_on
1494 def update_closed_on
1494 if closing? || (new_record? && closed?)
1495 if closing? || (new_record? && closed?)
1495 self.closed_on = updated_on
1496 self.closed_on = updated_on
1496 end
1497 end
1497 end
1498 end
1498
1499
1499 # Saves the changes in a Journal
1500 # Saves the changes in a Journal
1500 # Called after_save
1501 # Called after_save
1501 def create_journal
1502 def create_journal
1502 if @current_journal
1503 if @current_journal
1503 # attributes changes
1504 # attributes changes
1504 if @attributes_before_change
1505 if @attributes_before_change
1505 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1506 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1506 before = @attributes_before_change[c]
1507 before = @attributes_before_change[c]
1507 after = send(c)
1508 after = send(c)
1508 next if before == after || (before.blank? && after.blank?)
1509 next if before == after || (before.blank? && after.blank?)
1509 @current_journal.details << JournalDetail.new(:property => 'attr',
1510 @current_journal.details << JournalDetail.new(:property => 'attr',
1510 :prop_key => c,
1511 :prop_key => c,
1511 :old_value => before,
1512 :old_value => before,
1512 :value => after)
1513 :value => after)
1513 }
1514 }
1514 end
1515 end
1515 if @custom_values_before_change
1516 if @custom_values_before_change
1516 # custom fields changes
1517 # custom fields changes
1517 custom_field_values.each {|c|
1518 custom_field_values.each {|c|
1518 before = @custom_values_before_change[c.custom_field_id]
1519 before = @custom_values_before_change[c.custom_field_id]
1519 after = c.value
1520 after = c.value
1520 next if before == after || (before.blank? && after.blank?)
1521 next if before == after || (before.blank? && after.blank?)
1521
1522
1522 if before.is_a?(Array) || after.is_a?(Array)
1523 if before.is_a?(Array) || after.is_a?(Array)
1523 before = [before] unless before.is_a?(Array)
1524 before = [before] unless before.is_a?(Array)
1524 after = [after] unless after.is_a?(Array)
1525 after = [after] unless after.is_a?(Array)
1525
1526
1526 # values removed
1527 # values removed
1527 (before - after).reject(&:blank?).each do |value|
1528 (before - after).reject(&:blank?).each do |value|
1528 @current_journal.details << JournalDetail.new(:property => 'cf',
1529 @current_journal.details << JournalDetail.new(:property => 'cf',
1529 :prop_key => c.custom_field_id,
1530 :prop_key => c.custom_field_id,
1530 :old_value => value,
1531 :old_value => value,
1531 :value => nil)
1532 :value => nil)
1532 end
1533 end
1533 # values added
1534 # values added
1534 (after - before).reject(&:blank?).each do |value|
1535 (after - before).reject(&:blank?).each do |value|
1535 @current_journal.details << JournalDetail.new(:property => 'cf',
1536 @current_journal.details << JournalDetail.new(:property => 'cf',
1536 :prop_key => c.custom_field_id,
1537 :prop_key => c.custom_field_id,
1537 :old_value => nil,
1538 :old_value => nil,
1538 :value => value)
1539 :value => value)
1539 end
1540 end
1540 else
1541 else
1541 @current_journal.details << JournalDetail.new(:property => 'cf',
1542 @current_journal.details << JournalDetail.new(:property => 'cf',
1542 :prop_key => c.custom_field_id,
1543 :prop_key => c.custom_field_id,
1543 :old_value => before,
1544 :old_value => before,
1544 :value => after)
1545 :value => after)
1545 end
1546 end
1546 }
1547 }
1547 end
1548 end
1548 @current_journal.save
1549 @current_journal.save
1549 # reset current journal
1550 # reset current journal
1550 init_journal @current_journal.user, @current_journal.notes
1551 init_journal @current_journal.user, @current_journal.notes
1551 end
1552 end
1552 end
1553 end
1553
1554
1554 def send_notification
1555 def send_notification
1555 if Setting.notified_events.include?('issue_added')
1556 if Setting.notified_events.include?('issue_added')
1556 Mailer.deliver_issue_add(self)
1557 Mailer.deliver_issue_add(self)
1557 end
1558 end
1558 end
1559 end
1559
1560
1560 # Stores the previous assignee so we can still have access
1561 # Stores the previous assignee so we can still have access
1561 # to it during after_save callbacks (assigned_to_id_was is reset)
1562 # to it during after_save callbacks (assigned_to_id_was is reset)
1562 def set_assigned_to_was
1563 def set_assigned_to_was
1563 @previous_assigned_to_id = assigned_to_id_was
1564 @previous_assigned_to_id = assigned_to_id_was
1564 end
1565 end
1565
1566
1566 # Clears the previous assignee at the end of after_save callbacks
1567 # Clears the previous assignee at the end of after_save callbacks
1567 def clear_assigned_to_was
1568 def clear_assigned_to_was
1568 @assigned_to_was = nil
1569 @assigned_to_was = nil
1569 @previous_assigned_to_id = nil
1570 @previous_assigned_to_id = nil
1570 end
1571 end
1571 end
1572 end
General Comments 0
You need to be logged in to leave comments. Login now