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