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