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