Mercurial > hg > soundsoftware-site
diff app/models/issue.rb @ 1295:622f24f53b42 redmine-2.3
Update to Redmine SVN revision 11972 on 2.3-stable branch
author | Chris Cannam |
---|---|
date | Fri, 14 Jun 2013 09:02:21 +0100 |
parents | 3e4c3460b6ca |
children | 4f746d8966dd |
line wrap: on
line diff
--- a/app/models/issue.rb Fri Jun 14 09:01:12 2013 +0100 +++ b/app/models/issue.rb Fri Jun 14 09:02:21 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 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 @@ -67,24 +67,31 @@ validates_length_of :subject, :maximum => 255 validates_inclusion_of :done_ratio, :in => 0..100 - validates_numericality_of :estimated_hours, :allow_nil => true + validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid} + validates :start_date, :date => true + validates :due_date, :date => true validate :validate_issue, :validate_required_fields - scope :visible, - lambda {|*args| { :include => :project, - :conditions => Issue.visible_condition(args.shift || User.current, *args) } } + scope :visible, lambda {|*args| + includes(:project).where(Issue.visible_condition(args.shift || User.current, *args)) + } scope :open, lambda {|*args| is_closed = args.size > 0 ? !args.first : false - {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status} + includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed) } - scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC" - scope :on_active_project, :include => [:status, :project, :tracker], - :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] + scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") } + scope :on_active_project, lambda { + includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE) + } + scope :fixed_version, lambda {|versions| + ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v} + ids.any? ? where(:fixed_version_id => ids) : where('1=0') + } before_create :default_assign - before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change + before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?} after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal # Should be after_create but would be called before previous after_save callbacks @@ -133,6 +140,11 @@ end end + # Returns true if user or current user is allowed to edit or add a note to the issue + def editable?(user=User.current) + user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project) + end + def initialize(attributes=nil, *args) super if new_record? @@ -143,6 +155,13 @@ end end + def create_or_update + super + ensure + @status_was = nil + end + private :create_or_update + # AR#Persistence#destroy would raise and RecordNotFound exception # if the issue was already deleted or updated (non matching lock_version). # This is a problem when bulk deleting issues or deleting a project @@ -165,10 +184,12 @@ super end + alias :base_reload :reload def reload(*args) @workflow_rule_by_attribute = nil @assignable_versions = nil - super + @relations = nil + base_reload(*args) end # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields @@ -526,14 +547,6 @@ end def validate_issue - if due_date.nil? && @attributes['due_date'].present? - errors.add :due_date, :not_a_date - end - - if start_date.nil? && @attributes['start_date'].present? - errors.add :start_date, :not_a_date - end - if due_date && start_date && due_date < start_date errors.add :due_date, :greater_than_start_date end @@ -563,6 +576,8 @@ elsif @parent_issue if !valid_parent_project?(@parent_issue) errors.add :parent_issue_id, :invalid + elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self)) + errors.add :parent_issue_id, :invalid elsif !new_record? # moving an existing issue if @parent_issue.root_id != root_id @@ -633,6 +648,14 @@ scope end + # Returns the initial status of the issue + # Returns nil for a new issue + def status_was + if status_id_was && status_id_was.to_i > 0 + @status_was ||= IssueStatus.find_by_id(status_id_was) + end + end + # Return true if the issue is closed, otherwise false def closed? self.status.is_closed? @@ -653,9 +676,7 @@ # Return true if the issue is being closed 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? + if status_was && status && !status_was.is_closed? && status.is_closed? return true end end @@ -785,7 +806,7 @@ end def relations - @relations ||= IssueRelations.new(self, (relations_from + relations_to).sort) + @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort) end # Preloads relations for a collection of issues @@ -822,7 +843,7 @@ relations_from.select {|relation| relation.issue_from_id == issue.id} + relations_to.select {|relation| relation.issue_to_id == issue.id} - issue.instance_variable_set "@relations", IssueRelations.new(issue, relations.sort) + issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort) end end end @@ -832,14 +853,18 @@ IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id]) end + # Returns all the other issues that depend on the issue def all_dependent_issues(except=[]) except << self dependencies = [] - relations_from.each do |relation| - if relation.issue_to && !except.include?(relation.issue_to) - dependencies << relation.issue_to - dependencies += relation.issue_to.all_dependent_issues(except) - end + dependencies += relations_from.map(&:issue_to) + dependencies += children unless leaf? + dependencies.compact! + dependencies -= except + dependencies += dependencies.map {|issue| issue.all_dependent_issues(except)}.flatten + if parent + dependencies << parent + dependencies += parent.all_dependent_issues(except + parent.descendants) end dependencies end @@ -873,7 +898,7 @@ @soonest_start = nil if reload @soonest_start ||= ( relations_to(reload).collect{|relation| relation.successor_soonest_start} + - ancestors.collect(&:soonest_start) + [(@parent_issue || parent).try(:soonest_start)] ).compact.max end @@ -936,7 +961,7 @@ # Returns a string of css classes that apply to the issue def css_classes - s = "issue status-#{status_id} #{priority.try(:css_classes)}" + s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}" s << ' closed' if closed? s << ' overdue' if overdue? s << ' child' if child? @@ -1005,8 +1030,8 @@ end end - # Returns true if issue's project is a valid - # parent issue project + # Returns true if issue's project is a valid + # parent issue project def valid_parent_project?(issue=parent) return true if issue.nil? || issue.project_id == project_id @@ -1128,20 +1153,27 @@ end unless @copied_from.leaf? || @copy_options[:subtasks] == false - @copied_from.children.each do |child| + copy_options = (@copy_options || {}).merge(:subtasks => false) + copied_issue_ids = {@copied_from.id => self.id} + @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child| + # Do not copy self when copying an issue as a descendant of the copied issue + next if child == self + # Do not copy subtasks of issues that were not copied + next unless copied_issue_ids[child.parent_id] + # Do not copy subtasks that are not visible to avoid potential disclosure of private data unless child.visible? - # Do not copy subtasks that are not visible to avoid potential disclosure of private data logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger next end - copy = Issue.new.copy_from(child, @copy_options) + copy = Issue.new.copy_from(child, copy_options) copy.author = author copy.project = project - copy.parent_issue_id = id - # Children subtasks are copied recursively + copy.parent_issue_id = copied_issue_ids[child.parent_id] unless copy.save logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger + next end + copied_issue_ids[child.id] = copy.id end end @after_create_from_copy_handled = true @@ -1303,10 +1335,23 @@ end end - # Make sure updated_on is updated when adding a note + # Make sure updated_on is updated when adding a note and set updated_on now + # so we can set closed_on with the same value on closing def force_updated_on_change - if @current_journal + if @current_journal || changed? self.updated_on = current_time_from_proper_timezone + if new_record? + self.created_on = updated_on + end + end + end + + # Callback for setting closed_on when the issue is closed. + # The closed_on attribute stores the time of the last closing + # and is preserved when the issue is reopened. + def update_closed_on + if closing? || (new_record? && closed?) + self.closed_on = updated_on end end @@ -1316,7 +1361,7 @@ if @current_journal # attributes changes if @attributes_before_change - (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c| + (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c| before = @attributes_before_change[c] after = send(c) next if before == after || (before.blank? && after.blank?)