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