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