##// END OF EJS Templates
Issue#editable_custom_field_values very slow for issues with many custom fields (#23334)....
Jean-Philippe Lang -
r15360:28fc7df5c470
parent child
Show More
@@ -1,1722 +1,1723
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(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 super attrs, *args
422 super attrs, *args
423 end
423 end
424
424
425 def attributes=(new_attributes)
425 def attributes=(new_attributes)
426 assign_attributes new_attributes
426 assign_attributes new_attributes
427 end
427 end
428
428
429 def estimated_hours=(h)
429 def estimated_hours=(h)
430 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)
431 end
431 end
432
432
433 safe_attributes 'project_id',
433 safe_attributes 'project_id',
434 'tracker_id',
434 'tracker_id',
435 'status_id',
435 'status_id',
436 'category_id',
436 'category_id',
437 'assigned_to_id',
437 'assigned_to_id',
438 'priority_id',
438 'priority_id',
439 'fixed_version_id',
439 'fixed_version_id',
440 'subject',
440 'subject',
441 'description',
441 'description',
442 'start_date',
442 'start_date',
443 'due_date',
443 'due_date',
444 'done_ratio',
444 'done_ratio',
445 'estimated_hours',
445 'estimated_hours',
446 'custom_field_values',
446 'custom_field_values',
447 'custom_fields',
447 'custom_fields',
448 'lock_version',
448 'lock_version',
449 'notes',
449 'notes',
450 :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
450 :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
451
451
452 safe_attributes 'notes',
452 safe_attributes 'notes',
453 :if => lambda {|issue, user| issue.notes_addable?(user)}
453 :if => lambda {|issue, user| issue.notes_addable?(user)}
454
454
455 safe_attributes 'private_notes',
455 safe_attributes 'private_notes',
456 :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)}
457
457
458 safe_attributes 'watcher_user_ids',
458 safe_attributes 'watcher_user_ids',
459 :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)}
460
460
461 safe_attributes 'is_private',
461 safe_attributes 'is_private',
462 :if => lambda {|issue, user|
462 :if => lambda {|issue, user|
463 user.allowed_to?(:set_issues_private, issue.project) ||
463 user.allowed_to?(:set_issues_private, issue.project) ||
464 (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))
465 }
465 }
466
466
467 safe_attributes 'parent_issue_id',
467 safe_attributes 'parent_issue_id',
468 :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
468 :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
469 user.allowed_to?(:manage_subtasks, issue.project)}
469 user.allowed_to?(:manage_subtasks, issue.project)}
470
470
471 safe_attributes 'deleted_attachment_ids',
471 safe_attributes 'deleted_attachment_ids',
472 :if => lambda {|issue, user| issue.attachments_deletable?(user)}
472 :if => lambda {|issue, user| issue.attachments_deletable?(user)}
473
473
474 def safe_attribute_names(user=nil)
474 def safe_attribute_names(user=nil)
475 names = super
475 names = super
476 names -= disabled_core_fields
476 names -= disabled_core_fields
477 names -= read_only_attribute_names(user)
477 names -= read_only_attribute_names(user)
478 if new_record?
478 if new_record?
479 # 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
480 names |= %w(project_id)
480 names |= %w(project_id)
481 end
481 end
482 if dates_derived?
482 if dates_derived?
483 names -= %w(start_date due_date)
483 names -= %w(start_date due_date)
484 end
484 end
485 if priority_derived?
485 if priority_derived?
486 names -= %w(priority_id)
486 names -= %w(priority_id)
487 end
487 end
488 if done_ratio_derived?
488 if done_ratio_derived?
489 names -= %w(done_ratio)
489 names -= %w(done_ratio)
490 end
490 end
491 names
491 names
492 end
492 end
493
493
494 # Safely sets attributes
494 # Safely sets attributes
495 # Should be called from controllers instead of #attributes=
495 # Should be called from controllers instead of #attributes=
496 # attr_accessible is too rough because we still want things like
496 # attr_accessible is too rough because we still want things like
497 # Issue.new(:project => foo) to work
497 # Issue.new(:project => foo) to work
498 def safe_attributes=(attrs, user=User.current)
498 def safe_attributes=(attrs, user=User.current)
499 return unless attrs.is_a?(Hash)
499 return unless attrs.is_a?(Hash)
500
500
501 attrs = attrs.deep_dup
501 attrs = attrs.deep_dup
502
502
503 # 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.
504 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
504 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
505 if allowed_target_projects(user).where(:id => p.to_i).exists?
505 if allowed_target_projects(user).where(:id => p.to_i).exists?
506 self.project_id = p
506 self.project_id = p
507 end
507 end
508
508
509 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
510 # Discard submitted category on previous project
510 # Discard submitted category on previous project
511 attrs.delete('category_id')
511 attrs.delete('category_id')
512 end
512 end
513 end
513 end
514
514
515 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
515 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
516 if allowed_target_trackers(user).where(:id => t.to_i).exists?
516 if allowed_target_trackers(user).where(:id => t.to_i).exists?
517 self.tracker_id = t
517 self.tracker_id = t
518 end
518 end
519 end
519 end
520 if project
520 if project
521 # Set a default tracker to accept custom field values
521 # Set a default tracker to accept custom field values
522 # even if tracker is not specified
522 # even if tracker is not specified
523 self.tracker ||= allowed_target_trackers(user).first
523 self.tracker ||= allowed_target_trackers(user).first
524 end
524 end
525
525
526 statuses_allowed = new_statuses_allowed_to(user)
526 statuses_allowed = new_statuses_allowed_to(user)
527 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
527 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
528 if statuses_allowed.collect(&:id).include?(s.to_i)
528 if statuses_allowed.collect(&:id).include?(s.to_i)
529 self.status_id = s
529 self.status_id = s
530 end
530 end
531 end
531 end
532 if new_record? && !statuses_allowed.include?(status)
532 if new_record? && !statuses_allowed.include?(status)
533 self.status = statuses_allowed.first || default_status
533 self.status = statuses_allowed.first || default_status
534 end
534 end
535 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')
536 if u.blank?
536 if u.blank?
537 self.assigned_to_id = nil
537 self.assigned_to_id = nil
538 else
538 else
539 u = u.to_i
539 u = u.to_i
540 if assignable_users.any?{|assignable_user| assignable_user.id == u}
540 if assignable_users.any?{|assignable_user| assignable_user.id == u}
541 self.assigned_to_id = u
541 self.assigned_to_id = u
542 end
542 end
543 end
543 end
544 end
544 end
545
545
546
546
547 attrs = delete_unsafe_attributes(attrs, user)
547 attrs = delete_unsafe_attributes(attrs, user)
548 return if attrs.empty?
548 return if attrs.empty?
549
549
550 if attrs['parent_issue_id'].present?
550 if attrs['parent_issue_id'].present?
551 s = attrs['parent_issue_id'].to_s
551 s = attrs['parent_issue_id'].to_s
552 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]))
553 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
553 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
554 end
554 end
555 end
555 end
556
556
557 if attrs['custom_field_values'].present?
557 if attrs['custom_field_values'].present?
558 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}
559 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)}
560 end
560 end
561
561
562 if attrs['custom_fields'].present?
562 if attrs['custom_fields'].present?
563 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}
564 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)}
565 end
565 end
566
566
567 # mass-assignment security bypass
567 # mass-assignment security bypass
568 assign_attributes attrs, :without_protection => true
568 assign_attributes attrs, :without_protection => true
569 end
569 end
570
570
571 def disabled_core_fields
571 def disabled_core_fields
572 tracker ? tracker.disabled_core_fields : []
572 tracker ? tracker.disabled_core_fields : []
573 end
573 end
574
574
575 # 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
576 def editable_custom_field_values(user=nil)
576 def editable_custom_field_values(user=nil)
577 read_only = read_only_attribute_names(user)
577 visible_custom_field_values(user).reject do |value|
578 visible_custom_field_values(user).reject do |value|
578 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
579 read_only.include?(value.custom_field_id.to_s)
579 end
580 end
580 end
581 end
581
582
582 # Returns the custom fields that can be edited by the given user
583 # Returns the custom fields that can be edited by the given user
583 def editable_custom_fields(user=nil)
584 def editable_custom_fields(user=nil)
584 editable_custom_field_values(user).map(&:custom_field).uniq
585 editable_custom_field_values(user).map(&:custom_field).uniq
585 end
586 end
586
587
587 # Returns the names of attributes that are read-only for user or the current user
588 # Returns the names of attributes that are read-only for user or the current user
588 # For users with multiple roles, the read-only fields are the intersection of
589 # For users with multiple roles, the read-only fields are the intersection of
589 # read-only fields of each role
590 # read-only fields of each role
590 # The result is an array of strings where sustom fields are represented with their ids
591 # The result is an array of strings where sustom fields are represented with their ids
591 #
592 #
592 # Examples:
593 # Examples:
593 # issue.read_only_attribute_names # => ['due_date', '2']
594 # issue.read_only_attribute_names # => ['due_date', '2']
594 # issue.read_only_attribute_names(user) # => []
595 # issue.read_only_attribute_names(user) # => []
595 def read_only_attribute_names(user=nil)
596 def read_only_attribute_names(user=nil)
596 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
597 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
597 end
598 end
598
599
599 # Returns the names of required attributes for user or the current user
600 # Returns the names of required attributes for user or the current user
600 # For users with multiple roles, the required fields are the intersection of
601 # For users with multiple roles, the required fields are the intersection of
601 # required fields of each role
602 # required fields of each role
602 # The result is an array of strings where sustom fields are represented with their ids
603 # The result is an array of strings where sustom fields are represented with their ids
603 #
604 #
604 # Examples:
605 # Examples:
605 # issue.required_attribute_names # => ['due_date', '2']
606 # issue.required_attribute_names # => ['due_date', '2']
606 # issue.required_attribute_names(user) # => []
607 # issue.required_attribute_names(user) # => []
607 def required_attribute_names(user=nil)
608 def required_attribute_names(user=nil)
608 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
609 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
609 end
610 end
610
611
611 # Returns true if the attribute is required for user
612 # Returns true if the attribute is required for user
612 def required_attribute?(name, user=nil)
613 def required_attribute?(name, user=nil)
613 required_attribute_names(user).include?(name.to_s)
614 required_attribute_names(user).include?(name.to_s)
614 end
615 end
615
616
616 # Returns a hash of the workflow rule by attribute for the given user
617 # Returns a hash of the workflow rule by attribute for the given user
617 #
618 #
618 # Examples:
619 # Examples:
619 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
620 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
620 def workflow_rule_by_attribute(user=nil)
621 def workflow_rule_by_attribute(user=nil)
621 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
622 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
622
623
623 user_real = user || User.current
624 user_real = user || User.current
624 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
625 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
625 roles = roles.select(&:consider_workflow?)
626 roles = roles.select(&:consider_workflow?)
626 return {} if roles.empty?
627 return {} if roles.empty?
627
628
628 result = {}
629 result = {}
629 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
630 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
630 if workflow_permissions.any?
631 if workflow_permissions.any?
631 workflow_rules = workflow_permissions.inject({}) do |h, wp|
632 workflow_rules = workflow_permissions.inject({}) do |h, wp|
632 h[wp.field_name] ||= {}
633 h[wp.field_name] ||= {}
633 h[wp.field_name][wp.role_id] = wp.rule
634 h[wp.field_name][wp.role_id] = wp.rule
634 h
635 h
635 end
636 end
636 fields_with_roles = {}
637 fields_with_roles = {}
637 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
638 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
638 fields_with_roles[field_id] ||= []
639 fields_with_roles[field_id] ||= []
639 fields_with_roles[field_id] << role_id
640 fields_with_roles[field_id] << role_id
640 end
641 end
641 roles.each do |role|
642 roles.each do |role|
642 fields_with_roles.each do |field_id, role_ids|
643 fields_with_roles.each do |field_id, role_ids|
643 unless role_ids.include?(role.id)
644 unless role_ids.include?(role.id)
644 field_name = field_id.to_s
645 field_name = field_id.to_s
645 workflow_rules[field_name] ||= {}
646 workflow_rules[field_name] ||= {}
646 workflow_rules[field_name][role.id] = 'readonly'
647 workflow_rules[field_name][role.id] = 'readonly'
647 end
648 end
648 end
649 end
649 end
650 end
650 workflow_rules.each do |attr, rules|
651 workflow_rules.each do |attr, rules|
651 next if rules.size < roles.size
652 next if rules.size < roles.size
652 uniq_rules = rules.values.uniq
653 uniq_rules = rules.values.uniq
653 if uniq_rules.size == 1
654 if uniq_rules.size == 1
654 result[attr] = uniq_rules.first
655 result[attr] = uniq_rules.first
655 else
656 else
656 result[attr] = 'required'
657 result[attr] = 'required'
657 end
658 end
658 end
659 end
659 end
660 end
660 @workflow_rule_by_attribute = result if user.nil?
661 @workflow_rule_by_attribute = result if user.nil?
661 result
662 result
662 end
663 end
663 private :workflow_rule_by_attribute
664 private :workflow_rule_by_attribute
664
665
665 def done_ratio
666 def done_ratio
666 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
667 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
667 status.default_done_ratio
668 status.default_done_ratio
668 else
669 else
669 read_attribute(:done_ratio)
670 read_attribute(:done_ratio)
670 end
671 end
671 end
672 end
672
673
673 def self.use_status_for_done_ratio?
674 def self.use_status_for_done_ratio?
674 Setting.issue_done_ratio == 'issue_status'
675 Setting.issue_done_ratio == 'issue_status'
675 end
676 end
676
677
677 def self.use_field_for_done_ratio?
678 def self.use_field_for_done_ratio?
678 Setting.issue_done_ratio == 'issue_field'
679 Setting.issue_done_ratio == 'issue_field'
679 end
680 end
680
681
681 def validate_issue
682 def validate_issue
682 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
683 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
683 errors.add :due_date, :greater_than_start_date
684 errors.add :due_date, :greater_than_start_date
684 end
685 end
685
686
686 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
687 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
687 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
688 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
688 end
689 end
689
690
690 if fixed_version
691 if fixed_version
691 if !assignable_versions.include?(fixed_version)
692 if !assignable_versions.include?(fixed_version)
692 errors.add :fixed_version_id, :inclusion
693 errors.add :fixed_version_id, :inclusion
693 elsif reopening? && fixed_version.closed?
694 elsif reopening? && fixed_version.closed?
694 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
695 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
695 end
696 end
696 end
697 end
697
698
698 # Checks that the issue can not be added/moved to a disabled tracker
699 # Checks that the issue can not be added/moved to a disabled tracker
699 if project && (tracker_id_changed? || project_id_changed?)
700 if project && (tracker_id_changed? || project_id_changed?)
700 if tracker && !project.trackers.include?(tracker)
701 if tracker && !project.trackers.include?(tracker)
701 errors.add :tracker_id, :inclusion
702 errors.add :tracker_id, :inclusion
702 end
703 end
703 end
704 end
704
705
705 # Checks parent issue assignment
706 # Checks parent issue assignment
706 if @invalid_parent_issue_id.present?
707 if @invalid_parent_issue_id.present?
707 errors.add :parent_issue_id, :invalid
708 errors.add :parent_issue_id, :invalid
708 elsif @parent_issue
709 elsif @parent_issue
709 if !valid_parent_project?(@parent_issue)
710 if !valid_parent_project?(@parent_issue)
710 errors.add :parent_issue_id, :invalid
711 errors.add :parent_issue_id, :invalid
711 elsif (@parent_issue != parent) && (
712 elsif (@parent_issue != parent) && (
712 self.would_reschedule?(@parent_issue) ||
713 self.would_reschedule?(@parent_issue) ||
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)}}
714 @parent_issue.self_and_ancestors.any? {|a| a.relations_from.any? {|r| r.relation_type == IssueRelation::TYPE_PRECEDES && r.issue_to.would_reschedule?(self)}}
714 )
715 )
715 errors.add :parent_issue_id, :invalid
716 errors.add :parent_issue_id, :invalid
716 elsif !new_record?
717 elsif !new_record?
717 # moving an existing issue
718 # moving an existing issue
718 if move_possible?(@parent_issue)
719 if move_possible?(@parent_issue)
719 # move accepted
720 # move accepted
720 else
721 else
721 errors.add :parent_issue_id, :invalid
722 errors.add :parent_issue_id, :invalid
722 end
723 end
723 end
724 end
724 end
725 end
725 end
726 end
726
727
727 # Validates the issue against additional workflow requirements
728 # Validates the issue against additional workflow requirements
728 def validate_required_fields
729 def validate_required_fields
729 user = new_record? ? author : current_journal.try(:user)
730 user = new_record? ? author : current_journal.try(:user)
730
731
731 required_attribute_names(user).each do |attribute|
732 required_attribute_names(user).each do |attribute|
732 if attribute =~ /^\d+$/
733 if attribute =~ /^\d+$/
733 attribute = attribute.to_i
734 attribute = attribute.to_i
734 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
735 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
735 if v && Array(v.value).detect(&:present?).nil?
736 if v && Array(v.value).detect(&:present?).nil?
736 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
737 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
737 end
738 end
738 else
739 else
739 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
740 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
740 next if attribute == 'category_id' && project.try(:issue_categories).blank?
741 next if attribute == 'category_id' && project.try(:issue_categories).blank?
741 next if attribute == 'fixed_version_id' && assignable_versions.blank?
742 next if attribute == 'fixed_version_id' && assignable_versions.blank?
742 errors.add attribute, :blank
743 errors.add attribute, :blank
743 end
744 end
744 end
745 end
745 end
746 end
746 end
747 end
747
748
748 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
749 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
749 # so that custom values that are not editable are not validated (eg. a custom field that
750 # so that custom values that are not editable are not validated (eg. a custom field that
750 # is marked as required should not trigger a validation error if the user is not allowed
751 # is marked as required should not trigger a validation error if the user is not allowed
751 # to edit this field).
752 # to edit this field).
752 def validate_custom_field_values
753 def validate_custom_field_values
753 user = new_record? ? author : current_journal.try(:user)
754 user = new_record? ? author : current_journal.try(:user)
754 if new_record? || custom_field_values_changed?
755 if new_record? || custom_field_values_changed?
755 editable_custom_field_values(user).each(&:validate_value)
756 editable_custom_field_values(user).each(&:validate_value)
756 end
757 end
757 end
758 end
758
759
759 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
760 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
760 # even if the user turns off the setting later
761 # even if the user turns off the setting later
761 def update_done_ratio_from_issue_status
762 def update_done_ratio_from_issue_status
762 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
763 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
763 self.done_ratio = status.default_done_ratio
764 self.done_ratio = status.default_done_ratio
764 end
765 end
765 end
766 end
766
767
767 def init_journal(user, notes = "")
768 def init_journal(user, notes = "")
768 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
769 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
769 end
770 end
770
771
771 # Returns the current journal or nil if it's not initialized
772 # Returns the current journal or nil if it's not initialized
772 def current_journal
773 def current_journal
773 @current_journal
774 @current_journal
774 end
775 end
775
776
776 # Returns the names of attributes that are journalized when updating the issue
777 # Returns the names of attributes that are journalized when updating the issue
777 def journalized_attribute_names
778 def journalized_attribute_names
778 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
779 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
779 if tracker
780 if tracker
780 names -= tracker.disabled_core_fields
781 names -= tracker.disabled_core_fields
781 end
782 end
782 names
783 names
783 end
784 end
784
785
785 # Returns the id of the last journal or nil
786 # Returns the id of the last journal or nil
786 def last_journal_id
787 def last_journal_id
787 if new_record?
788 if new_record?
788 nil
789 nil
789 else
790 else
790 journals.maximum(:id)
791 journals.maximum(:id)
791 end
792 end
792 end
793 end
793
794
794 # Returns a scope for journals that have an id greater than journal_id
795 # Returns a scope for journals that have an id greater than journal_id
795 def journals_after(journal_id)
796 def journals_after(journal_id)
796 scope = journals.reorder("#{Journal.table_name}.id ASC")
797 scope = journals.reorder("#{Journal.table_name}.id ASC")
797 if journal_id.present?
798 if journal_id.present?
798 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
799 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
799 end
800 end
800 scope
801 scope
801 end
802 end
802
803
803 # Returns the initial status of the issue
804 # Returns the initial status of the issue
804 # Returns nil for a new issue
805 # Returns nil for a new issue
805 def status_was
806 def status_was
806 if status_id_changed?
807 if status_id_changed?
807 if status_id_was.to_i > 0
808 if status_id_was.to_i > 0
808 @status_was ||= IssueStatus.find_by_id(status_id_was)
809 @status_was ||= IssueStatus.find_by_id(status_id_was)
809 end
810 end
810 else
811 else
811 @status_was ||= status
812 @status_was ||= status
812 end
813 end
813 end
814 end
814
815
815 # Return true if the issue is closed, otherwise false
816 # Return true if the issue is closed, otherwise false
816 def closed?
817 def closed?
817 status.present? && status.is_closed?
818 status.present? && status.is_closed?
818 end
819 end
819
820
820 # Returns true if the issue was closed when loaded
821 # Returns true if the issue was closed when loaded
821 def was_closed?
822 def was_closed?
822 status_was.present? && status_was.is_closed?
823 status_was.present? && status_was.is_closed?
823 end
824 end
824
825
825 # Return true if the issue is being reopened
826 # Return true if the issue is being reopened
826 def reopening?
827 def reopening?
827 if new_record?
828 if new_record?
828 false
829 false
829 else
830 else
830 status_id_changed? && !closed? && was_closed?
831 status_id_changed? && !closed? && was_closed?
831 end
832 end
832 end
833 end
833 alias :reopened? :reopening?
834 alias :reopened? :reopening?
834
835
835 # Return true if the issue is being closed
836 # Return true if the issue is being closed
836 def closing?
837 def closing?
837 if new_record?
838 if new_record?
838 closed?
839 closed?
839 else
840 else
840 status_id_changed? && closed? && !was_closed?
841 status_id_changed? && closed? && !was_closed?
841 end
842 end
842 end
843 end
843
844
844 # Returns true if the issue is overdue
845 # Returns true if the issue is overdue
845 def overdue?
846 def overdue?
846 due_date.present? && (due_date < User.current.today) && !closed?
847 due_date.present? && (due_date < User.current.today) && !closed?
847 end
848 end
848
849
849 # Is the amount of work done less than it should for the due date
850 # Is the amount of work done less than it should for the due date
850 def behind_schedule?
851 def behind_schedule?
851 return false if start_date.nil? || due_date.nil?
852 return false if start_date.nil? || due_date.nil?
852 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
853 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
853 return done_date <= User.current.today
854 return done_date <= User.current.today
854 end
855 end
855
856
856 # Does this issue have children?
857 # Does this issue have children?
857 def children?
858 def children?
858 !leaf?
859 !leaf?
859 end
860 end
860
861
861 # Users the issue can be assigned to
862 # Users the issue can be assigned to
862 def assignable_users
863 def assignable_users
863 users = project.assignable_users(tracker).to_a
864 users = project.assignable_users(tracker).to_a
864 users << author if author && author.active?
865 users << author if author && author.active?
865 users << assigned_to if assigned_to
866 users << assigned_to if assigned_to
866 users.uniq.sort
867 users.uniq.sort
867 end
868 end
868
869
869 # Versions that the issue can be assigned to
870 # Versions that the issue can be assigned to
870 def assignable_versions
871 def assignable_versions
871 return @assignable_versions if @assignable_versions
872 return @assignable_versions if @assignable_versions
872
873
873 versions = project.shared_versions.open.to_a
874 versions = project.shared_versions.open.to_a
874 if fixed_version
875 if fixed_version
875 if fixed_version_id_changed?
876 if fixed_version_id_changed?
876 # nothing to do
877 # nothing to do
877 elsif project_id_changed?
878 elsif project_id_changed?
878 if project.shared_versions.include?(fixed_version)
879 if project.shared_versions.include?(fixed_version)
879 versions << fixed_version
880 versions << fixed_version
880 end
881 end
881 else
882 else
882 versions << fixed_version
883 versions << fixed_version
883 end
884 end
884 end
885 end
885 @assignable_versions = versions.uniq.sort
886 @assignable_versions = versions.uniq.sort
886 end
887 end
887
888
888 # Returns true if this issue is blocked by another issue that is still open
889 # Returns true if this issue is blocked by another issue that is still open
889 def blocked?
890 def blocked?
890 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
891 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
891 end
892 end
892
893
893 # Returns the default status of the issue based on its tracker
894 # Returns the default status of the issue based on its tracker
894 # Returns nil if tracker is nil
895 # Returns nil if tracker is nil
895 def default_status
896 def default_status
896 tracker.try(:default_status)
897 tracker.try(:default_status)
897 end
898 end
898
899
899 # Returns an array of statuses that user is able to apply
900 # Returns an array of statuses that user is able to apply
900 def new_statuses_allowed_to(user=User.current, include_default=false)
901 def new_statuses_allowed_to(user=User.current, include_default=false)
901 if new_record? && @copied_from
902 if new_record? && @copied_from
902 [default_status, @copied_from.status].compact.uniq.sort
903 [default_status, @copied_from.status].compact.uniq.sort
903 else
904 else
904 initial_status = nil
905 initial_status = nil
905 if new_record?
906 if new_record?
906 # nop
907 # nop
907 elsif tracker_id_changed?
908 elsif tracker_id_changed?
908 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
909 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
909 initial_status = default_status
910 initial_status = default_status
910 elsif tracker.issue_status_ids.include?(status_id_was)
911 elsif tracker.issue_status_ids.include?(status_id_was)
911 initial_status = IssueStatus.find_by_id(status_id_was)
912 initial_status = IssueStatus.find_by_id(status_id_was)
912 else
913 else
913 initial_status = default_status
914 initial_status = default_status
914 end
915 end
915 else
916 else
916 initial_status = status_was
917 initial_status = status_was
917 end
918 end
918
919
919 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
920 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
920 assignee_transitions_allowed = initial_assigned_to_id.present? &&
921 assignee_transitions_allowed = initial_assigned_to_id.present? &&
921 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
922 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
922
923
923 statuses = []
924 statuses = []
924 statuses += IssueStatus.new_statuses_allowed(
925 statuses += IssueStatus.new_statuses_allowed(
925 initial_status,
926 initial_status,
926 user.admin ? Role.all.to_a : user.roles_for_project(project),
927 user.admin ? Role.all.to_a : user.roles_for_project(project),
927 tracker,
928 tracker,
928 author == user,
929 author == user,
929 assignee_transitions_allowed
930 assignee_transitions_allowed
930 )
931 )
931 statuses << initial_status unless statuses.empty?
932 statuses << initial_status unless statuses.empty?
932 statuses << default_status if include_default || (new_record? && statuses.empty?)
933 statuses << default_status if include_default || (new_record? && statuses.empty?)
933 statuses = statuses.compact.uniq.sort
934 statuses = statuses.compact.uniq.sort
934 if blocked?
935 if blocked?
935 statuses.reject!(&:is_closed?)
936 statuses.reject!(&:is_closed?)
936 end
937 end
937 statuses
938 statuses
938 end
939 end
939 end
940 end
940
941
941 # Returns the previous assignee (user or group) if changed
942 # Returns the previous assignee (user or group) if changed
942 def assigned_to_was
943 def assigned_to_was
943 # assigned_to_id_was is reset before after_save callbacks
944 # assigned_to_id_was is reset before after_save callbacks
944 user_id = @previous_assigned_to_id || assigned_to_id_was
945 user_id = @previous_assigned_to_id || assigned_to_id_was
945 if user_id && user_id != assigned_to_id
946 if user_id && user_id != assigned_to_id
946 @assigned_to_was ||= Principal.find_by_id(user_id)
947 @assigned_to_was ||= Principal.find_by_id(user_id)
947 end
948 end
948 end
949 end
949
950
950 # Returns the original tracker
951 # Returns the original tracker
951 def tracker_was
952 def tracker_was
952 Tracker.find_by_id(tracker_id_was)
953 Tracker.find_by_id(tracker_id_was)
953 end
954 end
954
955
955 # Returns the users that should be notified
956 # Returns the users that should be notified
956 def notified_users
957 def notified_users
957 notified = []
958 notified = []
958 # Author and assignee are always notified unless they have been
959 # Author and assignee are always notified unless they have been
959 # locked or don't want to be notified
960 # locked or don't want to be notified
960 notified << author if author
961 notified << author if author
961 if assigned_to
962 if assigned_to
962 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
963 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
963 end
964 end
964 if assigned_to_was
965 if assigned_to_was
965 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
966 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
966 end
967 end
967 notified = notified.select {|u| u.active? && u.notify_about?(self)}
968 notified = notified.select {|u| u.active? && u.notify_about?(self)}
968
969
969 notified += project.notified_users
970 notified += project.notified_users
970 notified.uniq!
971 notified.uniq!
971 # Remove users that can not view the issue
972 # Remove users that can not view the issue
972 notified.reject! {|user| !visible?(user)}
973 notified.reject! {|user| !visible?(user)}
973 notified
974 notified
974 end
975 end
975
976
976 # Returns the email addresses that should be notified
977 # Returns the email addresses that should be notified
977 def recipients
978 def recipients
978 notified_users.collect(&:mail)
979 notified_users.collect(&:mail)
979 end
980 end
980
981
981 def each_notification(users, &block)
982 def each_notification(users, &block)
982 if users.any?
983 if users.any?
983 if custom_field_values.detect {|value| !value.custom_field.visible?}
984 if custom_field_values.detect {|value| !value.custom_field.visible?}
984 users_by_custom_field_visibility = users.group_by do |user|
985 users_by_custom_field_visibility = users.group_by do |user|
985 visible_custom_field_values(user).map(&:custom_field_id).sort
986 visible_custom_field_values(user).map(&:custom_field_id).sort
986 end
987 end
987 users_by_custom_field_visibility.values.each do |users|
988 users_by_custom_field_visibility.values.each do |users|
988 yield(users)
989 yield(users)
989 end
990 end
990 else
991 else
991 yield(users)
992 yield(users)
992 end
993 end
993 end
994 end
994 end
995 end
995
996
996 def notify?
997 def notify?
997 @notify != false
998 @notify != false
998 end
999 end
999
1000
1000 def notify=(arg)
1001 def notify=(arg)
1001 @notify = arg
1002 @notify = arg
1002 end
1003 end
1003
1004
1004 # Returns the number of hours spent on this issue
1005 # Returns the number of hours spent on this issue
1005 def spent_hours
1006 def spent_hours
1006 @spent_hours ||= time_entries.sum(:hours) || 0
1007 @spent_hours ||= time_entries.sum(:hours) || 0
1007 end
1008 end
1008
1009
1009 # Returns the total number of hours spent on this issue and its descendants
1010 # Returns the total number of hours spent on this issue and its descendants
1010 def total_spent_hours
1011 def total_spent_hours
1011 @total_spent_hours ||= if leaf?
1012 @total_spent_hours ||= if leaf?
1012 spent_hours
1013 spent_hours
1013 else
1014 else
1014 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
1015 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
1015 end
1016 end
1016 end
1017 end
1017
1018
1018 def total_estimated_hours
1019 def total_estimated_hours
1019 if leaf?
1020 if leaf?
1020 estimated_hours
1021 estimated_hours
1021 else
1022 else
1022 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
1023 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
1023 end
1024 end
1024 end
1025 end
1025
1026
1026 def relations
1027 def relations
1027 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1028 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1028 end
1029 end
1029
1030
1030 # Preloads relations for a collection of issues
1031 # Preloads relations for a collection of issues
1031 def self.load_relations(issues)
1032 def self.load_relations(issues)
1032 if issues.any?
1033 if issues.any?
1033 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
1034 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
1034 issues.each do |issue|
1035 issues.each do |issue|
1035 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
1036 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
1036 end
1037 end
1037 end
1038 end
1038 end
1039 end
1039
1040
1040 # Preloads visible spent time for a collection of issues
1041 # Preloads visible spent time for a collection of issues
1041 def self.load_visible_spent_hours(issues, user=User.current)
1042 def self.load_visible_spent_hours(issues, user=User.current)
1042 if issues.any?
1043 if issues.any?
1043 hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
1044 hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
1044 issues.each do |issue|
1045 issues.each do |issue|
1045 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
1046 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
1046 end
1047 end
1047 end
1048 end
1048 end
1049 end
1049
1050
1050 # Preloads visible total spent time for a collection of issues
1051 # Preloads visible total spent time for a collection of issues
1051 def self.load_visible_total_spent_hours(issues, user=User.current)
1052 def self.load_visible_total_spent_hours(issues, user=User.current)
1052 if issues.any?
1053 if issues.any?
1053 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
1054 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
1054 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
1055 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
1055 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
1056 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
1056 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
1057 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
1057 issues.each do |issue|
1058 issues.each do |issue|
1058 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
1059 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
1059 end
1060 end
1060 end
1061 end
1061 end
1062 end
1062
1063
1063 # Preloads visible relations for a collection of issues
1064 # Preloads visible relations for a collection of issues
1064 def self.load_visible_relations(issues, user=User.current)
1065 def self.load_visible_relations(issues, user=User.current)
1065 if issues.any?
1066 if issues.any?
1066 issue_ids = issues.map(&:id)
1067 issue_ids = issues.map(&:id)
1067 # Relations with issue_from in given issues and visible issue_to
1068 # Relations with issue_from in given issues and visible issue_to
1068 relations_from = IssueRelation.joins(:issue_to => :project).
1069 relations_from = IssueRelation.joins(:issue_to => :project).
1069 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
1070 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
1070 # Relations with issue_to in given issues and visible issue_from
1071 # Relations with issue_to in given issues and visible issue_from
1071 relations_to = IssueRelation.joins(:issue_from => :project).
1072 relations_to = IssueRelation.joins(:issue_from => :project).
1072 where(visible_condition(user)).
1073 where(visible_condition(user)).
1073 where(:issue_to_id => issue_ids).to_a
1074 where(:issue_to_id => issue_ids).to_a
1074 issues.each do |issue|
1075 issues.each do |issue|
1075 relations =
1076 relations =
1076 relations_from.select {|relation| relation.issue_from_id == issue.id} +
1077 relations_from.select {|relation| relation.issue_from_id == issue.id} +
1077 relations_to.select {|relation| relation.issue_to_id == issue.id}
1078 relations_to.select {|relation| relation.issue_to_id == issue.id}
1078
1079
1079 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
1080 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
1080 end
1081 end
1081 end
1082 end
1082 end
1083 end
1083
1084
1084 # Finds an issue relation given its id.
1085 # Finds an issue relation given its id.
1085 def find_relation(relation_id)
1086 def find_relation(relation_id)
1086 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
1087 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
1087 end
1088 end
1088
1089
1089 # Returns true if this issue blocks the other issue, otherwise returns false
1090 # Returns true if this issue blocks the other issue, otherwise returns false
1090 def blocks?(other)
1091 def blocks?(other)
1091 all = [self]
1092 all = [self]
1092 last = [self]
1093 last = [self]
1093 while last.any?
1094 while last.any?
1094 current = last.map {|i| i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to)}.flatten.uniq
1095 current = last.map {|i| i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to)}.flatten.uniq
1095 current -= last
1096 current -= last
1096 current -= all
1097 current -= all
1097 return true if current.include?(other)
1098 return true if current.include?(other)
1098 last = current
1099 last = current
1099 all += last
1100 all += last
1100 end
1101 end
1101 false
1102 false
1102 end
1103 end
1103
1104
1104 # Returns true if the other issue might be rescheduled if the start/due dates of this issue change
1105 # Returns true if the other issue might be rescheduled if the start/due dates of this issue change
1105 def would_reschedule?(other)
1106 def would_reschedule?(other)
1106 all = [self]
1107 all = [self]
1107 last = [self]
1108 last = [self]
1108 while last.any?
1109 while last.any?
1109 current = last.map {|i|
1110 current = last.map {|i|
1110 i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
1111 i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
1111 i.leaves.to_a +
1112 i.leaves.to_a +
1112 i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)}
1113 i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)}
1113 }.flatten.uniq
1114 }.flatten.uniq
1114 current -= last
1115 current -= last
1115 current -= all
1116 current -= all
1116 return true if current.include?(other)
1117 return true if current.include?(other)
1117 last = current
1118 last = current
1118 all += last
1119 all += last
1119 end
1120 end
1120 false
1121 false
1121 end
1122 end
1122
1123
1123 # Returns an array of issues that duplicate this one
1124 # Returns an array of issues that duplicate this one
1124 def duplicates
1125 def duplicates
1125 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1126 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1126 end
1127 end
1127
1128
1128 # Returns the due date or the target due date if any
1129 # Returns the due date or the target due date if any
1129 # Used on gantt chart
1130 # Used on gantt chart
1130 def due_before
1131 def due_before
1131 due_date || (fixed_version ? fixed_version.effective_date : nil)
1132 due_date || (fixed_version ? fixed_version.effective_date : nil)
1132 end
1133 end
1133
1134
1134 # Returns the time scheduled for this issue.
1135 # Returns the time scheduled for this issue.
1135 #
1136 #
1136 # Example:
1137 # Example:
1137 # Start Date: 2/26/09, End Date: 3/04/09
1138 # Start Date: 2/26/09, End Date: 3/04/09
1138 # duration => 6
1139 # duration => 6
1139 def duration
1140 def duration
1140 (start_date && due_date) ? due_date - start_date : 0
1141 (start_date && due_date) ? due_date - start_date : 0
1141 end
1142 end
1142
1143
1143 # Returns the duration in working days
1144 # Returns the duration in working days
1144 def working_duration
1145 def working_duration
1145 (start_date && due_date) ? working_days(start_date, due_date) : 0
1146 (start_date && due_date) ? working_days(start_date, due_date) : 0
1146 end
1147 end
1147
1148
1148 def soonest_start(reload=false)
1149 def soonest_start(reload=false)
1149 if @soonest_start.nil? || reload
1150 if @soonest_start.nil? || reload
1150 relations_to.reload if reload
1151 relations_to.reload if reload
1151 dates = relations_to.collect{|relation| relation.successor_soonest_start}
1152 dates = relations_to.collect{|relation| relation.successor_soonest_start}
1152 p = @parent_issue || parent
1153 p = @parent_issue || parent
1153 if p && Setting.parent_issue_dates == 'derived'
1154 if p && Setting.parent_issue_dates == 'derived'
1154 dates << p.soonest_start
1155 dates << p.soonest_start
1155 end
1156 end
1156 @soonest_start = dates.compact.max
1157 @soonest_start = dates.compact.max
1157 end
1158 end
1158 @soonest_start
1159 @soonest_start
1159 end
1160 end
1160
1161
1161 # Sets start_date on the given date or the next working day
1162 # Sets start_date on the given date or the next working day
1162 # and changes due_date to keep the same working duration.
1163 # and changes due_date to keep the same working duration.
1163 def reschedule_on(date)
1164 def reschedule_on(date)
1164 wd = working_duration
1165 wd = working_duration
1165 date = next_working_date(date)
1166 date = next_working_date(date)
1166 self.start_date = date
1167 self.start_date = date
1167 self.due_date = add_working_days(date, wd)
1168 self.due_date = add_working_days(date, wd)
1168 end
1169 end
1169
1170
1170 # Reschedules the issue on the given date or the next working day and saves the record.
1171 # Reschedules the issue on the given date or the next working day and saves the record.
1171 # If the issue is a parent task, this is done by rescheduling its subtasks.
1172 # If the issue is a parent task, this is done by rescheduling its subtasks.
1172 def reschedule_on!(date)
1173 def reschedule_on!(date)
1173 return if date.nil?
1174 return if date.nil?
1174 if leaf? || !dates_derived?
1175 if leaf? || !dates_derived?
1175 if start_date.nil? || start_date != date
1176 if start_date.nil? || start_date != date
1176 if start_date && start_date > date
1177 if start_date && start_date > date
1177 # Issue can not be moved earlier than its soonest start date
1178 # Issue can not be moved earlier than its soonest start date
1178 date = [soonest_start(true), date].compact.max
1179 date = [soonest_start(true), date].compact.max
1179 end
1180 end
1180 reschedule_on(date)
1181 reschedule_on(date)
1181 begin
1182 begin
1182 save
1183 save
1183 rescue ActiveRecord::StaleObjectError
1184 rescue ActiveRecord::StaleObjectError
1184 reload
1185 reload
1185 reschedule_on(date)
1186 reschedule_on(date)
1186 save
1187 save
1187 end
1188 end
1188 end
1189 end
1189 else
1190 else
1190 leaves.each do |leaf|
1191 leaves.each do |leaf|
1191 if leaf.start_date
1192 if leaf.start_date
1192 # Only move subtask if it starts at the same date as the parent
1193 # Only move subtask if it starts at the same date as the parent
1193 # or if it starts before the given date
1194 # or if it starts before the given date
1194 if start_date == leaf.start_date || date > leaf.start_date
1195 if start_date == leaf.start_date || date > leaf.start_date
1195 leaf.reschedule_on!(date)
1196 leaf.reschedule_on!(date)
1196 end
1197 end
1197 else
1198 else
1198 leaf.reschedule_on!(date)
1199 leaf.reschedule_on!(date)
1199 end
1200 end
1200 end
1201 end
1201 end
1202 end
1202 end
1203 end
1203
1204
1204 def dates_derived?
1205 def dates_derived?
1205 !leaf? && Setting.parent_issue_dates == 'derived'
1206 !leaf? && Setting.parent_issue_dates == 'derived'
1206 end
1207 end
1207
1208
1208 def priority_derived?
1209 def priority_derived?
1209 !leaf? && Setting.parent_issue_priority == 'derived'
1210 !leaf? && Setting.parent_issue_priority == 'derived'
1210 end
1211 end
1211
1212
1212 def done_ratio_derived?
1213 def done_ratio_derived?
1213 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1214 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1214 end
1215 end
1215
1216
1216 def <=>(issue)
1217 def <=>(issue)
1217 if issue.nil?
1218 if issue.nil?
1218 -1
1219 -1
1219 elsif root_id != issue.root_id
1220 elsif root_id != issue.root_id
1220 (root_id || 0) <=> (issue.root_id || 0)
1221 (root_id || 0) <=> (issue.root_id || 0)
1221 else
1222 else
1222 (lft || 0) <=> (issue.lft || 0)
1223 (lft || 0) <=> (issue.lft || 0)
1223 end
1224 end
1224 end
1225 end
1225
1226
1226 def to_s
1227 def to_s
1227 "#{tracker} ##{id}: #{subject}"
1228 "#{tracker} ##{id}: #{subject}"
1228 end
1229 end
1229
1230
1230 # Returns a string of css classes that apply to the issue
1231 # Returns a string of css classes that apply to the issue
1231 def css_classes(user=User.current)
1232 def css_classes(user=User.current)
1232 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1233 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1233 s << ' closed' if closed?
1234 s << ' closed' if closed?
1234 s << ' overdue' if overdue?
1235 s << ' overdue' if overdue?
1235 s << ' child' if child?
1236 s << ' child' if child?
1236 s << ' parent' unless leaf?
1237 s << ' parent' unless leaf?
1237 s << ' private' if is_private?
1238 s << ' private' if is_private?
1238 if user.logged?
1239 if user.logged?
1239 s << ' created-by-me' if author_id == user.id
1240 s << ' created-by-me' if author_id == user.id
1240 s << ' assigned-to-me' if assigned_to_id == user.id
1241 s << ' assigned-to-me' if assigned_to_id == user.id
1241 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1242 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1242 end
1243 end
1243 s
1244 s
1244 end
1245 end
1245
1246
1246 # Unassigns issues from +version+ if it's no longer shared with issue's project
1247 # Unassigns issues from +version+ if it's no longer shared with issue's project
1247 def self.update_versions_from_sharing_change(version)
1248 def self.update_versions_from_sharing_change(version)
1248 # Update issues assigned to the version
1249 # Update issues assigned to the version
1249 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1250 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1250 end
1251 end
1251
1252
1252 # Unassigns issues from versions that are no longer shared
1253 # Unassigns issues from versions that are no longer shared
1253 # after +project+ was moved
1254 # after +project+ was moved
1254 def self.update_versions_from_hierarchy_change(project)
1255 def self.update_versions_from_hierarchy_change(project)
1255 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1256 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1256 # Update issues of the moved projects and issues assigned to a version of a moved project
1257 # Update issues of the moved projects and issues assigned to a version of a moved project
1257 Issue.update_versions(
1258 Issue.update_versions(
1258 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1259 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1259 moved_project_ids, moved_project_ids]
1260 moved_project_ids, moved_project_ids]
1260 )
1261 )
1261 end
1262 end
1262
1263
1263 def parent_issue_id=(arg)
1264 def parent_issue_id=(arg)
1264 s = arg.to_s.strip.presence
1265 s = arg.to_s.strip.presence
1265 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1266 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1266 @invalid_parent_issue_id = nil
1267 @invalid_parent_issue_id = nil
1267 elsif s.blank?
1268 elsif s.blank?
1268 @parent_issue = nil
1269 @parent_issue = nil
1269 @invalid_parent_issue_id = nil
1270 @invalid_parent_issue_id = nil
1270 else
1271 else
1271 @parent_issue = nil
1272 @parent_issue = nil
1272 @invalid_parent_issue_id = arg
1273 @invalid_parent_issue_id = arg
1273 end
1274 end
1274 end
1275 end
1275
1276
1276 def parent_issue_id
1277 def parent_issue_id
1277 if @invalid_parent_issue_id
1278 if @invalid_parent_issue_id
1278 @invalid_parent_issue_id
1279 @invalid_parent_issue_id
1279 elsif instance_variable_defined? :@parent_issue
1280 elsif instance_variable_defined? :@parent_issue
1280 @parent_issue.nil? ? nil : @parent_issue.id
1281 @parent_issue.nil? ? nil : @parent_issue.id
1281 else
1282 else
1282 parent_id
1283 parent_id
1283 end
1284 end
1284 end
1285 end
1285
1286
1286 def set_parent_id
1287 def set_parent_id
1287 self.parent_id = parent_issue_id
1288 self.parent_id = parent_issue_id
1288 end
1289 end
1289
1290
1290 # Returns true if issue's project is a valid
1291 # Returns true if issue's project is a valid
1291 # parent issue project
1292 # parent issue project
1292 def valid_parent_project?(issue=parent)
1293 def valid_parent_project?(issue=parent)
1293 return true if issue.nil? || issue.project_id == project_id
1294 return true if issue.nil? || issue.project_id == project_id
1294
1295
1295 case Setting.cross_project_subtasks
1296 case Setting.cross_project_subtasks
1296 when 'system'
1297 when 'system'
1297 true
1298 true
1298 when 'tree'
1299 when 'tree'
1299 issue.project.root == project.root
1300 issue.project.root == project.root
1300 when 'hierarchy'
1301 when 'hierarchy'
1301 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1302 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1302 when 'descendants'
1303 when 'descendants'
1303 issue.project.is_or_is_ancestor_of?(project)
1304 issue.project.is_or_is_ancestor_of?(project)
1304 else
1305 else
1305 false
1306 false
1306 end
1307 end
1307 end
1308 end
1308
1309
1309 # Returns an issue scope based on project and scope
1310 # Returns an issue scope based on project and scope
1310 def self.cross_project_scope(project, scope=nil)
1311 def self.cross_project_scope(project, scope=nil)
1311 if project.nil?
1312 if project.nil?
1312 return Issue
1313 return Issue
1313 end
1314 end
1314 case scope
1315 case scope
1315 when 'all', 'system'
1316 when 'all', 'system'
1316 Issue
1317 Issue
1317 when 'tree'
1318 when 'tree'
1318 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1319 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1319 :lft => project.root.lft, :rgt => project.root.rgt)
1320 :lft => project.root.lft, :rgt => project.root.rgt)
1320 when 'hierarchy'
1321 when 'hierarchy'
1321 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)",
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)",
1322 :lft => project.lft, :rgt => project.rgt)
1323 :lft => project.lft, :rgt => project.rgt)
1323 when 'descendants'
1324 when 'descendants'
1324 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1325 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1325 :lft => project.lft, :rgt => project.rgt)
1326 :lft => project.lft, :rgt => project.rgt)
1326 else
1327 else
1327 Issue.where(:project_id => project.id)
1328 Issue.where(:project_id => project.id)
1328 end
1329 end
1329 end
1330 end
1330
1331
1331 def self.by_tracker(project)
1332 def self.by_tracker(project)
1332 count_and_group_by(:project => project, :association => :tracker)
1333 count_and_group_by(:project => project, :association => :tracker)
1333 end
1334 end
1334
1335
1335 def self.by_version(project)
1336 def self.by_version(project)
1336 count_and_group_by(:project => project, :association => :fixed_version)
1337 count_and_group_by(:project => project, :association => :fixed_version)
1337 end
1338 end
1338
1339
1339 def self.by_priority(project)
1340 def self.by_priority(project)
1340 count_and_group_by(:project => project, :association => :priority)
1341 count_and_group_by(:project => project, :association => :priority)
1341 end
1342 end
1342
1343
1343 def self.by_category(project)
1344 def self.by_category(project)
1344 count_and_group_by(:project => project, :association => :category)
1345 count_and_group_by(:project => project, :association => :category)
1345 end
1346 end
1346
1347
1347 def self.by_assigned_to(project)
1348 def self.by_assigned_to(project)
1348 count_and_group_by(:project => project, :association => :assigned_to)
1349 count_and_group_by(:project => project, :association => :assigned_to)
1349 end
1350 end
1350
1351
1351 def self.by_author(project)
1352 def self.by_author(project)
1352 count_and_group_by(:project => project, :association => :author)
1353 count_and_group_by(:project => project, :association => :author)
1353 end
1354 end
1354
1355
1355 def self.by_subproject(project)
1356 def self.by_subproject(project)
1356 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1357 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1357 r.reject {|r| r["project_id"] == project.id.to_s}
1358 r.reject {|r| r["project_id"] == project.id.to_s}
1358 end
1359 end
1359
1360
1360 # Query generator for selecting groups of issue counts for a project
1361 # Query generator for selecting groups of issue counts for a project
1361 # based on specific criteria
1362 # based on specific criteria
1362 #
1363 #
1363 # Options
1364 # Options
1364 # * project - Project to search in.
1365 # * project - Project to search in.
1365 # * with_subprojects - Includes subprojects issues if set to true.
1366 # * with_subprojects - Includes subprojects issues if set to true.
1366 # * association - Symbol. Association for grouping.
1367 # * association - Symbol. Association for grouping.
1367 def self.count_and_group_by(options)
1368 def self.count_and_group_by(options)
1368 assoc = reflect_on_association(options[:association])
1369 assoc = reflect_on_association(options[:association])
1369 select_field = assoc.foreign_key
1370 select_field = assoc.foreign_key
1370
1371
1371 Issue.
1372 Issue.
1372 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1373 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1373 joins(:status, assoc.name).
1374 joins(:status, assoc.name).
1374 group(:status_id, :is_closed, select_field).
1375 group(:status_id, :is_closed, select_field).
1375 count.
1376 count.
1376 map do |columns, total|
1377 map do |columns, total|
1377 status_id, is_closed, field_value = columns
1378 status_id, is_closed, field_value = columns
1378 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1379 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1379 {
1380 {
1380 "status_id" => status_id.to_s,
1381 "status_id" => status_id.to_s,
1381 "closed" => is_closed,
1382 "closed" => is_closed,
1382 select_field => field_value.to_s,
1383 select_field => field_value.to_s,
1383 "total" => total.to_s
1384 "total" => total.to_s
1384 }
1385 }
1385 end
1386 end
1386 end
1387 end
1387
1388
1388 # Returns a scope of projects that user can assign the issue to
1389 # Returns a scope of projects that user can assign the issue to
1389 def allowed_target_projects(user=User.current)
1390 def allowed_target_projects(user=User.current)
1390 current_project = new_record? ? nil : project
1391 current_project = new_record? ? nil : project
1391 self.class.allowed_target_projects(user, current_project)
1392 self.class.allowed_target_projects(user, current_project)
1392 end
1393 end
1393
1394
1394 # Returns a scope of projects that user can assign issues to
1395 # Returns a scope of projects that user can assign issues to
1395 # If current_project is given, it will be included in the scope
1396 # If current_project is given, it will be included in the scope
1396 def self.allowed_target_projects(user=User.current, current_project=nil)
1397 def self.allowed_target_projects(user=User.current, current_project=nil)
1397 condition = Project.allowed_to_condition(user, :add_issues)
1398 condition = Project.allowed_to_condition(user, :add_issues)
1398 if current_project
1399 if current_project
1399 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1400 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1400 end
1401 end
1401 Project.where(condition).having_trackers
1402 Project.where(condition).having_trackers
1402 end
1403 end
1403
1404
1404 # Returns a scope of trackers that user can assign the issue to
1405 # Returns a scope of trackers that user can assign the issue to
1405 def allowed_target_trackers(user=User.current)
1406 def allowed_target_trackers(user=User.current)
1406 self.class.allowed_target_trackers(project, user, tracker_id_was)
1407 self.class.allowed_target_trackers(project, user, tracker_id_was)
1407 end
1408 end
1408
1409
1409 # Returns a scope of trackers that user can assign project issues to
1410 # Returns a scope of trackers that user can assign project issues to
1410 def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
1411 def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
1411 if project
1412 if project
1412 scope = project.trackers.sorted
1413 scope = project.trackers.sorted
1413 unless user.admin?
1414 unless user.admin?
1414 roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
1415 roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
1415 unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
1416 unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
1416 tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
1417 tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
1417 if current_tracker
1418 if current_tracker
1418 tracker_ids << current_tracker
1419 tracker_ids << current_tracker
1419 end
1420 end
1420 scope = scope.where(:id => tracker_ids)
1421 scope = scope.where(:id => tracker_ids)
1421 end
1422 end
1422 end
1423 end
1423 scope
1424 scope
1424 else
1425 else
1425 Tracker.none
1426 Tracker.none
1426 end
1427 end
1427 end
1428 end
1428
1429
1429 private
1430 private
1430
1431
1431 def user_tracker_permission?(user, permission)
1432 def user_tracker_permission?(user, permission)
1432 if user.admin?
1433 if user.admin?
1433 true
1434 true
1434 else
1435 else
1435 roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
1436 roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
1436 roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)}
1437 roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)}
1437 end
1438 end
1438 end
1439 end
1439
1440
1440 def after_project_change
1441 def after_project_change
1441 # Update project_id on related time entries
1442 # Update project_id on related time entries
1442 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1443 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1443
1444
1444 # Delete issue relations
1445 # Delete issue relations
1445 unless Setting.cross_project_issue_relations?
1446 unless Setting.cross_project_issue_relations?
1446 relations_from.clear
1447 relations_from.clear
1447 relations_to.clear
1448 relations_to.clear
1448 end
1449 end
1449
1450
1450 # Move subtasks that were in the same project
1451 # Move subtasks that were in the same project
1451 children.each do |child|
1452 children.each do |child|
1452 next unless child.project_id == project_id_was
1453 next unless child.project_id == project_id_was
1453 # Change project and keep project
1454 # Change project and keep project
1454 child.send :project=, project, true
1455 child.send :project=, project, true
1455 unless child.save
1456 unless child.save
1456 raise ActiveRecord::Rollback
1457 raise ActiveRecord::Rollback
1457 end
1458 end
1458 end
1459 end
1459 end
1460 end
1460
1461
1461 # Callback for after the creation of an issue by copy
1462 # Callback for after the creation of an issue by copy
1462 # * adds a "copied to" relation with the copied issue
1463 # * adds a "copied to" relation with the copied issue
1463 # * copies subtasks from the copied issue
1464 # * copies subtasks from the copied issue
1464 def after_create_from_copy
1465 def after_create_from_copy
1465 return unless copy? && !@after_create_from_copy_handled
1466 return unless copy? && !@after_create_from_copy_handled
1466
1467
1467 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1468 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1468 if @current_journal
1469 if @current_journal
1469 @copied_from.init_journal(@current_journal.user)
1470 @copied_from.init_journal(@current_journal.user)
1470 end
1471 end
1471 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1472 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1472 unless relation.save
1473 unless relation.save
1473 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
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
1474 end
1475 end
1475 end
1476 end
1476
1477
1477 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1478 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1478 copy_options = (@copy_options || {}).merge(:subtasks => false)
1479 copy_options = (@copy_options || {}).merge(:subtasks => false)
1479 copied_issue_ids = {@copied_from.id => self.id}
1480 copied_issue_ids = {@copied_from.id => self.id}
1480 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1481 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1481 # Do not copy self when copying an issue as a descendant of the copied issue
1482 # Do not copy self when copying an issue as a descendant of the copied issue
1482 next if child == self
1483 next if child == self
1483 # Do not copy subtasks of issues that were not copied
1484 # Do not copy subtasks of issues that were not copied
1484 next unless copied_issue_ids[child.parent_id]
1485 next unless copied_issue_ids[child.parent_id]
1485 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1486 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1486 unless child.visible?
1487 unless child.visible?
1487 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
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
1488 next
1489 next
1489 end
1490 end
1490 copy = Issue.new.copy_from(child, copy_options)
1491 copy = Issue.new.copy_from(child, copy_options)
1491 if @current_journal
1492 if @current_journal
1492 copy.init_journal(@current_journal.user)
1493 copy.init_journal(@current_journal.user)
1493 end
1494 end
1494 copy.author = author
1495 copy.author = author
1495 copy.project = project
1496 copy.project = project
1496 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1497 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1497 unless copy.save
1498 unless copy.save
1498 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
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
1499 next
1500 next
1500 end
1501 end
1501 copied_issue_ids[child.id] = copy.id
1502 copied_issue_ids[child.id] = copy.id
1502 end
1503 end
1503 end
1504 end
1504 @after_create_from_copy_handled = true
1505 @after_create_from_copy_handled = true
1505 end
1506 end
1506
1507
1507 def update_nested_set_attributes
1508 def update_nested_set_attributes
1508 if parent_id_changed?
1509 if parent_id_changed?
1509 update_nested_set_attributes_on_parent_change
1510 update_nested_set_attributes_on_parent_change
1510 end
1511 end
1511 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1512 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1512 end
1513 end
1513
1514
1514 # Updates the nested set for when an existing issue is moved
1515 # Updates the nested set for when an existing issue is moved
1515 def update_nested_set_attributes_on_parent_change
1516 def update_nested_set_attributes_on_parent_change
1516 former_parent_id = parent_id_was
1517 former_parent_id = parent_id_was
1517 # delete invalid relations of all descendants
1518 # delete invalid relations of all descendants
1518 self_and_descendants.each do |issue|
1519 self_and_descendants.each do |issue|
1519 issue.relations.each do |relation|
1520 issue.relations.each do |relation|
1520 relation.destroy unless relation.valid?
1521 relation.destroy unless relation.valid?
1521 end
1522 end
1522 end
1523 end
1523 # update former parent
1524 # update former parent
1524 recalculate_attributes_for(former_parent_id) if former_parent_id
1525 recalculate_attributes_for(former_parent_id) if former_parent_id
1525 end
1526 end
1526
1527
1527 def update_parent_attributes
1528 def update_parent_attributes
1528 if parent_id
1529 if parent_id
1529 recalculate_attributes_for(parent_id)
1530 recalculate_attributes_for(parent_id)
1530 association(:parent).reset
1531 association(:parent).reset
1531 end
1532 end
1532 end
1533 end
1533
1534
1534 def recalculate_attributes_for(issue_id)
1535 def recalculate_attributes_for(issue_id)
1535 if issue_id && p = Issue.find_by_id(issue_id)
1536 if issue_id && p = Issue.find_by_id(issue_id)
1536 if p.priority_derived?
1537 if p.priority_derived?
1537 # priority = highest priority of open children
1538 # priority = highest priority of open children
1538 if priority_position = p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1539 if priority_position = p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1539 p.priority = IssuePriority.find_by_position(priority_position)
1540 p.priority = IssuePriority.find_by_position(priority_position)
1540 else
1541 else
1541 p.priority = IssuePriority.default
1542 p.priority = IssuePriority.default
1542 end
1543 end
1543 end
1544 end
1544
1545
1545 if p.dates_derived?
1546 if p.dates_derived?
1546 # start/due dates = lowest/highest dates of children
1547 # start/due dates = lowest/highest dates of children
1547 p.start_date = p.children.minimum(:start_date)
1548 p.start_date = p.children.minimum(:start_date)
1548 p.due_date = p.children.maximum(:due_date)
1549 p.due_date = p.children.maximum(:due_date)
1549 if p.start_date && p.due_date && p.due_date < p.start_date
1550 if p.start_date && p.due_date && p.due_date < p.start_date
1550 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
1551 end
1552 end
1552 end
1553 end
1553
1554
1554 if p.done_ratio_derived?
1555 if p.done_ratio_derived?
1555 # done ratio = weighted average ratio of leaves
1556 # done ratio = weighted average ratio of leaves
1556 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1557 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1557 child_count = p.children.count
1558 child_count = p.children.count
1558 if child_count > 0
1559 if child_count > 0
1559 average = p.children.where("estimated_hours > 0").average(:estimated_hours).to_f
1560 average = p.children.where("estimated_hours > 0").average(:estimated_hours).to_f
1560 if average == 0
1561 if average == 0
1561 average = 1
1562 average = 1
1562 end
1563 end
1563 done = p.children.joins(:status).
1564 done = p.children.joins(:status).
1564 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1565 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1565 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1566 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1566 progress = done / (average * child_count)
1567 progress = done / (average * child_count)
1567 p.done_ratio = progress.round
1568 p.done_ratio = progress.round
1568 end
1569 end
1569 end
1570 end
1570 end
1571 end
1571
1572
1572 # ancestors will be recursively updated
1573 # ancestors will be recursively updated
1573 p.save(:validate => false)
1574 p.save(:validate => false)
1574 end
1575 end
1575 end
1576 end
1576
1577
1577 # Update issues so their versions are not pointing to a
1578 # Update issues so their versions are not pointing to a
1578 # fixed_version that is not shared with the issue's project
1579 # fixed_version that is not shared with the issue's project
1579 def self.update_versions(conditions=nil)
1580 def self.update_versions(conditions=nil)
1580 # Only need to update issues with a fixed_version from
1581 # Only need to update issues with a fixed_version from
1581 # a different project and that is not systemwide shared
1582 # a different project and that is not systemwide shared
1582 Issue.joins(:project, :fixed_version).
1583 Issue.joins(:project, :fixed_version).
1583 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1584 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1584 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1585 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1585 " AND #{Version.table_name}.sharing <> 'system'").
1586 " AND #{Version.table_name}.sharing <> 'system'").
1586 where(conditions).each do |issue|
1587 where(conditions).each do |issue|
1587 next if issue.project.nil? || issue.fixed_version.nil?
1588 next if issue.project.nil? || issue.fixed_version.nil?
1588 unless issue.project.shared_versions.include?(issue.fixed_version)
1589 unless issue.project.shared_versions.include?(issue.fixed_version)
1589 issue.init_journal(User.current)
1590 issue.init_journal(User.current)
1590 issue.fixed_version = nil
1591 issue.fixed_version = nil
1591 issue.save
1592 issue.save
1592 end
1593 end
1593 end
1594 end
1594 end
1595 end
1595
1596
1596 def delete_selected_attachments
1597 def delete_selected_attachments
1597 if deleted_attachment_ids.present?
1598 if deleted_attachment_ids.present?
1598 objects = attachments.where(:id => deleted_attachment_ids.map(&:to_i))
1599 objects = attachments.where(:id => deleted_attachment_ids.map(&:to_i))
1599 attachments.delete(objects)
1600 attachments.delete(objects)
1600 end
1601 end
1601 end
1602 end
1602
1603
1603 # Callback on file attachment
1604 # Callback on file attachment
1604 def attachment_added(attachment)
1605 def attachment_added(attachment)
1605 if current_journal && !attachment.new_record?
1606 if current_journal && !attachment.new_record?
1606 current_journal.journalize_attachment(attachment, :added)
1607 current_journal.journalize_attachment(attachment, :added)
1607 end
1608 end
1608 end
1609 end
1609
1610
1610 # Callback on attachment deletion
1611 # Callback on attachment deletion
1611 def attachment_removed(attachment)
1612 def attachment_removed(attachment)
1612 if current_journal && !attachment.new_record?
1613 if current_journal && !attachment.new_record?
1613 current_journal.journalize_attachment(attachment, :removed)
1614 current_journal.journalize_attachment(attachment, :removed)
1614 current_journal.save
1615 current_journal.save
1615 end
1616 end
1616 end
1617 end
1617
1618
1618 # Called after a relation is added
1619 # Called after a relation is added
1619 def relation_added(relation)
1620 def relation_added(relation)
1620 if current_journal
1621 if current_journal
1621 current_journal.journalize_relation(relation, :added)
1622 current_journal.journalize_relation(relation, :added)
1622 current_journal.save
1623 current_journal.save
1623 end
1624 end
1624 end
1625 end
1625
1626
1626 # Called after a relation is removed
1627 # Called after a relation is removed
1627 def relation_removed(relation)
1628 def relation_removed(relation)
1628 if current_journal
1629 if current_journal
1629 current_journal.journalize_relation(relation, :removed)
1630 current_journal.journalize_relation(relation, :removed)
1630 current_journal.save
1631 current_journal.save
1631 end
1632 end
1632 end
1633 end
1633
1634
1634 # Default assignment based on category
1635 # Default assignment based on category
1635 def default_assign
1636 def default_assign
1636 if assigned_to.nil? && category && category.assigned_to
1637 if assigned_to.nil? && category && category.assigned_to
1637 self.assigned_to = category.assigned_to
1638 self.assigned_to = category.assigned_to
1638 end
1639 end
1639 end
1640 end
1640
1641
1641 # Updates start/due dates of following issues
1642 # Updates start/due dates of following issues
1642 def reschedule_following_issues
1643 def reschedule_following_issues
1643 if start_date_changed? || due_date_changed?
1644 if start_date_changed? || due_date_changed?
1644 relations_from.each do |relation|
1645 relations_from.each do |relation|
1645 relation.set_issue_to_dates
1646 relation.set_issue_to_dates
1646 end
1647 end
1647 end
1648 end
1648 end
1649 end
1649
1650
1650 # Closes duplicates if the issue is being closed
1651 # Closes duplicates if the issue is being closed
1651 def close_duplicates
1652 def close_duplicates
1652 if closing?
1653 if closing?
1653 duplicates.each do |duplicate|
1654 duplicates.each do |duplicate|
1654 # Reload is needed in case the duplicate was updated by a previous duplicate
1655 # Reload is needed in case the duplicate was updated by a previous duplicate
1655 duplicate.reload
1656 duplicate.reload
1656 # Don't re-close it if it's already closed
1657 # Don't re-close it if it's already closed
1657 next if duplicate.closed?
1658 next if duplicate.closed?
1658 # Same user and notes
1659 # Same user and notes
1659 if @current_journal
1660 if @current_journal
1660 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1661 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1661 duplicate.private_notes = @current_journal.private_notes
1662 duplicate.private_notes = @current_journal.private_notes
1662 end
1663 end
1663 duplicate.update_attribute :status, self.status
1664 duplicate.update_attribute :status, self.status
1664 end
1665 end
1665 end
1666 end
1666 end
1667 end
1667
1668
1668 # Make sure updated_on is updated when adding a note and set updated_on now
1669 # Make sure updated_on is updated when adding a note and set updated_on now
1669 # so we can set closed_on with the same value on closing
1670 # so we can set closed_on with the same value on closing
1670 def force_updated_on_change
1671 def force_updated_on_change
1671 if @current_journal || changed?
1672 if @current_journal || changed?
1672 self.updated_on = current_time_from_proper_timezone
1673 self.updated_on = current_time_from_proper_timezone
1673 if new_record?
1674 if new_record?
1674 self.created_on = updated_on
1675 self.created_on = updated_on
1675 end
1676 end
1676 end
1677 end
1677 end
1678 end
1678
1679
1679 # Callback for setting closed_on when the issue is closed.
1680 # Callback for setting closed_on when the issue is closed.
1680 # The closed_on attribute stores the time of the last closing
1681 # The closed_on attribute stores the time of the last closing
1681 # and is preserved when the issue is reopened.
1682 # and is preserved when the issue is reopened.
1682 def update_closed_on
1683 def update_closed_on
1683 if closing?
1684 if closing?
1684 self.closed_on = updated_on
1685 self.closed_on = updated_on
1685 end
1686 end
1686 end
1687 end
1687
1688
1688 # Saves the changes in a Journal
1689 # Saves the changes in a Journal
1689 # Called after_save
1690 # Called after_save
1690 def create_journal
1691 def create_journal
1691 if current_journal
1692 if current_journal
1692 current_journal.save
1693 current_journal.save
1693 end
1694 end
1694 end
1695 end
1695
1696
1696 def send_notification
1697 def send_notification
1697 if notify? && Setting.notified_events.include?('issue_added')
1698 if notify? && Setting.notified_events.include?('issue_added')
1698 Mailer.deliver_issue_add(self)
1699 Mailer.deliver_issue_add(self)
1699 end
1700 end
1700 end
1701 end
1701
1702
1702 # Stores the previous assignee so we can still have access
1703 # Stores the previous assignee so we can still have access
1703 # to it during after_save callbacks (assigned_to_id_was is reset)
1704 # to it during after_save callbacks (assigned_to_id_was is reset)
1704 def set_assigned_to_was
1705 def set_assigned_to_was
1705 @previous_assigned_to_id = assigned_to_id_was
1706 @previous_assigned_to_id = assigned_to_id_was
1706 end
1707 end
1707
1708
1708 # Clears the previous assignee at the end of after_save callbacks
1709 # Clears the previous assignee at the end of after_save callbacks
1709 def clear_assigned_to_was
1710 def clear_assigned_to_was
1710 @assigned_to_was = nil
1711 @assigned_to_was = nil
1711 @previous_assigned_to_id = nil
1712 @previous_assigned_to_id = nil
1712 end
1713 end
1713
1714
1714 def clear_disabled_fields
1715 def clear_disabled_fields
1715 if tracker
1716 if tracker
1716 tracker.disabled_core_fields.each do |attribute|
1717 tracker.disabled_core_fields.each do |attribute|
1717 send "#{attribute}=", nil
1718 send "#{attribute}=", nil
1718 end
1719 end
1719 self.done_ratio ||= 0
1720 self.done_ratio ||= 0
1720 end
1721 end
1721 end
1722 end
1722 end
1723 end
General Comments 0
You need to be logged in to leave comments. Login now