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