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