Mercurial > hg > soundsoftware-site
comparison app/models/issue.rb @ 514:7eba09d624db live
Merge
author | Chris Cannam |
---|---|
date | Thu, 14 Jul 2011 10:50:53 +0100 |
parents | 851510f1b535 |
children | 5e80956cc792 |
comparison
equal
deleted
inserted
replaced
512:b9aebdd7dd40 | 514:7eba09d624db |
---|---|
1 # redMine - project management software | 1 # Redmine - project management software |
2 # Copyright (C) 2006-2007 Jean-Philippe Lang | 2 # Copyright (C) 2006-2011 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 | |
20 | |
19 belongs_to :project | 21 belongs_to :project |
20 belongs_to :tracker | 22 belongs_to :tracker |
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' | 23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' |
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' | 24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' |
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id' | 25 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id' |
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id' | 27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id' |
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' | 28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' |
27 | 29 |
28 has_many :journals, :as => :journalized, :dependent => :destroy | 30 has_many :journals, :as => :journalized, :dependent => :destroy |
29 has_many :time_entries, :dependent => :delete_all | 31 has_many :time_entries, :dependent => :delete_all |
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.id ASC" | 32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" |
31 | 33 |
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all | 34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all |
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all | 35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all |
34 | 36 |
35 acts_as_nested_set :scope => 'root_id' | 37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy |
36 acts_as_attachable :after_remove => :attachment_removed | 38 acts_as_attachable :after_remove => :attachment_removed |
37 acts_as_customizable | 39 acts_as_customizable |
38 acts_as_watchable | 40 acts_as_watchable |
39 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], | 41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], |
40 :include => [:project, :journals], | 42 :include => [:project, :journals], |
41 # sort by id so that limited eager loading doesn't break with postgresql | 43 # sort by id so that limited eager loading doesn't break with postgresql |
42 :order_column => "#{table_name}.id" | 44 :order_column => "#{table_name}.id" |
43 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"}, | 45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"}, |
44 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}, | 46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}, |
45 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') } | 47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') } |
46 | 48 |
47 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}, | 49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}, |
48 :author_key => :author_id | 50 :author_key => :author_id |
49 | 51 |
50 DONE_RATIO_OPTIONS = %w(issue_field issue_status) | 52 DONE_RATIO_OPTIONS = %w(issue_field issue_status) |
51 | 53 |
56 validates_length_of :subject, :maximum => 255 | 58 validates_length_of :subject, :maximum => 255 |
57 validates_inclusion_of :done_ratio, :in => 0..100 | 59 validates_inclusion_of :done_ratio, :in => 0..100 |
58 validates_numericality_of :estimated_hours, :allow_nil => true | 60 validates_numericality_of :estimated_hours, :allow_nil => true |
59 | 61 |
60 named_scope :visible, lambda {|*args| { :include => :project, | 62 named_scope :visible, lambda {|*args| { :include => :project, |
61 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } } | 63 :conditions => Issue.visible_condition(args.shift || User.current, *args) } } |
62 | 64 |
63 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status | 65 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status |
64 | 66 |
65 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC" | 67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC" |
66 named_scope :with_limit, lambda { |limit| { :limit => limit} } | 68 named_scope :with_limit, lambda { |limit| { :limit => limit} } |
67 named_scope :on_active_project, :include => [:status, :project, :tracker], | 69 named_scope :on_active_project, :include => [:status, :project, :tracker], |
68 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] | 70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] |
69 named_scope :for_gantt, lambda { | |
70 { | |
71 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version], | |
72 :order => "#{Issue.table_name}.due_date ASC, #{Issue.table_name}.start_date ASC, #{Issue.table_name}.id ASC" | |
73 } | |
74 } | |
75 | 71 |
76 named_scope :without_version, lambda { | 72 named_scope :without_version, lambda { |
77 { | 73 { |
78 :conditions => { :fixed_version_id => nil} | 74 :conditions => { :fixed_version_id => nil} |
79 } | 75 } |
86 } | 82 } |
87 | 83 |
88 before_create :default_assign | 84 before_create :default_assign |
89 before_save :close_duplicates, :update_done_ratio_from_issue_status | 85 before_save :close_duplicates, :update_done_ratio_from_issue_status |
90 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal | 86 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal |
91 after_destroy :destroy_children | |
92 after_destroy :update_parent_attributes | 87 after_destroy :update_parent_attributes |
88 | |
89 # Returns a SQL conditions string used to find all issues visible by the specified user | |
90 def self.visible_condition(user, options={}) | |
91 Project.allowed_to_condition(user, :view_issues, options) do |role, user| | |
92 case role.issues_visibility | |
93 when 'all' | |
94 nil | |
95 when 'default' | |
96 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})" | |
97 when 'own' | |
98 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})" | |
99 else | |
100 '1=0' | |
101 end | |
102 end | |
103 end | |
93 | 104 |
94 # Returns true if usr or current user is allowed to view the issue | 105 # Returns true if usr or current user is allowed to view the issue |
95 def visible?(usr=nil) | 106 def visible?(usr=nil) |
96 (usr || User.current).allowed_to?(:view_issues, self.project) | 107 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user| |
97 end | 108 case role.issues_visibility |
98 | 109 when 'all' |
110 true | |
111 when 'default' | |
112 !self.is_private? || self.author == user || self.assigned_to == user | |
113 when 'own' | |
114 self.author == user || self.assigned_to == user | |
115 else | |
116 false | |
117 end | |
118 end | |
119 end | |
120 | |
99 def after_initialize | 121 def after_initialize |
100 if new_record? | 122 if new_record? |
101 # set default values for new records only | 123 # set default values for new records only |
102 self.status ||= IssueStatus.default | 124 self.status ||= IssueStatus.default |
103 self.priority ||= IssuePriority.default | 125 self.priority ||= IssuePriority.default |
104 end | 126 end |
105 end | 127 end |
106 | 128 |
107 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields | 129 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields |
108 def available_custom_fields | 130 def available_custom_fields |
109 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : [] | 131 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : [] |
110 end | 132 end |
111 | 133 |
112 def copy_from(arg) | 134 def copy_from(arg) |
113 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg) | 135 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg) |
114 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on") | 136 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on") |
115 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} | 137 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} |
116 self.status = issue.status | 138 self.status = issue.status |
117 self | 139 self |
118 end | 140 end |
119 | 141 |
120 # Moves/copies an issue to a new project and tracker | 142 # Moves/copies an issue to a new project and tracker |
121 # Returns the moved/copied issue on success, false on failure | 143 # Returns the moved/copied issue on success, false on failure |
122 def move_to_project(*args) | 144 def move_to_project(*args) |
123 ret = Issue.transaction do | 145 ret = Issue.transaction do |
124 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback) | 146 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback) |
125 end || false | 147 end || false |
126 end | 148 end |
127 | 149 |
128 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {}) | 150 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {}) |
129 options ||= {} | 151 options ||= {} |
130 issue = options[:copy] ? self.class.new.copy_from(self) : self | 152 issue = options[:copy] ? self.class.new.copy_from(self) : self |
131 | 153 |
132 if new_project && issue.project_id != new_project.id | 154 if new_project && issue.project_id != new_project.id |
133 # delete issue relations | 155 # delete issue relations |
134 unless Setting.cross_project_issue_relations? | 156 unless Setting.cross_project_issue_relations? |
135 issue.relations_from.clear | 157 issue.relations_from.clear |
136 issue.relations_to.clear | 158 issue.relations_to.clear |
151 if new_tracker | 173 if new_tracker |
152 issue.tracker = new_tracker | 174 issue.tracker = new_tracker |
153 issue.reset_custom_values! | 175 issue.reset_custom_values! |
154 end | 176 end |
155 if options[:copy] | 177 if options[:copy] |
178 issue.author = User.current | |
156 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} | 179 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} |
157 issue.status = if options[:attributes] && options[:attributes][:status_id] | 180 issue.status = if options[:attributes] && options[:attributes][:status_id] |
158 IssueStatus.find_by_id(options[:attributes][:status_id]) | 181 IssueStatus.find_by_id(options[:attributes][:status_id]) |
159 else | 182 else |
160 self.status | 183 self.status |
163 # Allow bulk setting of attributes on the issue | 186 # Allow bulk setting of attributes on the issue |
164 if options[:attributes] | 187 if options[:attributes] |
165 issue.attributes = options[:attributes] | 188 issue.attributes = options[:attributes] |
166 end | 189 end |
167 if issue.save | 190 if issue.save |
168 unless options[:copy] | 191 if options[:copy] |
192 if current_journal && current_journal.notes.present? | |
193 issue.init_journal(current_journal.user, current_journal.notes) | |
194 issue.current_journal.notify = false | |
195 issue.save | |
196 end | |
197 else | |
169 # Manually update project_id on related time entries | 198 # Manually update project_id on related time entries |
170 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id}) | 199 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id}) |
171 | 200 |
172 issue.children.each do |child| | 201 issue.children.each do |child| |
173 unless child.move_to_project_without_transaction(new_project) | 202 unless child.move_to_project_without_transaction(new_project) |
174 # Move failed and transaction was rollback'd | 203 # Move failed and transaction was rollback'd |
175 return false | 204 return false |
176 end | 205 end |
184 | 213 |
185 def status_id=(sid) | 214 def status_id=(sid) |
186 self.status = nil | 215 self.status = nil |
187 write_attribute(:status_id, sid) | 216 write_attribute(:status_id, sid) |
188 end | 217 end |
189 | 218 |
190 def priority_id=(pid) | 219 def priority_id=(pid) |
191 self.priority = nil | 220 self.priority = nil |
192 write_attribute(:priority_id, pid) | 221 write_attribute(:priority_id, pid) |
193 end | 222 end |
194 | 223 |
197 result = write_attribute(:tracker_id, tid) | 226 result = write_attribute(:tracker_id, tid) |
198 @custom_field_values = nil | 227 @custom_field_values = nil |
199 result | 228 result |
200 end | 229 end |
201 | 230 |
231 def description=(arg) | |
232 if arg.is_a?(String) | |
233 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n") | |
234 end | |
235 write_attribute(:description, arg) | |
236 end | |
237 | |
202 # Overrides attributes= so that tracker_id gets assigned first | 238 # Overrides attributes= so that tracker_id gets assigned first |
203 def attributes_with_tracker_first=(new_attributes, *args) | 239 def attributes_with_tracker_first=(new_attributes, *args) |
204 return if new_attributes.nil? | 240 return if new_attributes.nil? |
205 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id] | 241 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id] |
206 if new_tracker_id | 242 if new_tracker_id |
208 end | 244 end |
209 send :attributes_without_tracker_first=, new_attributes, *args | 245 send :attributes_without_tracker_first=, new_attributes, *args |
210 end | 246 end |
211 # Do not redefine alias chain on reload (see #4838) | 247 # Do not redefine alias chain on reload (see #4838) |
212 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=) | 248 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=) |
213 | 249 |
214 def estimated_hours=(h) | 250 def estimated_hours=(h) |
215 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) | 251 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) |
216 end | 252 end |
217 | 253 |
218 SAFE_ATTRIBUTES = %w( | 254 safe_attributes 'tracker_id', |
219 tracker_id | 255 'status_id', |
220 status_id | 256 'parent_issue_id', |
221 parent_issue_id | 257 'category_id', |
222 category_id | 258 'assigned_to_id', |
223 assigned_to_id | 259 'priority_id', |
224 priority_id | 260 'fixed_version_id', |
225 fixed_version_id | 261 'subject', |
226 subject | 262 'description', |
227 description | 263 'start_date', |
228 start_date | 264 'due_date', |
229 due_date | 265 'done_ratio', |
230 done_ratio | 266 'estimated_hours', |
231 estimated_hours | 267 'custom_field_values', |
232 custom_field_values | 268 'custom_fields', |
233 lock_version | 269 'lock_version', |
234 ) unless const_defined?(:SAFE_ATTRIBUTES) | 270 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) } |
235 | 271 |
236 SAFE_ATTRIBUTES_ON_TRANSITION = %w( | 272 safe_attributes 'status_id', |
237 status_id | 273 'assigned_to_id', |
238 assigned_to_id | 274 'fixed_version_id', |
239 fixed_version_id | 275 'done_ratio', |
240 done_ratio | 276 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? } |
241 ) unless const_defined?(:SAFE_ATTRIBUTES_ON_TRANSITION) | 277 |
278 safe_attributes 'is_private', | |
279 :if => lambda {|issue, user| | |
280 user.allowed_to?(:set_issues_private, issue.project) || | |
281 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project)) | |
282 } | |
242 | 283 |
243 # Safely sets attributes | 284 # Safely sets attributes |
244 # Should be called from controllers instead of #attributes= | 285 # Should be called from controllers instead of #attributes= |
245 # attr_accessible is too rough because we still want things like | 286 # attr_accessible is too rough because we still want things like |
246 # Issue.new(:project => foo) to work | 287 # Issue.new(:project => foo) to work |
247 # TODO: move workflow/permission checks from controllers to here | 288 # TODO: move workflow/permission checks from controllers to here |
248 def safe_attributes=(attrs, user=User.current) | 289 def safe_attributes=(attrs, user=User.current) |
249 return unless attrs.is_a?(Hash) | 290 return unless attrs.is_a?(Hash) |
250 | 291 |
251 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed | 292 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed |
252 if new_record? || user.allowed_to?(:edit_issues, project) | 293 attrs = delete_unsafe_attributes(attrs, user) |
253 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)} | 294 return if attrs.empty? |
254 elsif new_statuses_allowed_to(user).any? | 295 |
255 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES_ON_TRANSITION.include?(k)} | |
256 else | |
257 return | |
258 end | |
259 | |
260 # Tracker must be set before since new_statuses_allowed_to depends on it. | 296 # Tracker must be set before since new_statuses_allowed_to depends on it. |
261 if t = attrs.delete('tracker_id') | 297 if t = attrs.delete('tracker_id') |
262 self.tracker_id = t | 298 self.tracker_id = t |
263 end | 299 end |
264 | 300 |
265 if attrs['status_id'] | 301 if attrs['status_id'] |
266 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i) | 302 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i) |
267 attrs.delete('status_id') | 303 attrs.delete('status_id') |
268 end | 304 end |
269 end | 305 end |
270 | 306 |
271 unless leaf? | 307 unless leaf? |
272 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)} | 308 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)} |
273 end | 309 end |
274 | 310 |
275 if attrs.has_key?('parent_issue_id') | 311 if attrs.has_key?('parent_issue_id') |
276 if !user.allowed_to?(:manage_subtasks, project) | 312 if !user.allowed_to?(:manage_subtasks, project) |
277 attrs.delete('parent_issue_id') | 313 attrs.delete('parent_issue_id') |
278 elsif !attrs['parent_issue_id'].blank? | 314 elsif !attrs['parent_issue_id'].blank? |
279 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id']) | 315 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i) |
280 end | 316 end |
281 end | 317 end |
282 | 318 |
283 self.attributes = attrs | 319 self.attributes = attrs |
284 end | 320 end |
285 | 321 |
286 def done_ratio | 322 def done_ratio |
287 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio | 323 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio |
288 status.default_done_ratio | 324 status.default_done_ratio |
289 else | 325 else |
290 read_attribute(:done_ratio) | 326 read_attribute(:done_ratio) |
296 end | 332 end |
297 | 333 |
298 def self.use_field_for_done_ratio? | 334 def self.use_field_for_done_ratio? |
299 Setting.issue_done_ratio == 'issue_field' | 335 Setting.issue_done_ratio == 'issue_field' |
300 end | 336 end |
301 | 337 |
302 def validate | 338 def validate |
303 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? | 339 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? |
304 errors.add :due_date, :not_a_date | 340 errors.add :due_date, :not_a_date |
305 end | 341 end |
306 | 342 |
307 if self.due_date and self.start_date and self.due_date < self.start_date | 343 if self.due_date and self.start_date and self.due_date < self.start_date |
308 errors.add :due_date, :greater_than_start_date | 344 errors.add :due_date, :greater_than_start_date |
309 end | 345 end |
310 | 346 |
311 if start_date && soonest_start && start_date < soonest_start | 347 if start_date && soonest_start && start_date < soonest_start |
312 errors.add :start_date, :invalid | 348 errors.add :start_date, :invalid |
313 end | 349 end |
314 | 350 |
315 if fixed_version | 351 if fixed_version |
316 if !assignable_versions.include?(fixed_version) | 352 if !assignable_versions.include?(fixed_version) |
317 errors.add :fixed_version_id, :inclusion | 353 errors.add :fixed_version_id, :inclusion |
318 elsif reopened? && fixed_version.closed? | 354 elsif reopened? && fixed_version.closed? |
319 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version) | 355 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version) |
320 end | 356 end |
321 end | 357 end |
322 | 358 |
323 # Checks that the issue can not be added/moved to a disabled tracker | 359 # Checks that the issue can not be added/moved to a disabled tracker |
324 if project && (tracker_id_changed? || project_id_changed?) | 360 if project && (tracker_id_changed? || project_id_changed?) |
325 unless project.trackers.include?(tracker) | 361 unless project.trackers.include?(tracker) |
326 errors.add :tracker_id, :inclusion | 362 errors.add :tracker_id, :inclusion |
327 end | 363 end |
328 end | 364 end |
329 | 365 |
330 # Checks parent issue assignment | 366 # Checks parent issue assignment |
331 if @parent_issue | 367 if @parent_issue |
332 if @parent_issue.project_id != project_id | 368 if @parent_issue.project_id != project_id |
333 errors.add :parent_issue_id, :not_same_project | 369 errors.add :parent_issue_id, :not_same_project |
334 elsif !new_record? | 370 elsif !new_record? |
341 errors.add :parent_issue_id, :not_a_valid_parent | 377 errors.add :parent_issue_id, :not_a_valid_parent |
342 end | 378 end |
343 end | 379 end |
344 end | 380 end |
345 end | 381 end |
346 | 382 |
347 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios | 383 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios |
348 # even if the user turns off the setting later | 384 # even if the user turns off the setting later |
349 def update_done_ratio_from_issue_status | 385 def update_done_ratio_from_issue_status |
350 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio | 386 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio |
351 self.done_ratio = status.default_done_ratio | 387 self.done_ratio = status.default_done_ratio |
352 end | 388 end |
353 end | 389 end |
354 | 390 |
355 def init_journal(user, notes = "") | 391 def init_journal(user, notes = "") |
356 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) | 392 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) |
357 @issue_before_change = self.clone | 393 @issue_before_change = self.clone |
358 @issue_before_change.status = self.status | 394 @issue_before_change.status = self.status |
359 @custom_values_before_change = {} | 395 @custom_values_before_change = {} |
360 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } | 396 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } |
361 # Make sure updated_on is updated when adding a note. | 397 # Make sure updated_on is updated when adding a note. |
362 updated_on_will_change! | 398 updated_on_will_change! |
363 @current_journal | 399 @current_journal |
364 end | 400 end |
365 | 401 |
366 # Return true if the issue is closed, otherwise false | 402 # Return true if the issue is closed, otherwise false |
367 def closed? | 403 def closed? |
368 self.status.is_closed? | 404 self.status.is_closed? |
369 end | 405 end |
370 | 406 |
371 # Return true if the issue is being reopened | 407 # Return true if the issue is being reopened |
372 def reopened? | 408 def reopened? |
373 if !new_record? && status_id_changed? | 409 if !new_record? && status_id_changed? |
374 status_was = IssueStatus.find_by_id(status_id_was) | 410 status_was = IssueStatus.find_by_id(status_id_was) |
375 status_new = IssueStatus.find_by_id(status_id) | 411 status_new = IssueStatus.find_by_id(status_id) |
389 return true | 425 return true |
390 end | 426 end |
391 end | 427 end |
392 false | 428 false |
393 end | 429 end |
394 | 430 |
395 # Returns true if the issue is overdue | 431 # Returns true if the issue is overdue |
396 def overdue? | 432 def overdue? |
397 !due_date.nil? && (due_date < Date.today) && !status.is_closed? | 433 !due_date.nil? && (due_date < Date.today) && !status.is_closed? |
398 end | 434 end |
399 | 435 |
406 | 442 |
407 # Does this issue have children? | 443 # Does this issue have children? |
408 def children? | 444 def children? |
409 !leaf? | 445 !leaf? |
410 end | 446 end |
411 | 447 |
412 # Users the issue can be assigned to | 448 # Users the issue can be assigned to |
413 def assignable_users | 449 def assignable_users |
414 users = project.assignable_users | 450 users = project.assignable_users |
415 users << author if author | 451 users << author if author |
416 users.uniq.sort | 452 users.uniq.sort |
417 end | 453 end |
418 | 454 |
419 # Versions that the issue can be assigned to | 455 # Versions that the issue can be assigned to |
420 def assignable_versions | 456 def assignable_versions |
421 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort | 457 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort |
422 end | 458 end |
423 | 459 |
424 # Returns true if this issue is blocked by another issue that is still open | 460 # Returns true if this issue is blocked by another issue that is still open |
425 def blocked? | 461 def blocked? |
426 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil? | 462 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil? |
427 end | 463 end |
428 | 464 |
429 # Returns an array of status that user is able to apply | 465 # Returns an array of status that user is able to apply |
430 def new_statuses_allowed_to(user, include_default=false) | 466 def new_statuses_allowed_to(user, include_default=false) |
431 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker) | 467 statuses = status.find_new_statuses_allowed_to( |
468 user.roles_for_project(project), | |
469 tracker, | |
470 author == user, | |
471 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id | |
472 ) | |
432 statuses << status unless statuses.empty? | 473 statuses << status unless statuses.empty? |
433 statuses << IssueStatus.default if include_default | 474 statuses << IssueStatus.default if include_default |
434 statuses = statuses.uniq.sort | 475 statuses = statuses.uniq.sort |
435 blocked? ? statuses.reject {|s| s.is_closed?} : statuses | 476 blocked? ? statuses.reject {|s| s.is_closed?} : statuses |
436 end | 477 end |
437 | 478 |
438 # Returns the mail adresses of users that should be notified | 479 # Returns the mail adresses of users that should be notified |
439 def recipients | 480 def recipients |
440 notified = project.notified_users | 481 notified = project.notified_users |
441 # Author and assignee are always notified unless they have been | 482 # Author and assignee are always notified unless they have been |
442 # locked or don't want to be notified | 483 # locked or don't want to be notified |
445 notified.uniq! | 486 notified.uniq! |
446 # Remove users that can not view the issue | 487 # Remove users that can not view the issue |
447 notified.reject! {|user| !visible?(user)} | 488 notified.reject! {|user| !visible?(user)} |
448 notified.collect(&:mail) | 489 notified.collect(&:mail) |
449 end | 490 end |
450 | 491 |
451 # Returns the total number of hours spent on this issue and its descendants | 492 # Returns the total number of hours spent on this issue and its descendants |
452 # | 493 # |
453 # Example: | 494 # Example: |
454 # spent_hours => 0.0 | 495 # spent_hours => 0.0 |
455 # spent_hours => 50.2 | 496 # spent_hours => 50.2 |
456 def spent_hours | 497 def spent_hours |
457 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0 | 498 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0 |
458 end | 499 end |
459 | 500 |
460 def relations | 501 def relations |
461 (relations_from + relations_to).sort | 502 (relations_from + relations_to).sort |
462 end | 503 end |
463 | 504 |
464 def all_dependent_issues | 505 def all_dependent_issues(except=[]) |
506 except << self | |
465 dependencies = [] | 507 dependencies = [] |
466 relations_from.each do |relation| | 508 relations_from.each do |relation| |
467 dependencies << relation.issue_to | 509 if relation.issue_to && !except.include?(relation.issue_to) |
468 dependencies += relation.issue_to.all_dependent_issues | 510 dependencies << relation.issue_to |
511 dependencies += relation.issue_to.all_dependent_issues(except) | |
512 end | |
469 end | 513 end |
470 dependencies | 514 dependencies |
471 end | 515 end |
472 | 516 |
473 # Returns an array of issues that duplicate this one | 517 # Returns an array of issues that duplicate this one |
474 def duplicates | 518 def duplicates |
475 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from} | 519 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from} |
476 end | 520 end |
477 | 521 |
478 # Returns the due date or the target due date if any | 522 # Returns the due date or the target due date if any |
479 # Used on gantt chart | 523 # Used on gantt chart |
480 def due_before | 524 def due_before |
481 due_date || (fixed_version ? fixed_version.effective_date : nil) | 525 due_date || (fixed_version ? fixed_version.effective_date : nil) |
482 end | 526 end |
483 | 527 |
484 # Returns the time scheduled for this issue. | 528 # Returns the time scheduled for this issue. |
485 # | 529 # |
486 # Example: | 530 # Example: |
487 # Start Date: 2/26/09, End Date: 3/04/09 | 531 # Start Date: 2/26/09, End Date: 3/04/09 |
488 # duration => 6 | 532 # duration => 6 |
489 def duration | 533 def duration |
490 (start_date && due_date) ? due_date - start_date : 0 | 534 (start_date && due_date) ? due_date - start_date : 0 |
491 end | 535 end |
492 | 536 |
493 def soonest_start | 537 def soonest_start |
494 @soonest_start ||= ( | 538 @soonest_start ||= ( |
495 relations_to.collect{|relation| relation.successor_soonest_start} + | 539 relations_to.collect{|relation| relation.successor_soonest_start} + |
496 ancestors.collect(&:soonest_start) | 540 ancestors.collect(&:soonest_start) |
497 ).compact.max | 541 ).compact.max |
498 end | 542 end |
499 | 543 |
500 def reschedule_after(date) | 544 def reschedule_after(date) |
501 return if date.nil? | 545 return if date.nil? |
502 if leaf? | 546 if leaf? |
503 if start_date.nil? || start_date < date | 547 if start_date.nil? || start_date < date |
504 self.start_date, self.due_date = date, date + duration | 548 self.start_date, self.due_date = date, date + duration |
508 leaves.each do |leaf| | 552 leaves.each do |leaf| |
509 leaf.reschedule_after(date) | 553 leaf.reschedule_after(date) |
510 end | 554 end |
511 end | 555 end |
512 end | 556 end |
513 | 557 |
514 def <=>(issue) | 558 def <=>(issue) |
515 if issue.nil? | 559 if issue.nil? |
516 -1 | 560 -1 |
517 elsif root_id != issue.root_id | 561 elsif root_id != issue.root_id |
518 (root_id || 0) <=> (issue.root_id || 0) | 562 (root_id || 0) <=> (issue.root_id || 0) |
519 else | 563 else |
520 (lft || 0) <=> (issue.lft || 0) | 564 (lft || 0) <=> (issue.lft || 0) |
521 end | 565 end |
522 end | 566 end |
523 | 567 |
524 def to_s | 568 def to_s |
525 "#{tracker} ##{id}: #{subject}" | 569 "#{tracker} ##{id}: #{subject}" |
526 end | 570 end |
527 | 571 |
528 # Returns a string of css classes that apply to the issue | 572 # Returns a string of css classes that apply to the issue |
529 def css_classes | 573 def css_classes |
530 s = "issue status-#{status.position} " | 574 s = "issue status-#{status.position} " |
531 s << "priority-#{priority.position}" | 575 s << "priority-#{priority.position}" |
532 s << ' closed' if closed? | 576 s << ' closed' if closed? |
533 s << ' overdue' if overdue? | 577 s << ' overdue' if overdue? |
578 s << ' child' if child? | |
579 s << ' parent' unless leaf? | |
580 s << ' private' if is_private? | |
534 s << ' created-by-me' if User.current.logged? && author_id == User.current.id | 581 s << ' created-by-me' if User.current.logged? && author_id == User.current.id |
535 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id | 582 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id |
536 s | 583 s |
537 end | 584 end |
538 | 585 |
539 # Saves an issue, time_entry, attachments, and a journal from the parameters | 586 # Saves an issue, time_entry, attachments, and a journal from the parameters |
540 # Returns false if save fails | 587 # Returns false if save fails |
541 def save_issue_with_child_records(params, existing_time_entry=nil) | 588 def save_issue_with_child_records(params, existing_time_entry=nil) |
542 Issue.transaction do | 589 Issue.transaction do |
543 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project) | 590 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project) |
544 @time_entry = existing_time_entry || TimeEntry.new | 591 @time_entry = existing_time_entry || TimeEntry.new |
545 @time_entry.project = project | 592 @time_entry.project = project |
546 @time_entry.issue = self | 593 @time_entry.issue = self |
547 @time_entry.user = User.current | 594 @time_entry.user = User.current |
548 @time_entry.spent_on = Date.today | 595 @time_entry.spent_on = Date.today |
549 @time_entry.attributes = params[:time_entry] | 596 @time_entry.attributes = params[:time_entry] |
550 self.time_entries << @time_entry | 597 self.time_entries << @time_entry |
551 end | 598 end |
552 | 599 |
553 if valid? | 600 if valid? |
554 attachments = Attachment.attach_files(self, params[:attachments]) | 601 attachments = Attachment.attach_files(self, params[:attachments]) |
555 | 602 |
556 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} | 603 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} |
557 # TODO: Rename hook | 604 # TODO: Rename hook |
558 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) | 605 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) |
559 begin | 606 begin |
560 if save | 607 if save |
575 # Unassigns issues from +version+ if it's no longer shared with issue's project | 622 # Unassigns issues from +version+ if it's no longer shared with issue's project |
576 def self.update_versions_from_sharing_change(version) | 623 def self.update_versions_from_sharing_change(version) |
577 # Update issues assigned to the version | 624 # Update issues assigned to the version |
578 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id]) | 625 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id]) |
579 end | 626 end |
580 | 627 |
581 # Unassigns issues from versions that are no longer shared | 628 # Unassigns issues from versions that are no longer shared |
582 # after +project+ was moved | 629 # after +project+ was moved |
583 def self.update_versions_from_hierarchy_change(project) | 630 def self.update_versions_from_hierarchy_change(project) |
584 moved_project_ids = project.self_and_descendants.reload.collect(&:id) | 631 moved_project_ids = project.self_and_descendants.reload.collect(&:id) |
585 # Update issues of the moved projects and issues assigned to a version of a moved project | 632 # Update issues of the moved projects and issues assigned to a version of a moved project |
593 else | 640 else |
594 @parent_issue = nil | 641 @parent_issue = nil |
595 nil | 642 nil |
596 end | 643 end |
597 end | 644 end |
598 | 645 |
599 def parent_issue_id | 646 def parent_issue_id |
600 if instance_variable_defined? :@parent_issue | 647 if instance_variable_defined? :@parent_issue |
601 @parent_issue.nil? ? nil : @parent_issue.id | 648 @parent_issue.nil? ? nil : @parent_issue.id |
602 else | 649 else |
603 parent_id | 650 parent_id |
642 end | 689 end |
643 | 690 |
644 def self.by_subproject(project) | 691 def self.by_subproject(project) |
645 ActiveRecord::Base.connection.select_all("select s.id as status_id, | 692 ActiveRecord::Base.connection.select_all("select s.id as status_id, |
646 s.is_closed as closed, | 693 s.is_closed as closed, |
647 i.project_id as project_id, | 694 #{Issue.table_name}.project_id as project_id, |
648 count(i.id) as total | 695 count(#{Issue.table_name}.id) as total |
649 from | 696 from |
650 #{Issue.table_name} i, #{IssueStatus.table_name} s | 697 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s |
651 where | 698 where |
652 i.status_id=s.id | 699 #{Issue.table_name}.status_id=s.id |
653 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')}) | 700 and #{Issue.table_name}.project_id = #{Project.table_name}.id |
654 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any? | 701 and #{visible_condition(User.current, :project => project, :with_subprojects => true)} |
702 and #{Issue.table_name}.project_id <> #{project.id} | |
703 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any? | |
655 end | 704 end |
656 # End ReportsController extraction | 705 # End ReportsController extraction |
657 | 706 |
658 # Returns an array of projects that current user can move issues to | 707 # Returns an array of projects that current user can move issues to |
659 def self.allowed_target_projects_on_move | 708 def self.allowed_target_projects_on_move |
660 projects = [] | 709 projects = [] |
661 if User.current.admin? | 710 if User.current.admin? |
662 # admin is allowed to move issues to any active (visible) project | 711 # admin is allowed to move issues to any active (visible) project |
668 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}} | 717 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}} |
669 end | 718 end |
670 end | 719 end |
671 projects | 720 projects |
672 end | 721 end |
673 | 722 |
674 private | 723 private |
675 | 724 |
676 def update_nested_set_attributes | 725 def update_nested_set_attributes |
677 if root_id.nil? | 726 if root_id.nil? |
678 # issue was just created | 727 # issue was just created |
679 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) | 728 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) |
680 set_default_left_and_right | 729 set_default_left_and_right |
717 # update former parent | 766 # update former parent |
718 recalculate_attributes_for(former_parent_id) if former_parent_id | 767 recalculate_attributes_for(former_parent_id) if former_parent_id |
719 end | 768 end |
720 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue) | 769 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue) |
721 end | 770 end |
722 | 771 |
723 def update_parent_attributes | 772 def update_parent_attributes |
724 recalculate_attributes_for(parent_id) if parent_id | 773 recalculate_attributes_for(parent_id) if parent_id |
725 end | 774 end |
726 | 775 |
727 def recalculate_attributes_for(issue_id) | 776 def recalculate_attributes_for(issue_id) |
728 if issue_id && p = Issue.find_by_id(issue_id) | 777 if issue_id && p = Issue.find_by_id(issue_id) |
729 # priority = highest priority of children | 778 # priority = highest priority of children |
730 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority) | 779 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority) |
731 p.priority = IssuePriority.find_by_position(priority_position) | 780 p.priority = IssuePriority.find_by_position(priority_position) |
732 end | 781 end |
733 | 782 |
734 # start/due dates = lowest/highest dates of children | 783 # start/due dates = lowest/highest dates of children |
735 p.start_date = p.children.minimum(:start_date) | 784 p.start_date = p.children.minimum(:start_date) |
736 p.due_date = p.children.maximum(:due_date) | 785 p.due_date = p.children.maximum(:due_date) |
737 if p.start_date && p.due_date && p.due_date < p.start_date | 786 if p.start_date && p.due_date && p.due_date < p.start_date |
738 p.start_date, p.due_date = p.due_date, p.start_date | 787 p.start_date, p.due_date = p.due_date, p.start_date |
739 end | 788 end |
740 | 789 |
741 # done ratio = weighted average ratio of leaves | 790 # done ratio = weighted average ratio of leaves |
742 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio | 791 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio |
743 leaves_count = p.leaves.count | 792 leaves_count = p.leaves.count |
744 if leaves_count > 0 | 793 if leaves_count > 0 |
745 average = p.leaves.average(:estimated_hours).to_f | 794 average = p.leaves.average(:estimated_hours).to_f |
749 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f | 798 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f |
750 progress = done / (average * leaves_count) | 799 progress = done / (average * leaves_count) |
751 p.done_ratio = progress.round | 800 p.done_ratio = progress.round |
752 end | 801 end |
753 end | 802 end |
754 | 803 |
755 # estimate = sum of leaves estimates | 804 # estimate = sum of leaves estimates |
756 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f | 805 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f |
757 p.estimated_hours = nil if p.estimated_hours == 0.0 | 806 p.estimated_hours = nil if p.estimated_hours == 0.0 |
758 | 807 |
759 # ancestors will be recursively updated | 808 # ancestors will be recursively updated |
760 p.save(false) | 809 p.save(false) |
761 end | 810 end |
762 end | 811 end |
763 | 812 |
764 def destroy_children | |
765 unless leaf? | |
766 children.each do |child| | |
767 child.destroy | |
768 end | |
769 end | |
770 end | |
771 | |
772 # Update issues so their versions are not pointing to a | 813 # Update issues so their versions are not pointing to a |
773 # fixed_version that is not shared with the issue's project | 814 # fixed_version that is not shared with the issue's project |
774 def self.update_versions(conditions=nil) | 815 def self.update_versions(conditions=nil) |
775 # Only need to update issues with a fixed_version from | 816 # Only need to update issues with a fixed_version from |
776 # a different project and that is not systemwide shared | 817 # a different project and that is not systemwide shared |
786 issue.fixed_version = nil | 827 issue.fixed_version = nil |
787 issue.save | 828 issue.save |
788 end | 829 end |
789 end | 830 end |
790 end | 831 end |
791 | 832 |
792 # Callback on attachment deletion | 833 # Callback on attachment deletion |
793 def attachment_removed(obj) | 834 def attachment_removed(obj) |
794 journal = init_journal(User.current) | 835 journal = init_journal(User.current) |
795 journal.details << JournalDetail.new(:property => 'attachment', | 836 journal.details << JournalDetail.new(:property => 'attachment', |
796 :prop_key => obj.id, | 837 :prop_key => obj.id, |
797 :old_value => obj.filename) | 838 :old_value => obj.filename) |
798 journal.save | 839 journal.save |
799 end | 840 end |
800 | 841 |
801 # Default assignment based on category | 842 # Default assignment based on category |
802 def default_assign | 843 def default_assign |
803 if assigned_to.nil? && category && category.assigned_to | 844 if assigned_to.nil? && category && category.assigned_to |
804 self.assigned_to = category.assigned_to | 845 self.assigned_to = category.assigned_to |
805 end | 846 end |
828 end | 869 end |
829 duplicate.update_attribute :status, self.status | 870 duplicate.update_attribute :status, self.status |
830 end | 871 end |
831 end | 872 end |
832 end | 873 end |
833 | 874 |
834 # Saves the changes in a Journal | 875 # Saves the changes in a Journal |
835 # Called after_save | 876 # Called after_save |
836 def create_journal | 877 def create_journal |
837 if @current_journal | 878 if @current_journal |
838 # attributes changes | 879 # attributes changes |
839 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c| | 880 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c| |
881 before = @issue_before_change.send(c) | |
882 after = send(c) | |
883 next if before == after || (before.blank? && after.blank?) | |
840 @current_journal.details << JournalDetail.new(:property => 'attr', | 884 @current_journal.details << JournalDetail.new(:property => 'attr', |
841 :prop_key => c, | 885 :prop_key => c, |
842 :old_value => @issue_before_change.send(c), | 886 :old_value => @issue_before_change.send(c), |
843 :value => send(c)) unless send(c)==@issue_before_change.send(c) | 887 :value => send(c)) |
844 } | 888 } |
845 # custom fields changes | 889 # custom fields changes |
846 custom_values.each {|c| | 890 custom_values.each {|c| |
847 next if (@custom_values_before_change[c.custom_field_id]==c.value || | 891 next if (@custom_values_before_change[c.custom_field_id]==c.value || |
848 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?)) | 892 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?)) |
849 @current_journal.details << JournalDetail.new(:property => 'cf', | 893 @current_journal.details << JournalDetail.new(:property => 'cf', |
850 :prop_key => c.custom_field_id, | 894 :prop_key => c.custom_field_id, |
851 :old_value => @custom_values_before_change[c.custom_field_id], | 895 :old_value => @custom_values_before_change[c.custom_field_id], |
852 :value => c.value) | 896 :value => c.value) |
853 } | 897 } |
854 @current_journal.save | 898 @current_journal.save |
855 # reset current journal | 899 # reset current journal |
856 init_journal @current_journal.user, @current_journal.notes | 900 init_journal @current_journal.user, @current_journal.notes |
857 end | 901 end |
858 end | 902 end |
867 def self.count_and_group_by(options) | 911 def self.count_and_group_by(options) |
868 project = options.delete(:project) | 912 project = options.delete(:project) |
869 select_field = options.delete(:field) | 913 select_field = options.delete(:field) |
870 joins = options.delete(:joins) | 914 joins = options.delete(:joins) |
871 | 915 |
872 where = "i.#{select_field}=j.id" | 916 where = "#{Issue.table_name}.#{select_field}=j.id" |
873 | 917 |
874 ActiveRecord::Base.connection.select_all("select s.id as status_id, | 918 ActiveRecord::Base.connection.select_all("select s.id as status_id, |
875 s.is_closed as closed, | 919 s.is_closed as closed, |
876 j.id as #{select_field}, | 920 j.id as #{select_field}, |
877 count(i.id) as total | 921 count(#{Issue.table_name}.id) as total |
878 from | 922 from |
879 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j | 923 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j |
880 where | 924 where |
881 i.status_id=s.id | 925 #{Issue.table_name}.status_id=s.id |
882 and #{where} | 926 and #{where} |
883 and i.project_id=#{project.id} | 927 and #{Issue.table_name}.project_id=#{Project.table_name}.id |
928 and #{visible_condition(User.current, :project => project)} | |
884 group by s.id, s.is_closed, j.id") | 929 group by s.id, s.is_closed, j.id") |
885 end | 930 end |
886 | |
887 | |
888 end | 931 end |