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