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