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