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