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