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