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