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