Mercurial > hg > soundsoftware-site
comparison app/models/.svn/text-base/issue.rb.svn-base @ 441:cbce1fd3b1b7 redmine-1.2
Update to Redmine 1.2-stable branch (Redmine SVN rev 6000)
author | Chris Cannam |
---|---|
date | Mon, 06 Jun 2011 14:24:13 +0100 |
parents | 051f544170fe |
children | 0c939c159af4 |
comparison
equal
deleted
inserted
replaced
245:051f544170fe | 441:cbce1fd3b1b7 |
---|---|
3 # | 3 # |
4 # This program is free software; you can redistribute it and/or | 4 # This program is free software; you can redistribute it and/or |
5 # modify it under the terms of the GNU General Public License | 5 # modify it under the terms of the GNU General Public License |
6 # as published by the Free Software Foundation; either version 2 | 6 # as published by the Free Software Foundation; either version 2 |
7 # of the License, or (at your option) any later version. | 7 # of the License, or (at your option) any later version. |
8 # | 8 # |
9 # This program is distributed in the hope that it will be useful, | 9 # This program is distributed in the hope that it will be useful, |
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 # GNU General Public License for more details. | 12 # GNU General Public License for more details. |
13 # | 13 # |
14 # You should have received a copy of the GNU General Public License | 14 # You should have received a copy of the GNU General Public License |
15 # along with this program; if not, write to the Free Software | 15 # along with this program; if not, write to the Free Software |
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | 17 |
18 class Issue < ActiveRecord::Base | 18 class Issue < ActiveRecord::Base |
19 include Redmine::SafeAttributes | 19 include Redmine::SafeAttributes |
20 | 20 |
21 belongs_to :project | 21 belongs_to :project |
22 belongs_to :tracker | 22 belongs_to :tracker |
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' | 23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' |
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' | 24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' |
25 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' |
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' | 28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' |
29 | 29 |
30 has_many :journals, :as => :journalized, :dependent => :destroy | 30 has_many :journals, :as => :journalized, :dependent => :destroy |
31 has_many :time_entries, :dependent => :delete_all | 31 has_many :time_entries, :dependent => :delete_all |
32 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" |
33 | 33 |
34 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 |
35 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 |
36 | 36 |
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy | 37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy |
38 acts_as_attachable :after_remove => :attachment_removed | 38 acts_as_attachable :after_remove => :attachment_removed |
39 acts_as_customizable | 39 acts_as_customizable |
40 acts_as_watchable | 40 acts_as_watchable |
41 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"], |
43 # 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 |
44 :order_column => "#{table_name}.id" | 44 :order_column => "#{table_name}.id" |
45 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}"}, |
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}, | 46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}, |
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') } | 47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') } |
48 | 48 |
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}, | 49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}, |
50 :author_key => :author_id | 50 :author_key => :author_id |
51 | 51 |
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status) | 52 DONE_RATIO_OPTIONS = %w(issue_field issue_status) |
53 | 53 |
58 validates_length_of :subject, :maximum => 255 | 58 validates_length_of :subject, :maximum => 255 |
59 validates_inclusion_of :done_ratio, :in => 0..100 | 59 validates_inclusion_of :done_ratio, :in => 0..100 |
60 validates_numericality_of :estimated_hours, :allow_nil => true | 60 validates_numericality_of :estimated_hours, :allow_nil => true |
61 | 61 |
62 named_scope :visible, lambda {|*args| { :include => :project, | 62 named_scope :visible, lambda {|*args| { :include => :project, |
63 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } } | 63 :conditions => Issue.visible_condition(args.shift || User.current, *args) } } |
64 | 64 |
65 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 |
66 | 66 |
67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC" | 67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC" |
68 named_scope :with_limit, lambda { |limit| { :limit => limit} } | 68 named_scope :with_limit, lambda { |limit| { :limit => limit} } |
69 named_scope :on_active_project, :include => [:status, :project, :tracker], | 69 named_scope :on_active_project, :include => [:status, :project, :tracker], |
70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] | 70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] |
71 named_scope :for_gantt, lambda { | |
72 { | |
73 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version] | |
74 } | |
75 } | |
76 | 71 |
77 named_scope :without_version, lambda { | 72 named_scope :without_version, lambda { |
78 { | 73 { |
79 :conditions => { :fixed_version_id => nil} | 74 :conditions => { :fixed_version_id => nil} |
80 } | 75 } |
88 | 83 |
89 before_create :default_assign | 84 before_create :default_assign |
90 before_save :close_duplicates, :update_done_ratio_from_issue_status | 85 before_save :close_duplicates, :update_done_ratio_from_issue_status |
91 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 |
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 |
196 self.tracker = nil | 225 self.tracker = nil |
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 |
202 # Overrides attributes= so that tracker_id gets assigned first | 231 # Overrides attributes= so that tracker_id gets assigned first |
203 def attributes_with_tracker_first=(new_attributes, *args) | 232 def attributes_with_tracker_first=(new_attributes, *args) |
204 return if new_attributes.nil? | 233 return if new_attributes.nil? |
205 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id] | 234 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id] |
206 if new_tracker_id | 235 if new_tracker_id |
208 end | 237 end |
209 send :attributes_without_tracker_first=, new_attributes, *args | 238 send :attributes_without_tracker_first=, new_attributes, *args |
210 end | 239 end |
211 # Do not redefine alias chain on reload (see #4838) | 240 # Do not redefine alias chain on reload (see #4838) |
212 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=) | 241 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=) |
213 | 242 |
214 def estimated_hours=(h) | 243 def estimated_hours=(h) |
215 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) | 244 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) |
216 end | 245 end |
217 | 246 |
218 safe_attributes 'tracker_id', | 247 safe_attributes 'tracker_id', |
219 'status_id', | 248 'status_id', |
220 'parent_issue_id', | 249 'parent_issue_id', |
221 'category_id', | 250 'category_id', |
222 'assigned_to_id', | 251 'assigned_to_id', |
230 'estimated_hours', | 259 'estimated_hours', |
231 'custom_field_values', | 260 'custom_field_values', |
232 'custom_fields', | 261 'custom_fields', |
233 'lock_version', | 262 'lock_version', |
234 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) } | 263 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) } |
235 | 264 |
236 safe_attributes 'status_id', | 265 safe_attributes 'status_id', |
237 'assigned_to_id', | 266 'assigned_to_id', |
238 'fixed_version_id', | 267 'fixed_version_id', |
239 'done_ratio', | 268 'done_ratio', |
240 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? } | 269 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? } |
270 | |
271 safe_attributes 'is_private', | |
272 :if => lambda {|issue, user| | |
273 user.allowed_to?(:set_issues_private, issue.project) || | |
274 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project)) | |
275 } | |
241 | 276 |
242 # Safely sets attributes | 277 # Safely sets attributes |
243 # Should be called from controllers instead of #attributes= | 278 # Should be called from controllers instead of #attributes= |
244 # attr_accessible is too rough because we still want things like | 279 # attr_accessible is too rough because we still want things like |
245 # Issue.new(:project => foo) to work | 280 # Issue.new(:project => foo) to work |
246 # TODO: move workflow/permission checks from controllers to here | 281 # TODO: move workflow/permission checks from controllers to here |
247 def safe_attributes=(attrs, user=User.current) | 282 def safe_attributes=(attrs, user=User.current) |
248 return unless attrs.is_a?(Hash) | 283 return unless attrs.is_a?(Hash) |
249 | 284 |
250 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed | 285 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed |
251 attrs = delete_unsafe_attributes(attrs, user) | 286 attrs = delete_unsafe_attributes(attrs, user) |
252 return if attrs.empty? | 287 return if attrs.empty? |
253 | 288 |
254 # Tracker must be set before since new_statuses_allowed_to depends on it. | 289 # Tracker must be set before since new_statuses_allowed_to depends on it. |
255 if t = attrs.delete('tracker_id') | 290 if t = attrs.delete('tracker_id') |
256 self.tracker_id = t | 291 self.tracker_id = t |
257 end | 292 end |
258 | 293 |
259 if attrs['status_id'] | 294 if attrs['status_id'] |
260 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i) | 295 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i) |
261 attrs.delete('status_id') | 296 attrs.delete('status_id') |
262 end | 297 end |
263 end | 298 end |
264 | 299 |
265 unless leaf? | 300 unless leaf? |
266 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)} | 301 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)} |
267 end | 302 end |
268 | 303 |
269 if attrs.has_key?('parent_issue_id') | 304 if attrs.has_key?('parent_issue_id') |
270 if !user.allowed_to?(:manage_subtasks, project) | 305 if !user.allowed_to?(:manage_subtasks, project) |
271 attrs.delete('parent_issue_id') | 306 attrs.delete('parent_issue_id') |
272 elsif !attrs['parent_issue_id'].blank? | 307 elsif !attrs['parent_issue_id'].blank? |
273 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i) | 308 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i) |
274 end | 309 end |
275 end | 310 end |
276 | 311 |
277 self.attributes = attrs | 312 self.attributes = attrs |
278 end | 313 end |
279 | 314 |
280 def done_ratio | 315 def done_ratio |
281 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio | 316 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio |
282 status.default_done_ratio | 317 status.default_done_ratio |
283 else | 318 else |
284 read_attribute(:done_ratio) | 319 read_attribute(:done_ratio) |
290 end | 325 end |
291 | 326 |
292 def self.use_field_for_done_ratio? | 327 def self.use_field_for_done_ratio? |
293 Setting.issue_done_ratio == 'issue_field' | 328 Setting.issue_done_ratio == 'issue_field' |
294 end | 329 end |
295 | 330 |
296 def validate | 331 def validate |
297 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? | 332 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? |
298 errors.add :due_date, :not_a_date | 333 errors.add :due_date, :not_a_date |
299 end | 334 end |
300 | 335 |
301 if self.due_date and self.start_date and self.due_date < self.start_date | 336 if self.due_date and self.start_date and self.due_date < self.start_date |
302 errors.add :due_date, :greater_than_start_date | 337 errors.add :due_date, :greater_than_start_date |
303 end | 338 end |
304 | 339 |
305 if start_date && soonest_start && start_date < soonest_start | 340 if start_date && soonest_start && start_date < soonest_start |
306 errors.add :start_date, :invalid | 341 errors.add :start_date, :invalid |
307 end | 342 end |
308 | 343 |
309 if fixed_version | 344 if fixed_version |
310 if !assignable_versions.include?(fixed_version) | 345 if !assignable_versions.include?(fixed_version) |
311 errors.add :fixed_version_id, :inclusion | 346 errors.add :fixed_version_id, :inclusion |
312 elsif reopened? && fixed_version.closed? | 347 elsif reopened? && fixed_version.closed? |
313 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version) | 348 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version) |
314 end | 349 end |
315 end | 350 end |
316 | 351 |
317 # Checks that the issue can not be added/moved to a disabled tracker | 352 # Checks that the issue can not be added/moved to a disabled tracker |
318 if project && (tracker_id_changed? || project_id_changed?) | 353 if project && (tracker_id_changed? || project_id_changed?) |
319 unless project.trackers.include?(tracker) | 354 unless project.trackers.include?(tracker) |
320 errors.add :tracker_id, :inclusion | 355 errors.add :tracker_id, :inclusion |
321 end | 356 end |
322 end | 357 end |
323 | 358 |
324 # Checks parent issue assignment | 359 # Checks parent issue assignment |
325 if @parent_issue | 360 if @parent_issue |
326 if @parent_issue.project_id != project_id | 361 if @parent_issue.project_id != project_id |
327 errors.add :parent_issue_id, :not_same_project | 362 errors.add :parent_issue_id, :not_same_project |
328 elsif !new_record? | 363 elsif !new_record? |
335 errors.add :parent_issue_id, :not_a_valid_parent | 370 errors.add :parent_issue_id, :not_a_valid_parent |
336 end | 371 end |
337 end | 372 end |
338 end | 373 end |
339 end | 374 end |
340 | 375 |
341 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios | 376 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios |
342 # even if the user turns off the setting later | 377 # even if the user turns off the setting later |
343 def update_done_ratio_from_issue_status | 378 def update_done_ratio_from_issue_status |
344 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio | 379 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio |
345 self.done_ratio = status.default_done_ratio | 380 self.done_ratio = status.default_done_ratio |
346 end | 381 end |
347 end | 382 end |
348 | 383 |
349 def init_journal(user, notes = "") | 384 def init_journal(user, notes = "") |
350 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) | 385 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) |
351 @issue_before_change = self.clone | 386 @issue_before_change = self.clone |
352 @issue_before_change.status = self.status | 387 @issue_before_change.status = self.status |
353 @custom_values_before_change = {} | 388 @custom_values_before_change = {} |
354 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } | 389 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } |
355 # Make sure updated_on is updated when adding a note. | 390 # Make sure updated_on is updated when adding a note. |
356 updated_on_will_change! | 391 updated_on_will_change! |
357 @current_journal | 392 @current_journal |
358 end | 393 end |
359 | 394 |
360 # Return true if the issue is closed, otherwise false | 395 # Return true if the issue is closed, otherwise false |
361 def closed? | 396 def closed? |
362 self.status.is_closed? | 397 self.status.is_closed? |
363 end | 398 end |
364 | 399 |
365 # Return true if the issue is being reopened | 400 # Return true if the issue is being reopened |
366 def reopened? | 401 def reopened? |
367 if !new_record? && status_id_changed? | 402 if !new_record? && status_id_changed? |
368 status_was = IssueStatus.find_by_id(status_id_was) | 403 status_was = IssueStatus.find_by_id(status_id_was) |
369 status_new = IssueStatus.find_by_id(status_id) | 404 status_new = IssueStatus.find_by_id(status_id) |
383 return true | 418 return true |
384 end | 419 end |
385 end | 420 end |
386 false | 421 false |
387 end | 422 end |
388 | 423 |
389 # Returns true if the issue is overdue | 424 # Returns true if the issue is overdue |
390 def overdue? | 425 def overdue? |
391 !due_date.nil? && (due_date < Date.today) && !status.is_closed? | 426 !due_date.nil? && (due_date < Date.today) && !status.is_closed? |
392 end | 427 end |
393 | 428 |
400 | 435 |
401 # Does this issue have children? | 436 # Does this issue have children? |
402 def children? | 437 def children? |
403 !leaf? | 438 !leaf? |
404 end | 439 end |
405 | 440 |
406 # Users the issue can be assigned to | 441 # Users the issue can be assigned to |
407 def assignable_users | 442 def assignable_users |
408 users = project.assignable_users | 443 users = project.assignable_users |
409 users << author if author | 444 users << author if author |
410 users.uniq.sort | 445 users.uniq.sort |
411 end | 446 end |
412 | 447 |
413 # Versions that the issue can be assigned to | 448 # Versions that the issue can be assigned to |
414 def assignable_versions | 449 def assignable_versions |
415 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort | 450 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort |
416 end | 451 end |
417 | 452 |
418 # Returns true if this issue is blocked by another issue that is still open | 453 # Returns true if this issue is blocked by another issue that is still open |
419 def blocked? | 454 def blocked? |
420 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil? | 455 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil? |
421 end | 456 end |
422 | 457 |
423 # Returns an array of status that user is able to apply | 458 # Returns an array of status that user is able to apply |
424 def new_statuses_allowed_to(user, include_default=false) | 459 def new_statuses_allowed_to(user, include_default=false) |
425 statuses = status.find_new_statuses_allowed_to( | 460 statuses = status.find_new_statuses_allowed_to( |
426 user.roles_for_project(project), | 461 user.roles_for_project(project), |
427 tracker, | 462 tracker, |
431 statuses << status unless statuses.empty? | 466 statuses << status unless statuses.empty? |
432 statuses << IssueStatus.default if include_default | 467 statuses << IssueStatus.default if include_default |
433 statuses = statuses.uniq.sort | 468 statuses = statuses.uniq.sort |
434 blocked? ? statuses.reject {|s| s.is_closed?} : statuses | 469 blocked? ? statuses.reject {|s| s.is_closed?} : statuses |
435 end | 470 end |
436 | 471 |
437 # Returns the mail adresses of users that should be notified | 472 # Returns the mail adresses of users that should be notified |
438 def recipients | 473 def recipients |
439 notified = project.notified_users | 474 notified = project.notified_users |
440 # Author and assignee are always notified unless they have been | 475 # Author and assignee are always notified unless they have been |
441 # locked or don't want to be notified | 476 # locked or don't want to be notified |
444 notified.uniq! | 479 notified.uniq! |
445 # Remove users that can not view the issue | 480 # Remove users that can not view the issue |
446 notified.reject! {|user| !visible?(user)} | 481 notified.reject! {|user| !visible?(user)} |
447 notified.collect(&:mail) | 482 notified.collect(&:mail) |
448 end | 483 end |
449 | 484 |
450 # Returns the total number of hours spent on this issue and its descendants | 485 # Returns the total number of hours spent on this issue and its descendants |
451 # | 486 # |
452 # Example: | 487 # Example: |
453 # spent_hours => 0.0 | 488 # spent_hours => 0.0 |
454 # spent_hours => 50.2 | 489 # spent_hours => 50.2 |
455 def spent_hours | 490 def spent_hours |
456 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0 | 491 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0 |
457 end | 492 end |
458 | 493 |
459 def relations | 494 def relations |
460 (relations_from + relations_to).sort | 495 (relations_from + relations_to).sort |
461 end | 496 end |
462 | 497 |
463 def all_dependent_issues(except=nil) | 498 def all_dependent_issues(except=[]) |
464 except ||= self | 499 except << self |
465 dependencies = [] | 500 dependencies = [] |
466 relations_from.each do |relation| | 501 relations_from.each do |relation| |
467 if relation.issue_to && relation.issue_to != except | 502 if relation.issue_to && !except.include?(relation.issue_to) |
468 dependencies << relation.issue_to | 503 dependencies << relation.issue_to |
469 dependencies += relation.issue_to.all_dependent_issues(except) | 504 dependencies += relation.issue_to.all_dependent_issues(except) |
470 end | 505 end |
471 end | 506 end |
472 dependencies | 507 dependencies |
473 end | 508 end |
474 | 509 |
475 # Returns an array of issues that duplicate this one | 510 # Returns an array of issues that duplicate this one |
476 def duplicates | 511 def duplicates |
477 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from} | 512 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from} |
478 end | 513 end |
479 | 514 |
480 # Returns the due date or the target due date if any | 515 # Returns the due date or the target due date if any |
481 # Used on gantt chart | 516 # Used on gantt chart |
482 def due_before | 517 def due_before |
483 due_date || (fixed_version ? fixed_version.effective_date : nil) | 518 due_date || (fixed_version ? fixed_version.effective_date : nil) |
484 end | 519 end |
485 | 520 |
486 # Returns the time scheduled for this issue. | 521 # Returns the time scheduled for this issue. |
487 # | 522 # |
488 # Example: | 523 # Example: |
489 # Start Date: 2/26/09, End Date: 3/04/09 | 524 # Start Date: 2/26/09, End Date: 3/04/09 |
490 # duration => 6 | 525 # duration => 6 |
491 def duration | 526 def duration |
492 (start_date && due_date) ? due_date - start_date : 0 | 527 (start_date && due_date) ? due_date - start_date : 0 |
493 end | 528 end |
494 | 529 |
495 def soonest_start | 530 def soonest_start |
496 @soonest_start ||= ( | 531 @soonest_start ||= ( |
497 relations_to.collect{|relation| relation.successor_soonest_start} + | 532 relations_to.collect{|relation| relation.successor_soonest_start} + |
498 ancestors.collect(&:soonest_start) | 533 ancestors.collect(&:soonest_start) |
499 ).compact.max | 534 ).compact.max |
500 end | 535 end |
501 | 536 |
502 def reschedule_after(date) | 537 def reschedule_after(date) |
503 return if date.nil? | 538 return if date.nil? |
504 if leaf? | 539 if leaf? |
505 if start_date.nil? || start_date < date | 540 if start_date.nil? || start_date < date |
506 self.start_date, self.due_date = date, date + duration | 541 self.start_date, self.due_date = date, date + duration |
510 leaves.each do |leaf| | 545 leaves.each do |leaf| |
511 leaf.reschedule_after(date) | 546 leaf.reschedule_after(date) |
512 end | 547 end |
513 end | 548 end |
514 end | 549 end |
515 | 550 |
516 def <=>(issue) | 551 def <=>(issue) |
517 if issue.nil? | 552 if issue.nil? |
518 -1 | 553 -1 |
519 elsif root_id != issue.root_id | 554 elsif root_id != issue.root_id |
520 (root_id || 0) <=> (issue.root_id || 0) | 555 (root_id || 0) <=> (issue.root_id || 0) |
521 else | 556 else |
522 (lft || 0) <=> (issue.lft || 0) | 557 (lft || 0) <=> (issue.lft || 0) |
523 end | 558 end |
524 end | 559 end |
525 | 560 |
526 def to_s | 561 def to_s |
527 "#{tracker} ##{id}: #{subject}" | 562 "#{tracker} ##{id}: #{subject}" |
528 end | 563 end |
529 | 564 |
530 # Returns a string of css classes that apply to the issue | 565 # Returns a string of css classes that apply to the issue |
531 def css_classes | 566 def css_classes |
532 s = "issue status-#{status.position} priority-#{priority.position}" | 567 s = "issue status-#{status.position} priority-#{priority.position}" |
533 s << ' closed' if closed? | 568 s << ' closed' if closed? |
534 s << ' overdue' if overdue? | 569 s << ' overdue' if overdue? |
570 s << ' child' if child? | |
571 s << ' parent' unless leaf? | |
572 s << ' private' if is_private? | |
535 s << ' created-by-me' if User.current.logged? && author_id == User.current.id | 573 s << ' created-by-me' if User.current.logged? && author_id == User.current.id |
536 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id | 574 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id |
537 s | 575 s |
538 end | 576 end |
539 | 577 |
540 # Saves an issue, time_entry, attachments, and a journal from the parameters | 578 # Saves an issue, time_entry, attachments, and a journal from the parameters |
541 # Returns false if save fails | 579 # Returns false if save fails |
542 def save_issue_with_child_records(params, existing_time_entry=nil) | 580 def save_issue_with_child_records(params, existing_time_entry=nil) |
543 Issue.transaction do | 581 Issue.transaction do |
544 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project) | 582 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project) |
545 @time_entry = existing_time_entry || TimeEntry.new | 583 @time_entry = existing_time_entry || TimeEntry.new |
546 @time_entry.project = project | 584 @time_entry.project = project |
547 @time_entry.issue = self | 585 @time_entry.issue = self |
548 @time_entry.user = User.current | 586 @time_entry.user = User.current |
549 @time_entry.spent_on = Date.today | 587 @time_entry.spent_on = Date.today |
550 @time_entry.attributes = params[:time_entry] | 588 @time_entry.attributes = params[:time_entry] |
551 self.time_entries << @time_entry | 589 self.time_entries << @time_entry |
552 end | 590 end |
553 | 591 |
554 if valid? | 592 if valid? |
555 attachments = Attachment.attach_files(self, params[:attachments]) | 593 attachments = Attachment.attach_files(self, params[:attachments]) |
556 | 594 |
557 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} | 595 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} |
558 # TODO: Rename hook | 596 # TODO: Rename hook |
559 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) | 597 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) |
560 begin | 598 begin |
561 if save | 599 if save |
576 # Unassigns issues from +version+ if it's no longer shared with issue's project | 614 # Unassigns issues from +version+ if it's no longer shared with issue's project |
577 def self.update_versions_from_sharing_change(version) | 615 def self.update_versions_from_sharing_change(version) |
578 # Update issues assigned to the version | 616 # Update issues assigned to the version |
579 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id]) | 617 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id]) |
580 end | 618 end |
581 | 619 |
582 # Unassigns issues from versions that are no longer shared | 620 # Unassigns issues from versions that are no longer shared |
583 # after +project+ was moved | 621 # after +project+ was moved |
584 def self.update_versions_from_hierarchy_change(project) | 622 def self.update_versions_from_hierarchy_change(project) |
585 moved_project_ids = project.self_and_descendants.reload.collect(&:id) | 623 moved_project_ids = project.self_and_descendants.reload.collect(&:id) |
586 # Update issues of the moved projects and issues assigned to a version of a moved project | 624 # Update issues of the moved projects and issues assigned to a version of a moved project |
594 else | 632 else |
595 @parent_issue = nil | 633 @parent_issue = nil |
596 nil | 634 nil |
597 end | 635 end |
598 end | 636 end |
599 | 637 |
600 def parent_issue_id | 638 def parent_issue_id |
601 if instance_variable_defined? :@parent_issue | 639 if instance_variable_defined? :@parent_issue |
602 @parent_issue.nil? ? nil : @parent_issue.id | 640 @parent_issue.nil? ? nil : @parent_issue.id |
603 else | 641 else |
604 parent_id | 642 parent_id |
643 end | 681 end |
644 | 682 |
645 def self.by_subproject(project) | 683 def self.by_subproject(project) |
646 ActiveRecord::Base.connection.select_all("select s.id as status_id, | 684 ActiveRecord::Base.connection.select_all("select s.id as status_id, |
647 s.is_closed as closed, | 685 s.is_closed as closed, |
648 i.project_id as project_id, | 686 #{Issue.table_name}.project_id as project_id, |
649 count(i.id) as total | 687 count(#{Issue.table_name}.id) as total |
650 from | 688 from |
651 #{Issue.table_name} i, #{IssueStatus.table_name} s | 689 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s |
652 where | 690 where |
653 i.status_id=s.id | 691 #{Issue.table_name}.status_id=s.id |
654 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')}) | 692 and #{Issue.table_name}.project_id = #{Project.table_name}.id |
655 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any? | 693 and #{visible_condition(User.current, :project => project, :with_subprojects => true)} |
694 and #{Issue.table_name}.project_id <> #{project.id} | |
695 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any? | |
656 end | 696 end |
657 # End ReportsController extraction | 697 # End ReportsController extraction |
658 | 698 |
659 # Returns an array of projects that current user can move issues to | 699 # Returns an array of projects that current user can move issues to |
660 def self.allowed_target_projects_on_move | 700 def self.allowed_target_projects_on_move |
661 projects = [] | 701 projects = [] |
662 if User.current.admin? | 702 if User.current.admin? |
663 # admin is allowed to move issues to any active (visible) project | 703 # admin is allowed to move issues to any active (visible) project |
669 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}} | 709 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}} |
670 end | 710 end |
671 end | 711 end |
672 projects | 712 projects |
673 end | 713 end |
674 | 714 |
675 private | 715 private |
676 | 716 |
677 def update_nested_set_attributes | 717 def update_nested_set_attributes |
678 if root_id.nil? | 718 if root_id.nil? |
679 # issue was just created | 719 # issue was just created |
680 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) | 720 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) |
681 set_default_left_and_right | 721 set_default_left_and_right |
718 # update former parent | 758 # update former parent |
719 recalculate_attributes_for(former_parent_id) if former_parent_id | 759 recalculate_attributes_for(former_parent_id) if former_parent_id |
720 end | 760 end |
721 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue) | 761 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue) |
722 end | 762 end |
723 | 763 |
724 def update_parent_attributes | 764 def update_parent_attributes |
725 recalculate_attributes_for(parent_id) if parent_id | 765 recalculate_attributes_for(parent_id) if parent_id |
726 end | 766 end |
727 | 767 |
728 def recalculate_attributes_for(issue_id) | 768 def recalculate_attributes_for(issue_id) |
729 if issue_id && p = Issue.find_by_id(issue_id) | 769 if issue_id && p = Issue.find_by_id(issue_id) |
730 # priority = highest priority of children | 770 # priority = highest priority of children |
731 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority) | 771 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority) |
732 p.priority = IssuePriority.find_by_position(priority_position) | 772 p.priority = IssuePriority.find_by_position(priority_position) |
733 end | 773 end |
734 | 774 |
735 # start/due dates = lowest/highest dates of children | 775 # start/due dates = lowest/highest dates of children |
736 p.start_date = p.children.minimum(:start_date) | 776 p.start_date = p.children.minimum(:start_date) |
737 p.due_date = p.children.maximum(:due_date) | 777 p.due_date = p.children.maximum(:due_date) |
738 if p.start_date && p.due_date && p.due_date < p.start_date | 778 if p.start_date && p.due_date && p.due_date < p.start_date |
739 p.start_date, p.due_date = p.due_date, p.start_date | 779 p.start_date, p.due_date = p.due_date, p.start_date |
740 end | 780 end |
741 | 781 |
742 # done ratio = weighted average ratio of leaves | 782 # done ratio = weighted average ratio of leaves |
743 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio | 783 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio |
744 leaves_count = p.leaves.count | 784 leaves_count = p.leaves.count |
745 if leaves_count > 0 | 785 if leaves_count > 0 |
746 average = p.leaves.average(:estimated_hours).to_f | 786 average = p.leaves.average(:estimated_hours).to_f |
750 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 | 790 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 |
751 progress = done / (average * leaves_count) | 791 progress = done / (average * leaves_count) |
752 p.done_ratio = progress.round | 792 p.done_ratio = progress.round |
753 end | 793 end |
754 end | 794 end |
755 | 795 |
756 # estimate = sum of leaves estimates | 796 # estimate = sum of leaves estimates |
757 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f | 797 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f |
758 p.estimated_hours = nil if p.estimated_hours == 0.0 | 798 p.estimated_hours = nil if p.estimated_hours == 0.0 |
759 | 799 |
760 # ancestors will be recursively updated | 800 # ancestors will be recursively updated |
761 p.save(false) | 801 p.save(false) |
762 end | 802 end |
763 end | 803 end |
764 | 804 |
765 # Update issues so their versions are not pointing to a | 805 # Update issues so their versions are not pointing to a |
766 # fixed_version that is not shared with the issue's project | 806 # fixed_version that is not shared with the issue's project |
767 def self.update_versions(conditions=nil) | 807 def self.update_versions(conditions=nil) |
768 # Only need to update issues with a fixed_version from | 808 # Only need to update issues with a fixed_version from |
769 # a different project and that is not systemwide shared | 809 # a different project and that is not systemwide shared |
779 issue.fixed_version = nil | 819 issue.fixed_version = nil |
780 issue.save | 820 issue.save |
781 end | 821 end |
782 end | 822 end |
783 end | 823 end |
784 | 824 |
785 # Callback on attachment deletion | 825 # Callback on attachment deletion |
786 def attachment_removed(obj) | 826 def attachment_removed(obj) |
787 journal = init_journal(User.current) | 827 journal = init_journal(User.current) |
788 journal.details << JournalDetail.new(:property => 'attachment', | 828 journal.details << JournalDetail.new(:property => 'attachment', |
789 :prop_key => obj.id, | 829 :prop_key => obj.id, |
790 :old_value => obj.filename) | 830 :old_value => obj.filename) |
791 journal.save | 831 journal.save |
792 end | 832 end |
793 | 833 |
794 # Default assignment based on category | 834 # Default assignment based on category |
795 def default_assign | 835 def default_assign |
796 if assigned_to.nil? && category && category.assigned_to | 836 if assigned_to.nil? && category && category.assigned_to |
797 self.assigned_to = category.assigned_to | 837 self.assigned_to = category.assigned_to |
798 end | 838 end |
821 end | 861 end |
822 duplicate.update_attribute :status, self.status | 862 duplicate.update_attribute :status, self.status |
823 end | 863 end |
824 end | 864 end |
825 end | 865 end |
826 | 866 |
827 # Saves the changes in a Journal | 867 # Saves the changes in a Journal |
828 # Called after_save | 868 # Called after_save |
829 def create_journal | 869 def create_journal |
830 if @current_journal | 870 if @current_journal |
831 # attributes changes | 871 # attributes changes |
837 } | 877 } |
838 # custom fields changes | 878 # custom fields changes |
839 custom_values.each {|c| | 879 custom_values.each {|c| |
840 next if (@custom_values_before_change[c.custom_field_id]==c.value || | 880 next if (@custom_values_before_change[c.custom_field_id]==c.value || |
841 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?)) | 881 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?)) |
842 @current_journal.details << JournalDetail.new(:property => 'cf', | 882 @current_journal.details << JournalDetail.new(:property => 'cf', |
843 :prop_key => c.custom_field_id, | 883 :prop_key => c.custom_field_id, |
844 :old_value => @custom_values_before_change[c.custom_field_id], | 884 :old_value => @custom_values_before_change[c.custom_field_id], |
845 :value => c.value) | 885 :value => c.value) |
846 } | 886 } |
847 @current_journal.save | 887 @current_journal.save |
848 # reset current journal | 888 # reset current journal |
849 init_journal @current_journal.user, @current_journal.notes | 889 init_journal @current_journal.user, @current_journal.notes |
850 end | 890 end |
851 end | 891 end |
860 def self.count_and_group_by(options) | 900 def self.count_and_group_by(options) |
861 project = options.delete(:project) | 901 project = options.delete(:project) |
862 select_field = options.delete(:field) | 902 select_field = options.delete(:field) |
863 joins = options.delete(:joins) | 903 joins = options.delete(:joins) |
864 | 904 |
865 where = "i.#{select_field}=j.id" | 905 where = "#{Issue.table_name}.#{select_field}=j.id" |
866 | 906 |
867 ActiveRecord::Base.connection.select_all("select s.id as status_id, | 907 ActiveRecord::Base.connection.select_all("select s.id as status_id, |
868 s.is_closed as closed, | 908 s.is_closed as closed, |
869 j.id as #{select_field}, | 909 j.id as #{select_field}, |
870 count(i.id) as total | 910 count(#{Issue.table_name}.id) as total |
871 from | 911 from |
872 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j | 912 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j |
873 where | 913 where |
874 i.status_id=s.id | 914 #{Issue.table_name}.status_id=s.id |
875 and #{where} | 915 and #{where} |
876 and i.project_id=#{project.id} | 916 and #{Issue.table_name}.project_id=#{Project.table_name}.id |
917 and #{visible_condition(User.current, :project => project)} | |
877 group by s.id, s.is_closed, j.id") | 918 group by s.id, s.is_closed, j.id") |
878 end | 919 end |
879 | |
880 | |
881 end | 920 end |