Class: Issue
- Inherits:
-
ActiveRecord::Base
- Object
- ActiveRecord::Base
- Issue
- Defined in:
- app/models/issue.rb
Overview
redMine - project management software Copyright (C) 2006-2007 Jean-Philippe Lang
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Constant Summary
- DONE_RATIO_OPTIONS =
%w(issue_field issue_status)
- SAFE_ATTRIBUTES =
%w( tracker_id status_id parent_issue_id category_id assigned_to_id priority_id fixed_version_id subject description start_date due_date done_ratio estimated_hours custom_field_values ) unless const_defined?(:SAFE_ATTRIBUTES)
Instance Attribute Summary
-
- (Object) current_journal
readonly
Returns the value of attribute current_journal.
Class Method Summary
- + (Object) by_assigned_to(project)
- + (Object) by_author(project)
- + (Object) by_category(project)
- + (Object) by_priority(project)
- + (Object) by_subproject(project)
-
+ (Object) by_tracker(project)
Extracted from the ReportsController.
- + (Object) by_version(project)
-
+ (Object) update_versions_from_hierarchy_change(project)
Unassigns issues from versions that are no longer shared after project was moved.
-
+ (Object) update_versions_from_sharing_change(version)
Unassigns issues from version if it’s no longer shared with issue’s project.
- + (Boolean) use_field_for_done_ratio?
- + (Boolean) use_status_for_done_ratio?
Instance Method Summary
- - (Object) <=>(issue)
- - (Object) after_initialize
- - (Object) all_dependent_issues
-
- (Object) assignable_users
Users the issue can be assigned to.
-
- (Object) assignable_versions
Versions that the issue can be assigned to.
-
- (Object) attributes_with_tracker_first(new_attributes, *args)
Overrides attributes= so that tracker_id gets assigned first.
-
- (Object) available_custom_fields
Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields.
-
- (Boolean) blocked?
Returns true if this issue is blocked by another issue that is still open.
-
- (Boolean) closed?
Return true if the issue is closed, otherwise false.
-
- (Boolean) closing?
Return true if the issue is being closed.
- - (Object) copy_from(arg)
-
- (Object) css_classes
Returns a string of css classes that apply to the issue.
- - (Object) done_ratio
-
- (Object) due_before
Returns the due date or the target due date if any Used on gantt chart.
-
- (Object) duplicates
Returns an array of issues that duplicate this one.
-
- (Object) duration
Returns the time scheduled for this issue.
- - (Object) estimated_hours(h)
- - (Object) init_journal(user, notes = "")
-
- (Object) move_to_project(*args)
Moves/copies an issue to a new project and tracker Returns the moved/copied issue on success, false on failure.
- - (Object) move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
-
- (Object) new_statuses_allowed_to(user, include_default = false)
Returns an array of status that user is able to apply.
-
- (Boolean) overdue?
Returns true if the issue is overdue.
- - (Object) parent_issue_id
- - (Object) parent_issue_id(arg)
- - (Object) priority_id(pid)
-
- (Object) recipients
Returns the mail adresses of users that should be notified.
- - (Object) relations
-
- (Boolean) reopened?
Return true if the issue is being reopened.
- - (Object) reschedule_after(date)
-
- (Object) safe_attributes(attrs, user = User.current)
Safely sets attributes Should be called from controllers instead of #attributes= attr_accessible is too rough because we still want things like Issue.new(:project => foo) to work TODO: move workflow/permission checks from controllers to here.
-
- (Object) save_issue_with_child_records(params, existing_time_entry = nil)
Saves an issue, time_entry, attachments, and a journal from the parameters.
- - (Object) soonest_start
-
- (Object) spent_hours
Returns the total number of hours spent on this issue and its descendants.
- - (Object) status_id(sid)
- - (Object) to_s
- - (Object) tracker_id(tid)
-
- (Object) update_done_ratio_from_issue_status
Set the done_ratio using the status if that setting is set.
- - (Object) validate
-
- (Boolean) visible?(usr = nil)
Returns true if usr or current user is allowed to view the issue.
Methods inherited from ActiveRecord::Base
Instance Attribute Details
- (Object) current_journal (readonly)
Returns the value of attribute current_journal
52 53 54 |
# File 'app/models/issue.rb', line 52 def current_journal @current_journal end |
Class Method Details
+ (Object) by_assigned_to(project)
568 569 570 571 572 |
# File 'app/models/issue.rb', line 568 def self.by_assigned_to(project) count_and_group_by(:project => project, :field => 'assigned_to_id', :joins => User.table_name) end |
+ (Object) by_author(project)
574 575 576 577 578 |
# File 'app/models/issue.rb', line 574 def self.(project) count_and_group_by(:project => project, :field => 'author_id', :joins => User.table_name) end |
+ (Object) by_category(project)
562 563 564 565 566 |
# File 'app/models/issue.rb', line 562 def self.by_category(project) count_and_group_by(:project => project, :field => 'category_id', :joins => IssueCategory.table_name) end |
+ (Object) by_priority(project)
556 557 558 559 560 |
# File 'app/models/issue.rb', line 556 def self.by_priority(project) count_and_group_by(:project => project, :field => 'priority_id', :joins => IssuePriority.table_name) end |
+ (Object) by_subproject(project)
580 581 582 583 584 585 586 587 588 589 590 591 |
# File 'app/models/issue.rb', line 580 def self.by_subproject(project) ActiveRecord::Base.connection.select_all("select s.id as status_id, s.is_closed as closed, i.project_id as project_id, count(i.id) as total from #{Issue.table_name} i, #{IssueStatus.table_name} s where i.status_id=s.id and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')}) group by s.id, s.is_closed, i.project_id") if project.descendants.active.any? end |
+ (Object) by_tracker(project)
Extracted from the ReportsController.
544 545 546 547 548 |
# File 'app/models/issue.rb', line 544 def self.by_tracker(project) count_and_group_by(:project => project, :field => 'tracker_id', :joins => Tracker.table_name) end |
+ (Object) by_version(project)
550 551 552 553 554 |
# File 'app/models/issue.rb', line 550 def self.by_version(project) count_and_group_by(:project => project, :field => 'fixed_version_id', :joins => Version.table_name) end |
+ (Object) update_versions_from_hierarchy_change(project)
Unassigns issues from versions that are no longer shared after project was moved
519 520 521 522 523 |
# File 'app/models/issue.rb', line 519 def self.update_versions_from_hierarchy_change(project) moved_project_ids = project.self_and_descendants.reload.collect(&:id) # Update issues of the moved projects and issues assigned to a version of a moved project Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids]) end |
+ (Object) update_versions_from_sharing_change(version)
Unassigns issues from version if it’s no longer shared with issue’s project
512 513 514 515 |
# File 'app/models/issue.rb', line 512 def self.update_versions_from_sharing_change(version) # Update issues assigned to the version update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id]) end |
+ (Boolean) use_field_for_done_ratio?
258 259 260 |
# File 'app/models/issue.rb', line 258 def self.use_field_for_done_ratio? Setting.issue_done_ratio == 'issue_field' end |
+ (Boolean) use_status_for_done_ratio?
254 255 256 |
# File 'app/models/issue.rb', line 254 def self.use_status_for_done_ratio? Setting.issue_done_ratio == 'issue_status' end |
Instance Method Details
- (Object) <=>(issue)
459 460 461 462 463 464 465 466 467 |
# File 'app/models/issue.rb', line 459 def <=>(issue) if issue.nil? -1 elsif root_id != issue.root_id (root_id || 0) <=> (issue.root_id || 0) else (lft || 0) <=> (issue.lft || 0) end end |
- (Object) after_initialize
81 82 83 84 85 86 87 |
# File 'app/models/issue.rb', line 81 def after_initialize if new_record? # set default values for new records only self.status ||= IssueStatus.default self.priority ||= IssuePriority.default end end |
- (Object) all_dependent_issues
409 410 411 412 413 414 415 416 |
# File 'app/models/issue.rb', line 409 def all_dependent_issues dependencies = [] relations_from.each do |relation| dependencies << relation.issue_to dependencies += relation.issue_to.all_dependent_issues end dependencies end |
- (Object) assignable_users
Users the issue can be assigned to
361 362 363 |
# File 'app/models/issue.rb', line 361 def assignable_users project.assignable_users end |
- (Object) assignable_versions
Versions that the issue can be assigned to
366 367 368 |
# File 'app/models/issue.rb', line 366 def assignable_versions @assignable_versions ||= (project..open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort end |
- (Object) attributes_with_tracker_first=(new_attributes, *args)
Overrides attributes= so that tracker_id gets assigned first
185 186 187 188 189 190 191 192 |
# File 'app/models/issue.rb', line 185 def attributes_with_tracker_first=(new_attributes, *args) return if new_attributes.nil? new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id] if new_tracker_id self.tracker_id = new_tracker_id end send :attributes_without_tracker_first=, new_attributes, *args end |
- (Object) available_custom_fields
Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
90 91 92 |
# File 'app/models/issue.rb', line 90 def available_custom_fields (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : [] end |
- (Boolean) blocked?
Returns true if this issue is blocked by another issue that is still open
371 372 373 |
# File 'app/models/issue.rb', line 371 def blocked? !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil? end |
- (Boolean) closed?
Return true if the issue is closed, otherwise false
327 328 329 |
# File 'app/models/issue.rb', line 327 def closed? self.status.is_closed? end |
- (Boolean) closing?
Return true if the issue is being closed
344 345 346 347 348 349 350 351 352 353 |
# File 'app/models/issue.rb', line 344 def closing? if !new_record? && status_id_changed? status_was = IssueStatus.find_by_id(status_id_was) status_new = IssueStatus.find_by_id(status_id) if status_was && status_new && !status_was.is_closed? && status_new.is_closed? return true end end false end |
- (Object) copy_from(arg)
94 95 96 97 98 99 100 |
# File 'app/models/issue.rb', line 94 def copy_from(arg) issue = arg.is_a?(Issue) ? arg : Issue.find(arg) self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on") self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} self.status = issue.status self end |
- (Object) css_classes
Returns a string of css classes that apply to the issue
474 475 476 477 478 479 480 481 |
# File 'app/models/issue.rb', line 474 def css_classes s = "issue status-#{status.position} priority-#{priority.position}" s << ' closed' if closed? s << ' overdue' if overdue? s << ' created-by-me' if User.current.logged? && == User.current.id s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id s end |
- (Object) done_ratio
246 247 248 249 250 251 252 |
# File 'app/models/issue.rb', line 246 def done_ratio if Issue.use_status_for_done_ratio? && status && status.default_done_ratio? status.default_done_ratio else read_attribute(:done_ratio) end end |
- (Object) due_before
Returns the due date or the target due date if any Used on gantt chart
425 426 427 |
# File 'app/models/issue.rb', line 425 def due_before due_date || (fixed_version ? fixed_version.effective_date : nil) end |
- (Object) duplicates
Returns an array of issues that duplicate this one
419 420 421 |
# File 'app/models/issue.rb', line 419 def duplicates relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from} end |
- (Object) duration
Returns the time scheduled for this issue.
Example:
Start Date: 2/26/09, End Date: 3/04/09 duration => 6
434 435 436 |
# File 'app/models/issue.rb', line 434 def duration (start_date && due_date) ? due_date - start_date : 0 end |
- (Object) estimated_hours=(h)
196 197 198 |
# File 'app/models/issue.rb', line 196 def estimated_hours=(h) write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) end |
- (Object) init_journal(user, notes = "")
315 316 317 318 319 320 321 322 323 324 |
# File 'app/models/issue.rb', line 315 def init_journal(user, notes = "") @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) @issue_before_change = self.clone @issue_before_change.status = self.status @custom_values_before_change = {} self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } # Make sure updated_on is updated when adding a note. updated_on_will_change! @current_journal end |
- (Object) move_to_project(*args)
Moves/copies an issue to a new project and tracker Returns the moved/copied issue on success, false on failure
104 105 106 107 108 |
# File 'app/models/issue.rb', line 104 def move_to_project(*args) ret = Issue.transaction do move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback) end || false end |
- (Object) move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'app/models/issue.rb', line 110 def move_to_project_without_transaction(new_project, new_tracker = nil, = {}) ||= {} issue = [:copy] ? self.class.new.copy_from(self) : self if new_project && issue.project_id != new_project.id # delete issue relations unless Setting.cross_project_issue_relations? issue.relations_from.clear issue.relations_to.clear end # issue is moved to another project # reassign to the category with same name if any new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name) issue.category = new_category # Keep the fixed_version if it's still valid in the new_project unless new_project..include?(issue.fixed_version) issue.fixed_version = nil end issue.project = new_project if issue.parent && issue.parent.project_id != issue.project_id issue.parent_issue_id = nil end end if new_tracker issue.tracker = new_tracker issue.reset_custom_values! end if [:copy] issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} issue.status = if [:attributes] && [:attributes][:status_id] IssueStatus.find_by_id([:attributes][:status_id]) else self.status end end # Allow bulk setting of attributes on the issue if [:attributes] issue.attributes = [:attributes] end if issue.save unless [:copy] # Manually update project_id on related time entries TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id}) issue.children.each do |child| unless child.move_to_project_without_transaction(new_project) # Move failed and transaction was rollback'd return false end end end else return false end issue end |
- (Object) new_statuses_allowed_to(user, include_default = false)
Returns an array of status that user is able to apply
376 377 378 379 380 381 382 |
# File 'app/models/issue.rb', line 376 def new_statuses_allowed_to(user, include_default=false) statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker) statuses << status unless statuses.empty? statuses << IssueStatus.default if include_default statuses = statuses.uniq.sort blocked? ? statuses.reject {|s| s.is_closed?} : statuses end |
- (Boolean) overdue?
Returns true if the issue is overdue
356 357 358 |
# File 'app/models/issue.rb', line 356 def overdue? !due_date.nil? && (due_date < Date.today) && !status.is_closed? end |
- (Object) parent_issue_id
535 536 537 538 539 540 541 |
# File 'app/models/issue.rb', line 535 def parent_issue_id if instance_variable_defined? :@parent_issue @parent_issue.nil? ? nil : @parent_issue.id else parent_id end end |
- (Object) parent_issue_id=(arg)
525 526 527 528 529 530 531 532 533 |
# File 'app/models/issue.rb', line 525 def parent_issue_id=(arg) parent_issue_id = arg.blank? ? nil : arg.to_i if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id) @parent_issue.id else @parent_issue = nil nil end end |
- (Object) priority_id=(pid)
172 173 174 175 |
# File 'app/models/issue.rb', line 172 def priority_id=(pid) self.priority = nil write_attribute(:priority_id, pid) end |
- (Object) recipients
Returns the mail adresses of users that should be notified
385 386 387 388 389 390 391 392 393 394 |
# File 'app/models/issue.rb', line 385 def recipients notified = project.notified_users # Author and assignee are always notified unless they have been locked notified << if && .active? notified << assigned_to if assigned_to && assigned_to.active? notified.uniq! # Remove users that can not view the issue notified.reject! {|user| !visible?(user)} notified.collect(&:mail) end |
- (Object) relations
405 406 407 |
# File 'app/models/issue.rb', line 405 def relations (relations_from + relations_to).sort end |
- (Boolean) reopened?
Return true if the issue is being reopened
332 333 334 335 336 337 338 339 340 341 |
# File 'app/models/issue.rb', line 332 def reopened? if !new_record? && status_id_changed? status_was = IssueStatus.find_by_id(status_id_was) status_new = IssueStatus.find_by_id(status_id) if status_was && status_new && status_was.is_closed? && !status_new.is_closed? return true end end false end |
- (Object) reschedule_after(date)
445 446 447 448 449 450 451 452 453 454 455 456 457 |
# File 'app/models/issue.rb', line 445 def reschedule_after(date) return if date.nil? if leaf? if start_date.nil? || start_date < date self.start_date, self.due_date = date, date + duration save end else leaves.each do |leaf| leaf.reschedule_after(date) end end end |
- (Object) safe_attributes=(attrs, user = User.current)
Safely sets attributes Should be called from controllers instead of #attributes= attr_accessible is too rough because we still want things like Issue.new(:project => foo) to work TODO: move workflow/permission checks from controllers to here
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 |
# File 'app/models/issue.rb', line 222 def safe_attributes=(attrs, user=User.current) return if attrs.nil? attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)} if attrs['status_id'] unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i) attrs.delete('status_id') end end unless leaf? attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)} end if attrs.has_key?('parent_issue_id') if !user.allowed_to?(:manage_subtasks, project) attrs.delete('parent_issue_id') elsif !attrs['parent_issue_id'].blank? attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id']) end end self.attributes = attrs end |
- (Object) save_issue_with_child_records(params, existing_time_entry = nil)
Saves an issue, time_entry, attachments, and a journal from the parameters
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 |
# File 'app/models/issue.rb', line 484 def save_issue_with_child_records(params, existing_time_entry=nil) if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project) @time_entry = existing_time_entry || TimeEntry.new @time_entry.project = project @time_entry.issue = self @time_entry.user = User.current @time_entry.spent_on = Date.today @time_entry.attributes = params[:time_entry] self.time_entries << @time_entry end if valid? = Attachment.attach_files(self, params[:attachments]) [:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} # TODO: Rename hook Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) if save # TODO: Rename hook Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) return true end end # failure, returns false end |
- (Object) soonest_start
438 439 440 441 442 443 |
# File 'app/models/issue.rb', line 438 def soonest_start @soonest_start ||= ( relations_to.collect{|relation| relation.successor_soonest_start} + ancestors.collect(&:soonest_start) ).compact.max end |
- (Object) spent_hours
Returns the total number of hours spent on this issue and its descendants
Example:
spent_hours => 0.0 spent_hours => 50.2
401 402 403 |
# File 'app/models/issue.rb', line 401 def spent_hours @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0 end |
- (Object) status_id=(sid)
167 168 169 170 |
# File 'app/models/issue.rb', line 167 def status_id=(sid) self.status = nil write_attribute(:status_id, sid) end |
- (Object) to_s
469 470 471 |
# File 'app/models/issue.rb', line 469 def to_s "#{tracker} ##{id}: #{subject}" end |
- (Object) tracker_id=(tid)
177 178 179 180 181 182 |
# File 'app/models/issue.rb', line 177 def tracker_id=(tid) self.tracker = nil result = write_attribute(:tracker_id, tid) @custom_field_values = nil result end |
- (Object) update_done_ratio_from_issue_status
Set the done_ratio using the status if that setting is set. This will keep the done_ratios even if the user turns off the setting later
309 310 311 312 313 |
# File 'app/models/issue.rb', line 309 def update_done_ratio_from_issue_status if Issue.use_status_for_done_ratio? && status && status.default_done_ratio? self.done_ratio = status.default_done_ratio end end |
- (Object) validate
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'app/models/issue.rb', line 262 def validate if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? errors.add :due_date, :not_a_date end if self.due_date and self.start_date and self.due_date < self.start_date errors.add :due_date, :greater_than_start_date end if start_date && soonest_start && start_date < soonest_start errors.add :start_date, :invalid end if fixed_version if !assignable_versions.include?(fixed_version) errors.add :fixed_version_id, :inclusion elsif reopened? && fixed_version.closed? errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version) end end # Checks that the issue can not be added/moved to a disabled tracker if project && (tracker_id_changed? || project_id_changed?) unless project.trackers.include?(tracker) errors.add :tracker_id, :inclusion end end # Checks parent issue assignment if @parent_issue if @parent_issue.project_id != project_id errors.add :parent_issue_id, :not_same_project elsif !new_record? # moving an existing issue if @parent_issue.root_id != root_id # we can always move to another tree elsif move_possible?(@parent_issue) # move accepted inside tree else errors.add :parent_issue_id, :not_a_valid_parent end end end end |
- (Boolean) visible?(usr = nil)
Returns true if usr or current user is allowed to view the issue
77 78 79 |
# File 'app/models/issue.rb', line 77 def visible?(usr=nil) (usr || User.current).allowed_to?(:view_issues, self.project) end |