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