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