Mercurial > hg > soundsoftware-site
comparison app/models/.svn/text-base/issue.rb.svn-base @ 511:107d36338b70 live
Merge from branch "cannam"
author | Chris Cannam |
---|---|
date | Thu, 14 Jul 2011 10:43:07 +0100 |
parents | 0c939c159af4 |
children |
comparison
equal
deleted
inserted
replaced
451:a9f6345cb43d | 511:107d36338b70 |
---|---|
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' |
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}.committed_on ASC, #{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 |
93 | 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 | |
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} priority-#{priority.position}" | 574 s = "issue status-#{status.position} priority-#{priority.position}" |
531 s << ' closed' if closed? | 575 s << ' closed' if closed? |
532 s << ' overdue' if overdue? | 576 s << ' overdue' if overdue? |
577 s << ' child' if child? | |
578 s << ' parent' unless leaf? | |
579 s << ' private' if is_private? | |
533 s << ' created-by-me' if User.current.logged? && author_id == User.current.id | 580 s << ' created-by-me' if User.current.logged? && author_id == User.current.id |
534 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id | 581 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id |
535 s | 582 s |
536 end | 583 end |
537 | 584 |
538 # Saves an issue, time_entry, attachments, and a journal from the parameters | 585 # Saves an issue, time_entry, attachments, and a journal from the parameters |
539 # Returns false if save fails | 586 # Returns false if save fails |
540 def save_issue_with_child_records(params, existing_time_entry=nil) | 587 def save_issue_with_child_records(params, existing_time_entry=nil) |
541 Issue.transaction do | 588 Issue.transaction do |
542 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project) | 589 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project) |
543 @time_entry = existing_time_entry || TimeEntry.new | 590 @time_entry = existing_time_entry || TimeEntry.new |
544 @time_entry.project = project | 591 @time_entry.project = project |
545 @time_entry.issue = self | 592 @time_entry.issue = self |
546 @time_entry.user = User.current | 593 @time_entry.user = User.current |
547 @time_entry.spent_on = Date.today | 594 @time_entry.spent_on = Date.today |
548 @time_entry.attributes = params[:time_entry] | 595 @time_entry.attributes = params[:time_entry] |
549 self.time_entries << @time_entry | 596 self.time_entries << @time_entry |
550 end | 597 end |
551 | 598 |
552 if valid? | 599 if valid? |
553 attachments = Attachment.attach_files(self, params[:attachments]) | 600 attachments = Attachment.attach_files(self, params[:attachments]) |
554 | 601 |
555 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} | 602 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} |
556 # TODO: Rename hook | 603 # TODO: Rename hook |
557 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) | 604 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) |
558 begin | 605 begin |
559 if save | 606 if save |
574 # Unassigns issues from +version+ if it's no longer shared with issue's project | 621 # Unassigns issues from +version+ if it's no longer shared with issue's project |
575 def self.update_versions_from_sharing_change(version) | 622 def self.update_versions_from_sharing_change(version) |
576 # Update issues assigned to the version | 623 # Update issues assigned to the version |
577 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id]) | 624 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id]) |
578 end | 625 end |
579 | 626 |
580 # Unassigns issues from versions that are no longer shared | 627 # Unassigns issues from versions that are no longer shared |
581 # after +project+ was moved | 628 # after +project+ was moved |
582 def self.update_versions_from_hierarchy_change(project) | 629 def self.update_versions_from_hierarchy_change(project) |
583 moved_project_ids = project.self_and_descendants.reload.collect(&:id) | 630 moved_project_ids = project.self_and_descendants.reload.collect(&:id) |
584 # Update issues of the moved projects and issues assigned to a version of a moved project | 631 # Update issues of the moved projects and issues assigned to a version of a moved project |
592 else | 639 else |
593 @parent_issue = nil | 640 @parent_issue = nil |
594 nil | 641 nil |
595 end | 642 end |
596 end | 643 end |
597 | 644 |
598 def parent_issue_id | 645 def parent_issue_id |
599 if instance_variable_defined? :@parent_issue | 646 if instance_variable_defined? :@parent_issue |
600 @parent_issue.nil? ? nil : @parent_issue.id | 647 @parent_issue.nil? ? nil : @parent_issue.id |
601 else | 648 else |
602 parent_id | 649 parent_id |
641 end | 688 end |
642 | 689 |
643 def self.by_subproject(project) | 690 def self.by_subproject(project) |
644 ActiveRecord::Base.connection.select_all("select s.id as status_id, | 691 ActiveRecord::Base.connection.select_all("select s.id as status_id, |
645 s.is_closed as closed, | 692 s.is_closed as closed, |
646 i.project_id as project_id, | 693 #{Issue.table_name}.project_id as project_id, |
647 count(i.id) as total | 694 count(#{Issue.table_name}.id) as total |
648 from | 695 from |
649 #{Issue.table_name} i, #{IssueStatus.table_name} s | 696 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s |
650 where | 697 where |
651 i.status_id=s.id | 698 #{Issue.table_name}.status_id=s.id |
652 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')}) | 699 and #{Issue.table_name}.project_id = #{Project.table_name}.id |
653 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any? | 700 and #{visible_condition(User.current, :project => project, :with_subprojects => true)} |
701 and #{Issue.table_name}.project_id <> #{project.id} | |
702 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any? | |
654 end | 703 end |
655 # End ReportsController extraction | 704 # End ReportsController extraction |
656 | 705 |
657 # Returns an array of projects that current user can move issues to | 706 # Returns an array of projects that current user can move issues to |
658 def self.allowed_target_projects_on_move | 707 def self.allowed_target_projects_on_move |
659 projects = [] | 708 projects = [] |
660 if User.current.admin? | 709 if User.current.admin? |
661 # admin is allowed to move issues to any active (visible) project | 710 # admin is allowed to move issues to any active (visible) project |
667 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}} | 716 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}} |
668 end | 717 end |
669 end | 718 end |
670 projects | 719 projects |
671 end | 720 end |
672 | 721 |
673 private | 722 private |
674 | 723 |
675 def update_nested_set_attributes | 724 def update_nested_set_attributes |
676 if root_id.nil? | 725 if root_id.nil? |
677 # issue was just created | 726 # issue was just created |
678 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) | 727 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) |
679 set_default_left_and_right | 728 set_default_left_and_right |
716 # update former parent | 765 # update former parent |
717 recalculate_attributes_for(former_parent_id) if former_parent_id | 766 recalculate_attributes_for(former_parent_id) if former_parent_id |
718 end | 767 end |
719 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue) | 768 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue) |
720 end | 769 end |
721 | 770 |
722 def update_parent_attributes | 771 def update_parent_attributes |
723 recalculate_attributes_for(parent_id) if parent_id | 772 recalculate_attributes_for(parent_id) if parent_id |
724 end | 773 end |
725 | 774 |
726 def recalculate_attributes_for(issue_id) | 775 def recalculate_attributes_for(issue_id) |
727 if issue_id && p = Issue.find_by_id(issue_id) | 776 if issue_id && p = Issue.find_by_id(issue_id) |
728 # priority = highest priority of children | 777 # priority = highest priority of children |
729 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority) | 778 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority) |
730 p.priority = IssuePriority.find_by_position(priority_position) | 779 p.priority = IssuePriority.find_by_position(priority_position) |
731 end | 780 end |
732 | 781 |
733 # start/due dates = lowest/highest dates of children | 782 # start/due dates = lowest/highest dates of children |
734 p.start_date = p.children.minimum(:start_date) | 783 p.start_date = p.children.minimum(:start_date) |
735 p.due_date = p.children.maximum(:due_date) | 784 p.due_date = p.children.maximum(:due_date) |
736 if p.start_date && p.due_date && p.due_date < p.start_date | 785 if p.start_date && p.due_date && p.due_date < p.start_date |
737 p.start_date, p.due_date = p.due_date, p.start_date | 786 p.start_date, p.due_date = p.due_date, p.start_date |
738 end | 787 end |
739 | 788 |
740 # done ratio = weighted average ratio of leaves | 789 # done ratio = weighted average ratio of leaves |
741 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio | 790 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio |
742 leaves_count = p.leaves.count | 791 leaves_count = p.leaves.count |
743 if leaves_count > 0 | 792 if leaves_count > 0 |
744 average = p.leaves.average(:estimated_hours).to_f | 793 average = p.leaves.average(:estimated_hours).to_f |
748 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 | 797 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 |
749 progress = done / (average * leaves_count) | 798 progress = done / (average * leaves_count) |
750 p.done_ratio = progress.round | 799 p.done_ratio = progress.round |
751 end | 800 end |
752 end | 801 end |
753 | 802 |
754 # estimate = sum of leaves estimates | 803 # estimate = sum of leaves estimates |
755 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f | 804 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f |
756 p.estimated_hours = nil if p.estimated_hours == 0.0 | 805 p.estimated_hours = nil if p.estimated_hours == 0.0 |
757 | 806 |
758 # ancestors will be recursively updated | 807 # ancestors will be recursively updated |
759 p.save(false) | 808 p.save(false) |
760 end | 809 end |
761 end | 810 end |
762 | 811 |
763 def destroy_children | |
764 unless leaf? | |
765 children.each do |child| | |
766 child.destroy | |
767 end | |
768 end | |
769 end | |
770 | |
771 # Update issues so their versions are not pointing to a | 812 # Update issues so their versions are not pointing to a |
772 # fixed_version that is not shared with the issue's project | 813 # fixed_version that is not shared with the issue's project |
773 def self.update_versions(conditions=nil) | 814 def self.update_versions(conditions=nil) |
774 # Only need to update issues with a fixed_version from | 815 # Only need to update issues with a fixed_version from |
775 # a different project and that is not systemwide shared | 816 # a different project and that is not systemwide shared |
785 issue.fixed_version = nil | 826 issue.fixed_version = nil |
786 issue.save | 827 issue.save |
787 end | 828 end |
788 end | 829 end |
789 end | 830 end |
790 | 831 |
791 # Callback on attachment deletion | 832 # Callback on attachment deletion |
792 def attachment_removed(obj) | 833 def attachment_removed(obj) |
793 journal = init_journal(User.current) | 834 journal = init_journal(User.current) |
794 journal.details << JournalDetail.new(:property => 'attachment', | 835 journal.details << JournalDetail.new(:property => 'attachment', |
795 :prop_key => obj.id, | 836 :prop_key => obj.id, |
796 :old_value => obj.filename) | 837 :old_value => obj.filename) |
797 journal.save | 838 journal.save |
798 end | 839 end |
799 | 840 |
800 # Default assignment based on category | 841 # Default assignment based on category |
801 def default_assign | 842 def default_assign |
802 if assigned_to.nil? && category && category.assigned_to | 843 if assigned_to.nil? && category && category.assigned_to |
803 self.assigned_to = category.assigned_to | 844 self.assigned_to = category.assigned_to |
804 end | 845 end |
827 end | 868 end |
828 duplicate.update_attribute :status, self.status | 869 duplicate.update_attribute :status, self.status |
829 end | 870 end |
830 end | 871 end |
831 end | 872 end |
832 | 873 |
833 # Saves the changes in a Journal | 874 # Saves the changes in a Journal |
834 # Called after_save | 875 # Called after_save |
835 def create_journal | 876 def create_journal |
836 if @current_journal | 877 if @current_journal |
837 # attributes changes | 878 # attributes changes |
838 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c| | 879 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c| |
880 before = @issue_before_change.send(c) | |
881 after = send(c) | |
882 next if before == after || (before.blank? && after.blank?) | |
839 @current_journal.details << JournalDetail.new(:property => 'attr', | 883 @current_journal.details << JournalDetail.new(:property => 'attr', |
840 :prop_key => c, | 884 :prop_key => c, |
841 :old_value => @issue_before_change.send(c), | 885 :old_value => @issue_before_change.send(c), |
842 :value => send(c)) unless send(c)==@issue_before_change.send(c) | 886 :value => send(c)) |
843 } | 887 } |
844 # custom fields changes | 888 # custom fields changes |
845 custom_values.each {|c| | 889 custom_values.each {|c| |
846 next if (@custom_values_before_change[c.custom_field_id]==c.value || | 890 next if (@custom_values_before_change[c.custom_field_id]==c.value || |
847 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?)) | 891 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?)) |
848 @current_journal.details << JournalDetail.new(:property => 'cf', | 892 @current_journal.details << JournalDetail.new(:property => 'cf', |
849 :prop_key => c.custom_field_id, | 893 :prop_key => c.custom_field_id, |
850 :old_value => @custom_values_before_change[c.custom_field_id], | 894 :old_value => @custom_values_before_change[c.custom_field_id], |
851 :value => c.value) | 895 :value => c.value) |
852 } | 896 } |
853 @current_journal.save | 897 @current_journal.save |
854 # reset current journal | 898 # reset current journal |
855 init_journal @current_journal.user, @current_journal.notes | 899 init_journal @current_journal.user, @current_journal.notes |
856 end | 900 end |
857 end | 901 end |
866 def self.count_and_group_by(options) | 910 def self.count_and_group_by(options) |
867 project = options.delete(:project) | 911 project = options.delete(:project) |
868 select_field = options.delete(:field) | 912 select_field = options.delete(:field) |
869 joins = options.delete(:joins) | 913 joins = options.delete(:joins) |
870 | 914 |
871 where = "i.#{select_field}=j.id" | 915 where = "#{Issue.table_name}.#{select_field}=j.id" |
872 | 916 |
873 ActiveRecord::Base.connection.select_all("select s.id as status_id, | 917 ActiveRecord::Base.connection.select_all("select s.id as status_id, |
874 s.is_closed as closed, | 918 s.is_closed as closed, |
875 j.id as #{select_field}, | 919 j.id as #{select_field}, |
876 count(i.id) as total | 920 count(#{Issue.table_name}.id) as total |
877 from | 921 from |
878 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j | 922 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j |
879 where | 923 where |
880 i.status_id=s.id | 924 #{Issue.table_name}.status_id=s.id |
881 and #{where} | 925 and #{where} |
882 and i.project_id=#{project.id} | 926 and #{Issue.table_name}.project_id=#{Project.table_name}.id |
927 and #{visible_condition(User.current, :project => project)} | |
883 group by s.id, s.is_closed, j.id") | 928 group by s.id, s.is_closed, j.id") |
884 end | 929 end |
885 | |
886 | |
887 end | 930 end |