##// END OF EJS Templates
Code cleanup....
Jean-Philippe Lang -
r13133:f33dfe92436e
parent child
Show More
@@ -1,1600 +1,1605
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.to_a : 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)).to_a
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 else
783 initial_status = IssueStatus.find_by_id(status_id_was)
783 initial_status = status_was
784 end
784 end
785 initial_status ||= status
786
785
787 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
786 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
788 assignee_transitions_allowed = initial_assigned_to_id.present? &&
787 assignee_transitions_allowed = initial_assigned_to_id.present? &&
789 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
788 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
790
789
791 statuses = initial_status.find_new_statuses_allowed_to(
790 statuses = []
792 user.admin ? Role.all : user.roles_for_project(project),
791 if initial_status
792 statuses += initial_status.find_new_statuses_allowed_to(
793 user.admin ? Role.all.to_a : user.roles_for_project(project),
793 tracker,
794 tracker,
794 author == user,
795 author == user,
795 assignee_transitions_allowed
796 assignee_transitions_allowed
796 )
797 )
798 end
797 statuses << initial_status unless statuses.empty?
799 statuses << initial_status unless statuses.empty?
798 statuses << IssueStatus.default if include_default
800 statuses << IssueStatus.default if include_default
799 statuses = statuses.compact.uniq.sort
801 statuses = statuses.compact.uniq.sort
800 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
802 if blocked?
803 statuses.reject!(&:is_closed?)
804 end
805 statuses
801 end
806 end
802 end
807 end
803
808
804 # Returns the previous assignee if changed
809 # Returns the previous assignee if changed
805 def assigned_to_was
810 def assigned_to_was
806 # assigned_to_id_was is reset before after_save callbacks
811 # assigned_to_id_was is reset before after_save callbacks
807 user_id = @previous_assigned_to_id || assigned_to_id_was
812 user_id = @previous_assigned_to_id || assigned_to_id_was
808 if user_id && user_id != assigned_to_id
813 if user_id && user_id != assigned_to_id
809 @assigned_to_was ||= User.find_by_id(user_id)
814 @assigned_to_was ||= User.find_by_id(user_id)
810 end
815 end
811 end
816 end
812
817
813 # Returns the users that should be notified
818 # Returns the users that should be notified
814 def notified_users
819 def notified_users
815 notified = []
820 notified = []
816 # Author and assignee are always notified unless they have been
821 # Author and assignee are always notified unless they have been
817 # locked or don't want to be notified
822 # locked or don't want to be notified
818 notified << author if author
823 notified << author if author
819 if assigned_to
824 if assigned_to
820 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
825 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
821 end
826 end
822 if assigned_to_was
827 if assigned_to_was
823 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
828 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
824 end
829 end
825 notified = notified.select {|u| u.active? && u.notify_about?(self)}
830 notified = notified.select {|u| u.active? && u.notify_about?(self)}
826
831
827 notified += project.notified_users
832 notified += project.notified_users
828 notified.uniq!
833 notified.uniq!
829 # Remove users that can not view the issue
834 # Remove users that can not view the issue
830 notified.reject! {|user| !visible?(user)}
835 notified.reject! {|user| !visible?(user)}
831 notified
836 notified
832 end
837 end
833
838
834 # Returns the email addresses that should be notified
839 # Returns the email addresses that should be notified
835 def recipients
840 def recipients
836 notified_users.collect(&:mail)
841 notified_users.collect(&:mail)
837 end
842 end
838
843
839 def each_notification(users, &block)
844 def each_notification(users, &block)
840 if users.any?
845 if users.any?
841 if custom_field_values.detect {|value| !value.custom_field.visible?}
846 if custom_field_values.detect {|value| !value.custom_field.visible?}
842 users_by_custom_field_visibility = users.group_by do |user|
847 users_by_custom_field_visibility = users.group_by do |user|
843 visible_custom_field_values(user).map(&:custom_field_id).sort
848 visible_custom_field_values(user).map(&:custom_field_id).sort
844 end
849 end
845 users_by_custom_field_visibility.values.each do |users|
850 users_by_custom_field_visibility.values.each do |users|
846 yield(users)
851 yield(users)
847 end
852 end
848 else
853 else
849 yield(users)
854 yield(users)
850 end
855 end
851 end
856 end
852 end
857 end
853
858
854 # Returns the number of hours spent on this issue
859 # Returns the number of hours spent on this issue
855 def spent_hours
860 def spent_hours
856 @spent_hours ||= time_entries.sum(:hours) || 0
861 @spent_hours ||= time_entries.sum(:hours) || 0
857 end
862 end
858
863
859 # Returns the total number of hours spent on this issue and its descendants
864 # Returns the total number of hours spent on this issue and its descendants
860 #
865 #
861 # Example:
866 # Example:
862 # spent_hours => 0.0
867 # spent_hours => 0.0
863 # spent_hours => 50.2
868 # spent_hours => 50.2
864 def total_spent_hours
869 def total_spent_hours
865 @total_spent_hours ||=
870 @total_spent_hours ||=
866 self_and_descendants.
871 self_and_descendants.
867 joins("LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").
872 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
873 sum("#{TimeEntry.table_name}.hours").to_f || 0.0
869 end
874 end
870
875
871 def relations
876 def relations
872 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
877 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
873 end
878 end
874
879
875 # Preloads relations for a collection of issues
880 # Preloads relations for a collection of issues
876 def self.load_relations(issues)
881 def self.load_relations(issues)
877 if issues.any?
882 if issues.any?
878 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
883 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
879 issues.each do |issue|
884 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}
885 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
881 end
886 end
882 end
887 end
883 end
888 end
884
889
885 # Preloads visible spent time for a collection of issues
890 # Preloads visible spent time for a collection of issues
886 def self.load_visible_spent_hours(issues, user=User.current)
891 def self.load_visible_spent_hours(issues, user=User.current)
887 if issues.any?
892 if issues.any?
888 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
893 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
889 issues.each do |issue|
894 issues.each do |issue|
890 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
895 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
891 end
896 end
892 end
897 end
893 end
898 end
894
899
895 # Preloads visible relations for a collection of issues
900 # Preloads visible relations for a collection of issues
896 def self.load_visible_relations(issues, user=User.current)
901 def self.load_visible_relations(issues, user=User.current)
897 if issues.any?
902 if issues.any?
898 issue_ids = issues.map(&:id)
903 issue_ids = issues.map(&:id)
899 # Relations with issue_from in given issues and visible issue_to
904 # Relations with issue_from in given issues and visible issue_to
900 relations_from = IssueRelation.joins(:issue_to => :project).
905 relations_from = IssueRelation.joins(:issue_to => :project).
901 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
906 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
902 # Relations with issue_to in given issues and visible issue_from
907 # Relations with issue_to in given issues and visible issue_from
903 relations_to = IssueRelation.joins(:issue_from => :project).
908 relations_to = IssueRelation.joins(:issue_from => :project).
904 where(visible_condition(user)).
909 where(visible_condition(user)).
905 where(:issue_to_id => issue_ids).to_a
910 where(:issue_to_id => issue_ids).to_a
906 issues.each do |issue|
911 issues.each do |issue|
907 relations =
912 relations =
908 relations_from.select {|relation| relation.issue_from_id == issue.id} +
913 relations_from.select {|relation| relation.issue_from_id == issue.id} +
909 relations_to.select {|relation| relation.issue_to_id == issue.id}
914 relations_to.select {|relation| relation.issue_to_id == issue.id}
910
915
911 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
916 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
912 end
917 end
913 end
918 end
914 end
919 end
915
920
916 # Finds an issue relation given its id.
921 # Finds an issue relation given its id.
917 def find_relation(relation_id)
922 def find_relation(relation_id)
918 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
923 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
919 end
924 end
920
925
921 # Returns all the other issues that depend on the issue
926 # Returns all the other issues that depend on the issue
922 # The algorithm is a modified breadth first search (bfs)
927 # The algorithm is a modified breadth first search (bfs)
923 def all_dependent_issues(except=[])
928 def all_dependent_issues(except=[])
924 # The found dependencies
929 # The found dependencies
925 dependencies = []
930 dependencies = []
926
931
927 # The visited flag for every node (issue) used by the breadth first search
932 # 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.
933 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
929
934
930 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
935 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
931 # the issue when it is processed.
936 # the issue when it is processed.
932
937
933 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
938 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.
939 # but its children will not be added to the queue when it is processed.
935
940
936 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
941 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.
942 # the queue, but its children have not been added.
938
943
939 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
944 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.
945 # the children still need to be processed.
941
946
942 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
947 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.
948 # added as dependent issues. It needs no further processing.
944
949
945 issue_status = Hash.new(eNOT_DISCOVERED)
950 issue_status = Hash.new(eNOT_DISCOVERED)
946
951
947 # The queue
952 # The queue
948 queue = []
953 queue = []
949
954
950 # Initialize the bfs, add start node (self) to the queue
955 # Initialize the bfs, add start node (self) to the queue
951 queue << self
956 queue << self
952 issue_status[self] = ePROCESS_ALL
957 issue_status[self] = ePROCESS_ALL
953
958
954 while (!queue.empty?) do
959 while (!queue.empty?) do
955 current_issue = queue.shift
960 current_issue = queue.shift
956 current_issue_status = issue_status[current_issue]
961 current_issue_status = issue_status[current_issue]
957 dependencies << current_issue
962 dependencies << current_issue
958
963
959 # Add parent to queue, if not already in it.
964 # Add parent to queue, if not already in it.
960 parent = current_issue.parent
965 parent = current_issue.parent
961 parent_status = issue_status[parent]
966 parent_status = issue_status[parent]
962
967
963 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
968 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
964 queue << parent
969 queue << parent
965 issue_status[parent] = ePROCESS_RELATIONS_ONLY
970 issue_status[parent] = ePROCESS_RELATIONS_ONLY
966 end
971 end
967
972
968 # Add children to queue, but only if they are not already in it and
973 # 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.
974 # the children of the current node need to be processed.
970 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
975 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
971 current_issue.children.each do |child|
976 current_issue.children.each do |child|
972 next if except.include?(child)
977 next if except.include?(child)
973
978
974 if (issue_status[child] == eNOT_DISCOVERED)
979 if (issue_status[child] == eNOT_DISCOVERED)
975 queue << child
980 queue << child
976 issue_status[child] = ePROCESS_ALL
981 issue_status[child] = ePROCESS_ALL
977 elsif (issue_status[child] == eRELATIONS_PROCESSED)
982 elsif (issue_status[child] == eRELATIONS_PROCESSED)
978 queue << child
983 queue << child
979 issue_status[child] = ePROCESS_CHILDREN_ONLY
984 issue_status[child] = ePROCESS_CHILDREN_ONLY
980 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
985 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
981 queue << child
986 queue << child
982 issue_status[child] = ePROCESS_ALL
987 issue_status[child] = ePROCESS_ALL
983 end
988 end
984 end
989 end
985 end
990 end
986
991
987 # Add related issues to the queue, if they are not already in it.
992 # 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|
993 current_issue.relations_from.map(&:issue_to).each do |related_issue|
989 next if except.include?(related_issue)
994 next if except.include?(related_issue)
990
995
991 if (issue_status[related_issue] == eNOT_DISCOVERED)
996 if (issue_status[related_issue] == eNOT_DISCOVERED)
992 queue << related_issue
997 queue << related_issue
993 issue_status[related_issue] = ePROCESS_ALL
998 issue_status[related_issue] = ePROCESS_ALL
994 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
999 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
995 queue << related_issue
1000 queue << related_issue
996 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1001 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
997 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1002 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
998 queue << related_issue
1003 queue << related_issue
999 issue_status[related_issue] = ePROCESS_ALL
1004 issue_status[related_issue] = ePROCESS_ALL
1000 end
1005 end
1001 end
1006 end
1002
1007
1003 # Set new status for current issue
1008 # Set new status for current issue
1004 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1009 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1005 issue_status[current_issue] = eALL_PROCESSED
1010 issue_status[current_issue] = eALL_PROCESSED
1006 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1011 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1007 issue_status[current_issue] = eRELATIONS_PROCESSED
1012 issue_status[current_issue] = eRELATIONS_PROCESSED
1008 end
1013 end
1009 end # while
1014 end # while
1010
1015
1011 # Remove the issues from the "except" parameter from the result array
1016 # Remove the issues from the "except" parameter from the result array
1012 dependencies -= except
1017 dependencies -= except
1013 dependencies.delete(self)
1018 dependencies.delete(self)
1014
1019
1015 dependencies
1020 dependencies
1016 end
1021 end
1017
1022
1018 # Returns an array of issues that duplicate this one
1023 # Returns an array of issues that duplicate this one
1019 def duplicates
1024 def duplicates
1020 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1025 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1021 end
1026 end
1022
1027
1023 # Returns the due date or the target due date if any
1028 # Returns the due date or the target due date if any
1024 # Used on gantt chart
1029 # Used on gantt chart
1025 def due_before
1030 def due_before
1026 due_date || (fixed_version ? fixed_version.effective_date : nil)
1031 due_date || (fixed_version ? fixed_version.effective_date : nil)
1027 end
1032 end
1028
1033
1029 # Returns the time scheduled for this issue.
1034 # Returns the time scheduled for this issue.
1030 #
1035 #
1031 # Example:
1036 # Example:
1032 # Start Date: 2/26/09, End Date: 3/04/09
1037 # Start Date: 2/26/09, End Date: 3/04/09
1033 # duration => 6
1038 # duration => 6
1034 def duration
1039 def duration
1035 (start_date && due_date) ? due_date - start_date : 0
1040 (start_date && due_date) ? due_date - start_date : 0
1036 end
1041 end
1037
1042
1038 # Returns the duration in working days
1043 # Returns the duration in working days
1039 def working_duration
1044 def working_duration
1040 (start_date && due_date) ? working_days(start_date, due_date) : 0
1045 (start_date && due_date) ? working_days(start_date, due_date) : 0
1041 end
1046 end
1042
1047
1043 def soonest_start(reload=false)
1048 def soonest_start(reload=false)
1044 @soonest_start = nil if reload
1049 @soonest_start = nil if reload
1045 @soonest_start ||= (
1050 @soonest_start ||= (
1046 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1051 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1047 [(@parent_issue || parent).try(:soonest_start)]
1052 [(@parent_issue || parent).try(:soonest_start)]
1048 ).compact.max
1053 ).compact.max
1049 end
1054 end
1050
1055
1051 # Sets start_date on the given date or the next working day
1056 # Sets start_date on the given date or the next working day
1052 # and changes due_date to keep the same working duration.
1057 # and changes due_date to keep the same working duration.
1053 def reschedule_on(date)
1058 def reschedule_on(date)
1054 wd = working_duration
1059 wd = working_duration
1055 date = next_working_date(date)
1060 date = next_working_date(date)
1056 self.start_date = date
1061 self.start_date = date
1057 self.due_date = add_working_days(date, wd)
1062 self.due_date = add_working_days(date, wd)
1058 end
1063 end
1059
1064
1060 # Reschedules the issue on the given date or the next working day and saves the record.
1065 # 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.
1066 # If the issue is a parent task, this is done by rescheduling its subtasks.
1062 def reschedule_on!(date)
1067 def reschedule_on!(date)
1063 return if date.nil?
1068 return if date.nil?
1064 if leaf?
1069 if leaf?
1065 if start_date.nil? || start_date != date
1070 if start_date.nil? || start_date != date
1066 if start_date && start_date > date
1071 if start_date && start_date > date
1067 # Issue can not be moved earlier than its soonest start date
1072 # Issue can not be moved earlier than its soonest start date
1068 date = [soonest_start(true), date].compact.max
1073 date = [soonest_start(true), date].compact.max
1069 end
1074 end
1070 reschedule_on(date)
1075 reschedule_on(date)
1071 begin
1076 begin
1072 save
1077 save
1073 rescue ActiveRecord::StaleObjectError
1078 rescue ActiveRecord::StaleObjectError
1074 reload
1079 reload
1075 reschedule_on(date)
1080 reschedule_on(date)
1076 save
1081 save
1077 end
1082 end
1078 end
1083 end
1079 else
1084 else
1080 leaves.each do |leaf|
1085 leaves.each do |leaf|
1081 if leaf.start_date
1086 if leaf.start_date
1082 # Only move subtask if it starts at the same date as the parent
1087 # Only move subtask if it starts at the same date as the parent
1083 # or if it starts before the given date
1088 # or if it starts before the given date
1084 if start_date == leaf.start_date || date > leaf.start_date
1089 if start_date == leaf.start_date || date > leaf.start_date
1085 leaf.reschedule_on!(date)
1090 leaf.reschedule_on!(date)
1086 end
1091 end
1087 else
1092 else
1088 leaf.reschedule_on!(date)
1093 leaf.reschedule_on!(date)
1089 end
1094 end
1090 end
1095 end
1091 end
1096 end
1092 end
1097 end
1093
1098
1094 def <=>(issue)
1099 def <=>(issue)
1095 if issue.nil?
1100 if issue.nil?
1096 -1
1101 -1
1097 elsif root_id != issue.root_id
1102 elsif root_id != issue.root_id
1098 (root_id || 0) <=> (issue.root_id || 0)
1103 (root_id || 0) <=> (issue.root_id || 0)
1099 else
1104 else
1100 (lft || 0) <=> (issue.lft || 0)
1105 (lft || 0) <=> (issue.lft || 0)
1101 end
1106 end
1102 end
1107 end
1103
1108
1104 def to_s
1109 def to_s
1105 "#{tracker} ##{id}: #{subject}"
1110 "#{tracker} ##{id}: #{subject}"
1106 end
1111 end
1107
1112
1108 # Returns a string of css classes that apply to the issue
1113 # Returns a string of css classes that apply to the issue
1109 def css_classes(user=User.current)
1114 def css_classes(user=User.current)
1110 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1115 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1111 s << ' closed' if closed?
1116 s << ' closed' if closed?
1112 s << ' overdue' if overdue?
1117 s << ' overdue' if overdue?
1113 s << ' child' if child?
1118 s << ' child' if child?
1114 s << ' parent' unless leaf?
1119 s << ' parent' unless leaf?
1115 s << ' private' if is_private?
1120 s << ' private' if is_private?
1116 if user.logged?
1121 if user.logged?
1117 s << ' created-by-me' if author_id == user.id
1122 s << ' created-by-me' if author_id == user.id
1118 s << ' assigned-to-me' if assigned_to_id == user.id
1123 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}
1124 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1120 end
1125 end
1121 s
1126 s
1122 end
1127 end
1123
1128
1124 # Unassigns issues from +version+ if it's no longer shared with issue's project
1129 # Unassigns issues from +version+ if it's no longer shared with issue's project
1125 def self.update_versions_from_sharing_change(version)
1130 def self.update_versions_from_sharing_change(version)
1126 # Update issues assigned to the version
1131 # Update issues assigned to the version
1127 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1132 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1128 end
1133 end
1129
1134
1130 # Unassigns issues from versions that are no longer shared
1135 # Unassigns issues from versions that are no longer shared
1131 # after +project+ was moved
1136 # after +project+ was moved
1132 def self.update_versions_from_hierarchy_change(project)
1137 def self.update_versions_from_hierarchy_change(project)
1133 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1138 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
1139 # Update issues of the moved projects and issues assigned to a version of a moved project
1135 Issue.update_versions(
1140 Issue.update_versions(
1136 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1141 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1137 moved_project_ids, moved_project_ids]
1142 moved_project_ids, moved_project_ids]
1138 )
1143 )
1139 end
1144 end
1140
1145
1141 def parent_issue_id=(arg)
1146 def parent_issue_id=(arg)
1142 s = arg.to_s.strip.presence
1147 s = arg.to_s.strip.presence
1143 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1148 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1144 @invalid_parent_issue_id = nil
1149 @invalid_parent_issue_id = nil
1145 elsif s.blank?
1150 elsif s.blank?
1146 @parent_issue = nil
1151 @parent_issue = nil
1147 @invalid_parent_issue_id = nil
1152 @invalid_parent_issue_id = nil
1148 else
1153 else
1149 @parent_issue = nil
1154 @parent_issue = nil
1150 @invalid_parent_issue_id = arg
1155 @invalid_parent_issue_id = arg
1151 end
1156 end
1152 end
1157 end
1153
1158
1154 def parent_issue_id
1159 def parent_issue_id
1155 if @invalid_parent_issue_id
1160 if @invalid_parent_issue_id
1156 @invalid_parent_issue_id
1161 @invalid_parent_issue_id
1157 elsif instance_variable_defined? :@parent_issue
1162 elsif instance_variable_defined? :@parent_issue
1158 @parent_issue.nil? ? nil : @parent_issue.id
1163 @parent_issue.nil? ? nil : @parent_issue.id
1159 else
1164 else
1160 parent_id
1165 parent_id
1161 end
1166 end
1162 end
1167 end
1163
1168
1164 # Returns true if issue's project is a valid
1169 # Returns true if issue's project is a valid
1165 # parent issue project
1170 # parent issue project
1166 def valid_parent_project?(issue=parent)
1171 def valid_parent_project?(issue=parent)
1167 return true if issue.nil? || issue.project_id == project_id
1172 return true if issue.nil? || issue.project_id == project_id
1168
1173
1169 case Setting.cross_project_subtasks
1174 case Setting.cross_project_subtasks
1170 when 'system'
1175 when 'system'
1171 true
1176 true
1172 when 'tree'
1177 when 'tree'
1173 issue.project.root == project.root
1178 issue.project.root == project.root
1174 when 'hierarchy'
1179 when 'hierarchy'
1175 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1180 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1176 when 'descendants'
1181 when 'descendants'
1177 issue.project.is_or_is_ancestor_of?(project)
1182 issue.project.is_or_is_ancestor_of?(project)
1178 else
1183 else
1179 false
1184 false
1180 end
1185 end
1181 end
1186 end
1182
1187
1183 # Returns an issue scope based on project and scope
1188 # Returns an issue scope based on project and scope
1184 def self.cross_project_scope(project, scope=nil)
1189 def self.cross_project_scope(project, scope=nil)
1185 if project.nil?
1190 if project.nil?
1186 return Issue
1191 return Issue
1187 end
1192 end
1188 case scope
1193 case scope
1189 when 'all', 'system'
1194 when 'all', 'system'
1190 Issue
1195 Issue
1191 when 'tree'
1196 when 'tree'
1192 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1197 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1193 :lft => project.root.lft, :rgt => project.root.rgt)
1198 :lft => project.root.lft, :rgt => project.root.rgt)
1194 when 'hierarchy'
1199 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)",
1200 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)
1201 :lft => project.lft, :rgt => project.rgt)
1197 when 'descendants'
1202 when 'descendants'
1198 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1203 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1199 :lft => project.lft, :rgt => project.rgt)
1204 :lft => project.lft, :rgt => project.rgt)
1200 else
1205 else
1201 Issue.where(:project_id => project.id)
1206 Issue.where(:project_id => project.id)
1202 end
1207 end
1203 end
1208 end
1204
1209
1205 def self.by_tracker(project)
1210 def self.by_tracker(project)
1206 count_and_group_by(:project => project, :association => :tracker)
1211 count_and_group_by(:project => project, :association => :tracker)
1207 end
1212 end
1208
1213
1209 def self.by_version(project)
1214 def self.by_version(project)
1210 count_and_group_by(:project => project, :association => :fixed_version)
1215 count_and_group_by(:project => project, :association => :fixed_version)
1211 end
1216 end
1212
1217
1213 def self.by_priority(project)
1218 def self.by_priority(project)
1214 count_and_group_by(:project => project, :association => :priority)
1219 count_and_group_by(:project => project, :association => :priority)
1215 end
1220 end
1216
1221
1217 def self.by_category(project)
1222 def self.by_category(project)
1218 count_and_group_by(:project => project, :association => :category)
1223 count_and_group_by(:project => project, :association => :category)
1219 end
1224 end
1220
1225
1221 def self.by_assigned_to(project)
1226 def self.by_assigned_to(project)
1222 count_and_group_by(:project => project, :association => :assigned_to)
1227 count_and_group_by(:project => project, :association => :assigned_to)
1223 end
1228 end
1224
1229
1225 def self.by_author(project)
1230 def self.by_author(project)
1226 count_and_group_by(:project => project, :association => :author)
1231 count_and_group_by(:project => project, :association => :author)
1227 end
1232 end
1228
1233
1229 def self.by_subproject(project)
1234 def self.by_subproject(project)
1230 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1235 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1231 r.reject {|r| r["project_id"] == project.id.to_s}
1236 r.reject {|r| r["project_id"] == project.id.to_s}
1232 end
1237 end
1233
1238
1234 # Query generator for selecting groups of issue counts for a project
1239 # Query generator for selecting groups of issue counts for a project
1235 # based on specific criteria
1240 # based on specific criteria
1236 #
1241 #
1237 # Options
1242 # Options
1238 # * project - Project to search in.
1243 # * project - Project to search in.
1239 # * with_subprojects - Includes subprojects issues if set to true.
1244 # * with_subprojects - Includes subprojects issues if set to true.
1240 # * association - Symbol. Association for grouping.
1245 # * association - Symbol. Association for grouping.
1241 def self.count_and_group_by(options)
1246 def self.count_and_group_by(options)
1242 assoc = reflect_on_association(options[:association])
1247 assoc = reflect_on_association(options[:association])
1243 select_field = assoc.foreign_key
1248 select_field = assoc.foreign_key
1244
1249
1245 Issue.
1250 Issue.
1246 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1251 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1247 joins(:status, assoc.name).
1252 joins(:status, assoc.name).
1248 group(:status_id, :is_closed, select_field).
1253 group(:status_id, :is_closed, select_field).
1249 count.
1254 count.
1250 map do |columns, total|
1255 map do |columns, total|
1251 status_id, is_closed, field_value = columns
1256 status_id, is_closed, field_value = columns
1252 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1257 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1253 {
1258 {
1254 "status_id" => status_id.to_s,
1259 "status_id" => status_id.to_s,
1255 "closed" => is_closed,
1260 "closed" => is_closed,
1256 select_field => field_value.to_s,
1261 select_field => field_value.to_s,
1257 "total" => total.to_s
1262 "total" => total.to_s
1258 }
1263 }
1259 end
1264 end
1260 end
1265 end
1261
1266
1262 # Returns a scope of projects that user can assign the issue to
1267 # Returns a scope of projects that user can assign the issue to
1263 def allowed_target_projects(user=User.current)
1268 def allowed_target_projects(user=User.current)
1264 if new_record?
1269 if new_record?
1265 Project.where(Project.allowed_to_condition(user, :add_issues))
1270 Project.where(Project.allowed_to_condition(user, :add_issues))
1266 else
1271 else
1267 self.class.allowed_target_projects_on_move(user)
1272 self.class.allowed_target_projects_on_move(user)
1268 end
1273 end
1269 end
1274 end
1270
1275
1271 # Returns a scope of projects that user can move issues to
1276 # Returns a scope of projects that user can move issues to
1272 def self.allowed_target_projects_on_move(user=User.current)
1277 def self.allowed_target_projects_on_move(user=User.current)
1273 Project.where(Project.allowed_to_condition(user, :move_issues))
1278 Project.where(Project.allowed_to_condition(user, :move_issues))
1274 end
1279 end
1275
1280
1276 private
1281 private
1277
1282
1278 def after_project_change
1283 def after_project_change
1279 # Update project_id on related time entries
1284 # Update project_id on related time entries
1280 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1285 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1281
1286
1282 # Delete issue relations
1287 # Delete issue relations
1283 unless Setting.cross_project_issue_relations?
1288 unless Setting.cross_project_issue_relations?
1284 relations_from.clear
1289 relations_from.clear
1285 relations_to.clear
1290 relations_to.clear
1286 end
1291 end
1287
1292
1288 # Move subtasks that were in the same project
1293 # Move subtasks that were in the same project
1289 children.each do |child|
1294 children.each do |child|
1290 next unless child.project_id == project_id_was
1295 next unless child.project_id == project_id_was
1291 # Change project and keep project
1296 # Change project and keep project
1292 child.send :project=, project, true
1297 child.send :project=, project, true
1293 unless child.save
1298 unless child.save
1294 raise ActiveRecord::Rollback
1299 raise ActiveRecord::Rollback
1295 end
1300 end
1296 end
1301 end
1297 end
1302 end
1298
1303
1299 # Callback for after the creation of an issue by copy
1304 # Callback for after the creation of an issue by copy
1300 # * adds a "copied to" relation with the copied issue
1305 # * adds a "copied to" relation with the copied issue
1301 # * copies subtasks from the copied issue
1306 # * copies subtasks from the copied issue
1302 def after_create_from_copy
1307 def after_create_from_copy
1303 return unless copy? && !@after_create_from_copy_handled
1308 return unless copy? && !@after_create_from_copy_handled
1304
1309
1305 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1310 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)
1311 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1307 unless relation.save
1312 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
1313 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
1314 end
1310 end
1315 end
1311
1316
1312 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1317 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1313 copy_options = (@copy_options || {}).merge(:subtasks => false)
1318 copy_options = (@copy_options || {}).merge(:subtasks => false)
1314 copied_issue_ids = {@copied_from.id => self.id}
1319 copied_issue_ids = {@copied_from.id => self.id}
1315 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1320 @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
1321 # Do not copy self when copying an issue as a descendant of the copied issue
1317 next if child == self
1322 next if child == self
1318 # Do not copy subtasks of issues that were not copied
1323 # Do not copy subtasks of issues that were not copied
1319 next unless copied_issue_ids[child.parent_id]
1324 next unless copied_issue_ids[child.parent_id]
1320 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1325 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1321 unless child.visible?
1326 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
1327 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
1328 next
1324 end
1329 end
1325 copy = Issue.new.copy_from(child, copy_options)
1330 copy = Issue.new.copy_from(child, copy_options)
1326 copy.author = author
1331 copy.author = author
1327 copy.project = project
1332 copy.project = project
1328 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1333 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1329 unless copy.save
1334 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
1335 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
1336 next
1332 end
1337 end
1333 copied_issue_ids[child.id] = copy.id
1338 copied_issue_ids[child.id] = copy.id
1334 end
1339 end
1335 end
1340 end
1336 @after_create_from_copy_handled = true
1341 @after_create_from_copy_handled = true
1337 end
1342 end
1338
1343
1339 def update_nested_set_attributes
1344 def update_nested_set_attributes
1340 if root_id.nil?
1345 if root_id.nil?
1341 # issue was just created
1346 # issue was just created
1342 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1347 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1343 Issue.where(["id = ?", id]).update_all(["root_id = ?", root_id])
1348 Issue.where(["id = ?", id]).update_all(["root_id = ?", root_id])
1344 if @parent_issue
1349 if @parent_issue
1345 move_to_child_of(@parent_issue)
1350 move_to_child_of(@parent_issue)
1346 end
1351 end
1347 elsif parent_issue_id != parent_id
1352 elsif parent_issue_id != parent_id
1348 update_nested_set_attributes_on_parent_change
1353 update_nested_set_attributes_on_parent_change
1349 end
1354 end
1350 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1355 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1351 end
1356 end
1352
1357
1353 # Updates the nested set for when an existing issue is moved
1358 # Updates the nested set for when an existing issue is moved
1354 def update_nested_set_attributes_on_parent_change
1359 def update_nested_set_attributes_on_parent_change
1355 former_parent_id = parent_id
1360 former_parent_id = parent_id
1356 # moving an existing issue
1361 # moving an existing issue
1357 if @parent_issue && @parent_issue.root_id == root_id
1362 if @parent_issue && @parent_issue.root_id == root_id
1358 # inside the same tree
1363 # inside the same tree
1359 move_to_child_of(@parent_issue)
1364 move_to_child_of(@parent_issue)
1360 else
1365 else
1361 # to another tree
1366 # to another tree
1362 unless root?
1367 unless root?
1363 move_to_right_of(root)
1368 move_to_right_of(root)
1364 end
1369 end
1365 old_root_id = root_id
1370 old_root_id = root_id
1366 in_tenacious_transaction do
1371 in_tenacious_transaction do
1367 @parent_issue.reload_nested_set if @parent_issue
1372 @parent_issue.reload_nested_set if @parent_issue
1368 self.reload_nested_set
1373 self.reload_nested_set
1369 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1374 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1370 cond = ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]
1375 cond = ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]
1371 self.class.base_class.select('id').lock(true).where(cond)
1376 self.class.base_class.select('id').lock(true).where(cond)
1372 offset = rdm_right_most_bound + 1 - lft
1377 offset = rdm_right_most_bound + 1 - lft
1373 Issue.where(cond).
1378 Issue.where(cond).
1374 update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset])
1379 update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset])
1375 end
1380 end
1376 if @parent_issue
1381 if @parent_issue
1377 move_to_child_of(@parent_issue)
1382 move_to_child_of(@parent_issue)
1378 end
1383 end
1379 end
1384 end
1380 # delete invalid relations of all descendants
1385 # delete invalid relations of all descendants
1381 self_and_descendants.each do |issue|
1386 self_and_descendants.each do |issue|
1382 issue.relations.each do |relation|
1387 issue.relations.each do |relation|
1383 relation.destroy unless relation.valid?
1388 relation.destroy unless relation.valid?
1384 end
1389 end
1385 end
1390 end
1386 # update former parent
1391 # update former parent
1387 recalculate_attributes_for(former_parent_id) if former_parent_id
1392 recalculate_attributes_for(former_parent_id) if former_parent_id
1388 end
1393 end
1389
1394
1390 def rdm_right_most_bound
1395 def rdm_right_most_bound
1391 right_most_node =
1396 right_most_node =
1392 self.class.base_class.unscoped.
1397 self.class.base_class.unscoped.
1393 order("#{quoted_right_column_full_name} desc").limit(1).lock(true).first
1398 order("#{quoted_right_column_full_name} desc").limit(1).lock(true).first
1394 right_most_node ? (right_most_node[right_column_name] || 0 ) : 0
1399 right_most_node ? (right_most_node[right_column_name] || 0 ) : 0
1395 end
1400 end
1396 private :rdm_right_most_bound
1401 private :rdm_right_most_bound
1397
1402
1398 def update_parent_attributes
1403 def update_parent_attributes
1399 recalculate_attributes_for(parent_id) if parent_id
1404 recalculate_attributes_for(parent_id) if parent_id
1400 end
1405 end
1401
1406
1402 def recalculate_attributes_for(issue_id)
1407 def recalculate_attributes_for(issue_id)
1403 if issue_id && p = Issue.find_by_id(issue_id)
1408 if issue_id && p = Issue.find_by_id(issue_id)
1404 # priority = highest priority of children
1409 # priority = highest priority of children
1405 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1410 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1406 p.priority = IssuePriority.find_by_position(priority_position)
1411 p.priority = IssuePriority.find_by_position(priority_position)
1407 end
1412 end
1408
1413
1409 # start/due dates = lowest/highest dates of children
1414 # start/due dates = lowest/highest dates of children
1410 p.start_date = p.children.minimum(:start_date)
1415 p.start_date = p.children.minimum(:start_date)
1411 p.due_date = p.children.maximum(:due_date)
1416 p.due_date = p.children.maximum(:due_date)
1412 if p.start_date && p.due_date && p.due_date < p.start_date
1417 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
1418 p.start_date, p.due_date = p.due_date, p.start_date
1414 end
1419 end
1415
1420
1416 # done ratio = weighted average ratio of leaves
1421 # done ratio = weighted average ratio of leaves
1417 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1422 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1418 leaves_count = p.leaves.count
1423 leaves_count = p.leaves.count
1419 if leaves_count > 0
1424 if leaves_count > 0
1420 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1425 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1421 if average == 0
1426 if average == 0
1422 average = 1
1427 average = 1
1423 end
1428 end
1424 done = p.leaves.joins(:status).
1429 done = p.leaves.joins(:status).
1425 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1430 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
1431 "* (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)
1432 progress = done / (average * leaves_count)
1428 p.done_ratio = progress.round
1433 p.done_ratio = progress.round
1429 end
1434 end
1430 end
1435 end
1431
1436
1432 # estimate = sum of leaves estimates
1437 # estimate = sum of leaves estimates
1433 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1438 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1434 p.estimated_hours = nil if p.estimated_hours == 0.0
1439 p.estimated_hours = nil if p.estimated_hours == 0.0
1435
1440
1436 # ancestors will be recursively updated
1441 # ancestors will be recursively updated
1437 p.save(:validate => false)
1442 p.save(:validate => false)
1438 end
1443 end
1439 end
1444 end
1440
1445
1441 # Update issues so their versions are not pointing to a
1446 # Update issues so their versions are not pointing to a
1442 # fixed_version that is not shared with the issue's project
1447 # fixed_version that is not shared with the issue's project
1443 def self.update_versions(conditions=nil)
1448 def self.update_versions(conditions=nil)
1444 # Only need to update issues with a fixed_version from
1449 # Only need to update issues with a fixed_version from
1445 # a different project and that is not systemwide shared
1450 # a different project and that is not systemwide shared
1446 Issue.joins(:project, :fixed_version).
1451 Issue.joins(:project, :fixed_version).
1447 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1452 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1448 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1453 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1449 " AND #{Version.table_name}.sharing <> 'system'").
1454 " AND #{Version.table_name}.sharing <> 'system'").
1450 where(conditions).each do |issue|
1455 where(conditions).each do |issue|
1451 next if issue.project.nil? || issue.fixed_version.nil?
1456 next if issue.project.nil? || issue.fixed_version.nil?
1452 unless issue.project.shared_versions.include?(issue.fixed_version)
1457 unless issue.project.shared_versions.include?(issue.fixed_version)
1453 issue.init_journal(User.current)
1458 issue.init_journal(User.current)
1454 issue.fixed_version = nil
1459 issue.fixed_version = nil
1455 issue.save
1460 issue.save
1456 end
1461 end
1457 end
1462 end
1458 end
1463 end
1459
1464
1460 # Callback on file attachment
1465 # Callback on file attachment
1461 def attachment_added(obj)
1466 def attachment_added(obj)
1462 if @current_journal && !obj.new_record?
1467 if @current_journal && !obj.new_record?
1463 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1468 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1464 end
1469 end
1465 end
1470 end
1466
1471
1467 # Callback on attachment deletion
1472 # Callback on attachment deletion
1468 def attachment_removed(obj)
1473 def attachment_removed(obj)
1469 if @current_journal && !obj.new_record?
1474 if @current_journal && !obj.new_record?
1470 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1475 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1471 @current_journal.save
1476 @current_journal.save
1472 end
1477 end
1473 end
1478 end
1474
1479
1475 # Default assignment based on category
1480 # Default assignment based on category
1476 def default_assign
1481 def default_assign
1477 if assigned_to.nil? && category && category.assigned_to
1482 if assigned_to.nil? && category && category.assigned_to
1478 self.assigned_to = category.assigned_to
1483 self.assigned_to = category.assigned_to
1479 end
1484 end
1480 end
1485 end
1481
1486
1482 # Updates start/due dates of following issues
1487 # Updates start/due dates of following issues
1483 def reschedule_following_issues
1488 def reschedule_following_issues
1484 if start_date_changed? || due_date_changed?
1489 if start_date_changed? || due_date_changed?
1485 relations_from.each do |relation|
1490 relations_from.each do |relation|
1486 relation.set_issue_to_dates
1491 relation.set_issue_to_dates
1487 end
1492 end
1488 end
1493 end
1489 end
1494 end
1490
1495
1491 # Closes duplicates if the issue is being closed
1496 # Closes duplicates if the issue is being closed
1492 def close_duplicates
1497 def close_duplicates
1493 if closing?
1498 if closing?
1494 duplicates.each do |duplicate|
1499 duplicates.each do |duplicate|
1495 # Reload is needed in case the duplicate was updated by a previous duplicate
1500 # Reload is needed in case the duplicate was updated by a previous duplicate
1496 duplicate.reload
1501 duplicate.reload
1497 # Don't re-close it if it's already closed
1502 # Don't re-close it if it's already closed
1498 next if duplicate.closed?
1503 next if duplicate.closed?
1499 # Same user and notes
1504 # Same user and notes
1500 if @current_journal
1505 if @current_journal
1501 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1506 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1502 end
1507 end
1503 duplicate.update_attribute :status, self.status
1508 duplicate.update_attribute :status, self.status
1504 end
1509 end
1505 end
1510 end
1506 end
1511 end
1507
1512
1508 # Make sure updated_on is updated when adding a note and set updated_on now
1513 # 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
1514 # so we can set closed_on with the same value on closing
1510 def force_updated_on_change
1515 def force_updated_on_change
1511 if @current_journal || changed?
1516 if @current_journal || changed?
1512 self.updated_on = current_time_from_proper_timezone
1517 self.updated_on = current_time_from_proper_timezone
1513 if new_record?
1518 if new_record?
1514 self.created_on = updated_on
1519 self.created_on = updated_on
1515 end
1520 end
1516 end
1521 end
1517 end
1522 end
1518
1523
1519 # Callback for setting closed_on when the issue is closed.
1524 # Callback for setting closed_on when the issue is closed.
1520 # The closed_on attribute stores the time of the last closing
1525 # The closed_on attribute stores the time of the last closing
1521 # and is preserved when the issue is reopened.
1526 # and is preserved when the issue is reopened.
1522 def update_closed_on
1527 def update_closed_on
1523 if closing?
1528 if closing?
1524 self.closed_on = updated_on
1529 self.closed_on = updated_on
1525 end
1530 end
1526 end
1531 end
1527
1532
1528 # Saves the changes in a Journal
1533 # Saves the changes in a Journal
1529 # Called after_save
1534 # Called after_save
1530 def create_journal
1535 def create_journal
1531 if @current_journal
1536 if @current_journal
1532 # attributes changes
1537 # attributes changes
1533 if @attributes_before_change
1538 if @attributes_before_change
1534 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1539 (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]
1540 before = @attributes_before_change[c]
1536 after = send(c)
1541 after = send(c)
1537 next if before == after || (before.blank? && after.blank?)
1542 next if before == after || (before.blank? && after.blank?)
1538 @current_journal.details << JournalDetail.new(:property => 'attr',
1543 @current_journal.details << JournalDetail.new(:property => 'attr',
1539 :prop_key => c,
1544 :prop_key => c,
1540 :old_value => before,
1545 :old_value => before,
1541 :value => after)
1546 :value => after)
1542 }
1547 }
1543 end
1548 end
1544 if @custom_values_before_change
1549 if @custom_values_before_change
1545 # custom fields changes
1550 # custom fields changes
1546 custom_field_values.each {|c|
1551 custom_field_values.each {|c|
1547 before = @custom_values_before_change[c.custom_field_id]
1552 before = @custom_values_before_change[c.custom_field_id]
1548 after = c.value
1553 after = c.value
1549 next if before == after || (before.blank? && after.blank?)
1554 next if before == after || (before.blank? && after.blank?)
1550
1555
1551 if before.is_a?(Array) || after.is_a?(Array)
1556 if before.is_a?(Array) || after.is_a?(Array)
1552 before = [before] unless before.is_a?(Array)
1557 before = [before] unless before.is_a?(Array)
1553 after = [after] unless after.is_a?(Array)
1558 after = [after] unless after.is_a?(Array)
1554
1559
1555 # values removed
1560 # values removed
1556 (before - after).reject(&:blank?).each do |value|
1561 (before - after).reject(&:blank?).each do |value|
1557 @current_journal.details << JournalDetail.new(:property => 'cf',
1562 @current_journal.details << JournalDetail.new(:property => 'cf',
1558 :prop_key => c.custom_field_id,
1563 :prop_key => c.custom_field_id,
1559 :old_value => value,
1564 :old_value => value,
1560 :value => nil)
1565 :value => nil)
1561 end
1566 end
1562 # values added
1567 # values added
1563 (after - before).reject(&:blank?).each do |value|
1568 (after - before).reject(&:blank?).each do |value|
1564 @current_journal.details << JournalDetail.new(:property => 'cf',
1569 @current_journal.details << JournalDetail.new(:property => 'cf',
1565 :prop_key => c.custom_field_id,
1570 :prop_key => c.custom_field_id,
1566 :old_value => nil,
1571 :old_value => nil,
1567 :value => value)
1572 :value => value)
1568 end
1573 end
1569 else
1574 else
1570 @current_journal.details << JournalDetail.new(:property => 'cf',
1575 @current_journal.details << JournalDetail.new(:property => 'cf',
1571 :prop_key => c.custom_field_id,
1576 :prop_key => c.custom_field_id,
1572 :old_value => before,
1577 :old_value => before,
1573 :value => after)
1578 :value => after)
1574 end
1579 end
1575 }
1580 }
1576 end
1581 end
1577 @current_journal.save
1582 @current_journal.save
1578 # reset current journal
1583 # reset current journal
1579 init_journal @current_journal.user, @current_journal.notes
1584 init_journal @current_journal.user, @current_journal.notes
1580 end
1585 end
1581 end
1586 end
1582
1587
1583 def send_notification
1588 def send_notification
1584 if Setting.notified_events.include?('issue_added')
1589 if Setting.notified_events.include?('issue_added')
1585 Mailer.deliver_issue_add(self)
1590 Mailer.deliver_issue_add(self)
1586 end
1591 end
1587 end
1592 end
1588
1593
1589 # Stores the previous assignee so we can still have access
1594 # Stores the previous assignee so we can still have access
1590 # to it during after_save callbacks (assigned_to_id_was is reset)
1595 # to it during after_save callbacks (assigned_to_id_was is reset)
1591 def set_assigned_to_was
1596 def set_assigned_to_was
1592 @previous_assigned_to_id = assigned_to_id_was
1597 @previous_assigned_to_id = assigned_to_id_was
1593 end
1598 end
1594
1599
1595 # Clears the previous assignee at the end of after_save callbacks
1600 # Clears the previous assignee at the end of after_save callbacks
1596 def clear_assigned_to_was
1601 def clear_assigned_to_was
1597 @assigned_to_was = nil
1602 @assigned_to_was = nil
1598 @previous_assigned_to_id = nil
1603 @previous_assigned_to_id = nil
1599 end
1604 end
1600 end
1605 end
General Comments 0
You need to be logged in to leave comments. Login now