Undev/redmine-stuff-to-do-plugin

View on GitHub
app/models/stuff_to_do.rb

Summary

Maintainability
A
0 mins
Test Coverage
# StuffToDo relates a user to another object at a specific position
# in a list.
#
# Supported objects:
# * Issue
# * Project
class StuffToDo < ActiveRecord::Base
  USE = {
    'All' => '0',
    'Only Issues' => '1',
    'Only Projects' => '2'
  }

  belongs_to :stuff, :polymorphic => true
  belongs_to :user
  acts_as_list :scope => :user

  scope :doing_now, lambda { |user|
    {
      :conditions => { :user_id => user.id },
      :limit => 5,
      :order => 'position ASC'
    }
  }

  # TODO: Rails bug
  #
  # ActiveRecord ignores :offset if :limit isn't added also.  But since we
  # want all the records, we need to provide a limit that will include everything
  #
  # http://dev.rubyonrails.org/ticket/7257
  #
  scope :recommended, lambda { |user|
    {
      :conditions => { :user_id => user.id },
      :limit => self.count,
      :offset => 5,
      :order => 'position ASC'
    }
  }

  # Filters the issues that are available to be added for a user.
  #
  # A filter can be a record:
  #
  # * User - issues are assigned to this user
  # * IssueStatus - issues with this status
  # * IssuePriority - issues with this priority
  #
  def self.available(user, filter=nil)
    return [] if filter.blank?

    if filter.is_a?(Project)
      potential_stuff_to_do = active_and_visible_projects.sort
    else
      potential_stuff_to_do = Issue.
          includes([:status, :priority, :project]).
          where(conditions_for_available(filter)).
          order("#{Issue.table_name}.updated_on DESC")
    end

    stuff_to_do = StuffToDo.where(:user_id => user.id).collect(&:stuff)

    return potential_stuff_to_do - stuff_to_do
  end

  def self.using_projects_as_items?
    ['All', 'Only Projects'].include?(use_setting)
  end

  def self.using_issues_as_items?
    ['All', 'Only Issues'].include?(use_setting)
  end

  # Callback used to destroy all StuffToDos when an object is removed and
  # send an email if a user is below the What's Recommend threshold
  def self.remove_associations_to(associated_object)
    user_ids = []
    associated_object.stuff_to_dos.each do |stuff_to_do|
      user_ids << stuff_to_do.user_id if stuff_to_do.user_id
      stuff_to_do.destroy
    end

    # Deliver an email for each user who is below the threshold
    user_ids.uniq.each do |user_id|
      count = self.count(:conditions => { :user_id => user_id})
      threshold = Setting.plugin_stuff_to_do_plugin['threshold']

      if threshold && threshold.to_i >= count
        user = User.find_by_id(user_id)
        StuffToDoMailer.deliver_recommended_below_threshold(user, count)
      end
    end

    return true
  end

  # Destroys all +NextIssues+ on an +issue+ that are not the assigned to user
  def self.remove_stale_assignments(issue)
    if issue.assigned_to_id.nil?
      self.destroy_all(['stuff_id = (?)', issue.id])
    else
      self.destroy_all(['stuff_id = (?) AND user_id NOT IN (?)',
                             issue.id,
                             issue.assigned_to_id])
    end
  end

  # Reorders the list of StuffToDo items for +user+ to be in the order of
  # +ids+.  New StuffToDos will be created if needed and old
  # StuffToDos will be removed if they are unassigned.
  #
  # Project based ids need to be prefixed with +project+
  def self.reorder_list(user, ids)
    ids ||= []
    id_position_mapping = {}
    ids.each_with_index { |e, i| id_position_mapping[i] = e }

    issue_ids = {}
    project_ids = {}

    id_position_mapping.each do |key,value|
      if value.match(/project/i)
        project_ids[key] = value.sub(/project/i,'').to_i
      else
        issue_ids[key] = value.to_i
      end
    end

    reorder_issues(user, issue_ids)
    reorder_projects(user, project_ids)
  end

  private

  def self.reorder_issues(user, issue_ids)
    reorder_items('Issue', user, issue_ids)
  end

  def self.reorder_projects(user, project_ids)
    reorder_items('Project', user, project_ids)
  end

  def self.reorder_items(type, user, ids)
    list = self.find_all_by_user_id_and_stuff_type(user.id, type)
    stuff_to_dos_found = list.collect { |std| std.stuff_id.to_i }

    remove_missing_records(user, stuff_to_dos_found, ids.values)

    ids.each do |position, id|
      if existing_list_position = stuff_to_dos_found.index(id.to_i)
        position = position + 1  # acts_as_list is 1 based
        stuff_to_do = list[existing_list_position]
        stuff_to_do.insert_at(position)
      else
        # Not found in list, so create a new StuffToDo item
        stuff_to_do = self.new
        stuff_to_do.stuff_id = id
        stuff_to_do.stuff_type = type
        stuff_to_do.user_id = user.id

        stuff_to_do.save # TODO: Check return

        # Have to resave next_issue since acts_as_list automatically moves it
        # to the bottom on create
        stuff_to_do.insert_at(position + 1)  # acts_as_list is 1 based
      end
    end

  end

  # Destroys saved records that are +ids_found_in_database+ but are
  # not in +ids_to_use+
  def self.remove_missing_records(user, ids_found_in_database, ids_to_use)
    removed = ids_found_in_database - ids_to_use
    removed.each do |id|
      removed_stuff_to_do = self.find_by_user_id_and_stuff_id(user.id, id)
      removed_stuff_to_do.destroy
    end
  end

  # Redmine 0.8.x compatibility method.
  def self.active_and_visible_projects
    if ::Project.respond_to?(:active) && ::Project.respond_to?(:visible)
      return ::Project.active.visible
    else
      return ::Project.find(:all, :conditions => Project.visible_by)
    end
  end

  def self.use_setting
    USE.key(Setting.plugin_stuff_to_do_plugin['use_as_stuff_to_do'])
  end

  def self.conditions_for_available(filter_by)
    conditions_builder = ARCondition.new(["#{IssueStatus.table_name}.is_closed = ?", false ])
    conditions_builder.add(["#{Project.table_name}.status = ?", Project::STATUS_ACTIVE])

    case
    when filter_by.is_a?(User)
      conditions_builder.add(["assigned_to_id = ?", filter_by.id])
    when filter_by.is_a?(IssueStatus), filter_by.is_a?(Enumeration)
      table_name = filter_by.class.table_name
      conditions_builder.add(["#{table_name}.id = (?)", filter_by.id])
    end

    conditions_builder.conditions
  end
end