##// END OF EJS Templates
Fixed that subtasks lose their custom fields when copying an issue to a different project (#22342)....
Jean-Philippe Lang -
r14936:bb2c6f607610
parent child
Show More

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

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