##// END OF EJS Templates
Merged r15318 and r15319 (#22342)....
Jean-Philippe Lang -
r14947:3d4d6c31f3b2
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

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