ari/jobsworth

View on GitHub
app/models/task_record.rb

Summary

Maintainability
C
1 day
Test Coverage
# encoding: UTF-8
# A task
#
# Belongs to a project, milestone, creator
# Has many tags, users (through task_owners), tags (through task_tags),
#   dependencies (tasks which should be done before this one) and
#   dependants (tasks which should be done after this one),
#   todos, and sheets
#
class TaskRecord < AbstractTask
  has_many :property_values, :through => :task_property_values

  scope :from_this_year, lambda { where('created_at > ?', Time.zone.now.beginning_of_year - 1.month) }
  scope :open_only, -> { where(:status => 0) }
  scope :not_snoozed, -> { where('weight IS NOT NULL') }
  after_validation :fix_work_log_error

  after_save do |r|
    r.delay.calculate_dependants_score if r.status_changed?

    r.ical_entry.destroy if r.ical_entry
    project = r.project
    project.update_project_stats
    project.save

    if r.project.id != r.project_id
      #Task has changed projects, update counts of target project as well
      p = Project.find(r.project_id)
      p.update_project_stats
      p.save
    end

    if r.milestone
      r.milestone.update_counts
      r.milestone.update_status
    end
  end

  before_save :calculate_score

  def snoozed?
    !self.dependencies.reject { |t| t.done? }.empty? or
        self.wait_for_customer or
        (!self.hide_until.nil? and self.hide_until > Time.now.utc) or
        (!self.milestone.nil? and self.milestone.status_name == :planning)
  end

  def self.expire_hide_until
    TaskRecord.where('hide_until IS NOT NULL').all.each do |task|
      if task.hide_until < Time.now.utc
        task.update_attribute :hide_until, nil
      end
    end
  end

  def worked_on?
    self.sheets.size > 0
  end

  def recalculate_worked_minutes
    self.worked_minutes = WorkLog.where('task_id = ?', self.id).sum(:duration).to_i
  end

  def recalculate_worked_minutes!
    recalculate_worked_minutes and save
  end

  def reopen!
    update_attributes :completed_at => nil,
                      :status => self.class.status_types.index('Open')
  end

  def minutes_left
    d = self.adjusted_duration - self.worked_minutes
    d = self.default_duration.to_i if d < 0
    d
  end

  def adjusted_duration
    self.duration > 0 ? self.duration : self.default_duration
  end

  def overworked?
    (self.adjusted_duration - self.worked_minutes) < 0
  end

  def self.search(user, keys, opts = {})
    tf = TaskFilter.new(:user => user)

    conditions = []
    keys.each do |k|
      conditions << "tasks.task_num = #{ k.to_i }"
    end
    name_conds = Search.search_conditions_for(keys, ['tasks.name'], :search_by_id => false)
    conditions << name_conds[1...-1] # strip off surounding parentheses

    conditions = "(#{ conditions.join(' or ') })"
    if opts[:status_in].present?
      conditions = "(#{conditions} AND tasks.status IN (#{Array.wrap(opts[:status_in]).map(&:to_i).join(',')}))"
    end
    return tf.tasks(conditions)
  end

  def csv_header
    ['Client', 'Project', 'Num', 'Name', 'Tags', 'User', 'Milestone', 'Due', 'Created', 'Completed', 'Worked', 'Estimated', 'Resolution'] +
        company.properties.collect { |property| property.name }
  end

  def to_csv
    [customers.uniq.map { |c| c.name }.join(','), project.name, task_num, name, tags.collect(&:name).join(','), owners_to_display, milestone.nil? ? nil : milestone.name, self.due_date, created_at, completed_at, worked_minutes, duration, status_type] +
        company.properties.collect { |property| property_value(property).to_s }
  end

  ###
  # This method return value of property named "Type"
  ###
  def type
    property_value(company.type_property)
  end

  ###
  # Returns an int to use for sorting this task. See Company.rank_by_properties
  # for more info.
  ###
  def sort_rank
    @sort_rank ||= company.rank_by_properties(self)
  end

  ###
  # A task is critical if it is in the top 20% of the possible
  # ranking using the companys sort.
  ###
  def critical?
    return false if company.maximum_sort_rank == 0

    sort_rank.to_f / company.maximum_sort_rank.to_f > 0.80
  end

  ###
  # A task is normal if it is not critical or low.
  ###
  def normal?
    !critical? and !low?
  end

  ###
  # A task is low if it is in the bottom 20% of the possible
  # ranking using the companys sort.
  ###
  def low?
    return false if company.maximum_sort_rank == 0

    sort_rank.to_f / company.maximum_sort_rank.to_f < 0.20
  end

  def users_to_notify(user_who_made_change=nil)
    if user_who_made_change and !user_who_made_change.receive_own_notifications?
      recipients= self.users.active.where('users.id != ? and users.receive_notifications = ?', user_who_made_change.id || 0, true)
    else
      recipients= self.users.active.where(:receive_notifications => true)
      recipients<< user_who_made_change unless user_who_made_change.nil? or user_who_made_change.id.nil? or recipients.include?(user_who_made_change)
    end
    recipients
  end

  ###
  # This method will mark this task as unread for any
  # setup watchers or task owners.
  # The exclude param should be a user which unread
  # status will not be updated. For example, the person who wrote a
  # comment should probably be excluded.
  ###
  def mark_as_unread(exclude = '')
    exclude = ['user_id !=?', exclude.id] if exclude.is_a?(User)
    self.task_users.where(exclude).update_all(:unread => true)
  end

  def self.public_comments_for(task)
    customer_ids = task.customers.collect { |customer| customer.id }.join(', ')
    WorkLog.comments.
        where('customer_id in (?)', customer_ids).
        order('started_at DESC')
  end

  ###
  # Sets this task as read for user.
  # If read is passed, and false, sets the task
  # as unread for user.
  ###
  def set_task_read(user, read = true)
    self.task_users.where(:user_id => user.id).update_all(:unread => !read)
  end

  ###
  # Returns true if this task is marked as unread for user.
  ###
  def unread?(user)
    unread = false

    user_notifications = self.task_users.includes(:user).select { |n| n.user == user }
    user_notifications.each do |n|
      unread ||= n.unread?
    end

    return unread
  end


  # return a users mapped to the duration of time they have worked on this task
  def user_work
    @user_work ||= work_logs.duration_per_user.inject({}) do |memo, l|
      memo[l._user_] = l.duration if l._user_ && l.duration.to_i > 0
      memo
    end
  end

  def update_group(user, group, value, icon = nil)
    if group == 'milestone'
      val_arr = value.split('/')
      task_project = user.projects.find_by(:name => val_arr[0])
      if user.can?(task_project, 'milestone')
        pid = task_project.id
        if val_arr.size == 1
          self.milestone_id = nil
        else
          mid = Milestone.order('completed_at').where('company_id = ? AND project_id = ? AND LTRIM(name) = ?', user.company.id, pid, val_arr[1].strip).first.id
          self.milestone_id = mid
        end
        self.project_id = pid
        save
      end
    elsif group == 'resolution' && user.can?(self.project, 'close')
      status = TaskRecord::MAX_STATUS
      self.statuses_for_select_list.each do |arr|
        status = arr[1] if arr[0] == value
      end
      self.status = status
      save
    elsif (prop = Property.find_by(:company_id => user.company_id, :name => group.camelize))
      if !value.blank?
        pv = PropertyValue.find_by(:value => value, :property_id => prop.id)
      elsif !icon.blank?
        icon = icon.split('?')[0]
        pv = PropertyValue.find_by(:icon_url => icon, :property_id => prop.id)
      end
      #prevent duplicate entry when user dragging task to same group
      if TaskPropertyValue.find_by(:task_id => self.id, :property_id => prop.id).try(:property_value_id) != pv.id
        self.set_property_value(prop, pv)
      end
    end
  end

  def score_rules
    score_rules = []

    # Query scores only if company is using score rules
    if company.use_score_rules?
      score_rules.concat(project.score_rules) if self.project
      score_rules.concat(company.score_rules) if self.company
      score_rules.concat(milestone.score_rules) if self.milestone

      customers.each do |customer|
        score_rules.concat(customer.score_rules)
      end

      property_values.each do |property_value|
        score_rules.concat(property_value.score_rules)
      end
    end

    score_rules
  end

  def calculate_score
    if self.closed?
      self.weight = 0
      return
    end

    # If the task is snozzed, score should be nil
    if self.snoozed?
      self.weight = nil
      return
    end

    all_score_rules = score_rules

    if all_score_rules.empty?
      self.weight = self.weight_adjustment
    else
      self.weight = all_score_rules.inject(self.weight_adjustment) do |result, score_rule|
        result + score_rule.calculate_score_for(self)
      end
    end
  end

  def self.calculate_score
    TaskRecord.open_only.each do |task|
      task.save(:validate => false)
    end
  end

  def calculate_dependants_score
    self.dependants.each do |t|
      t.calculate_score
      t.save
    end
  end

  # If creating a new work log with a duration, fails because it work log
  # has a mandatory attribute missing, the error message it the unhelpful
  # "Work logs in invalid". Fix that here
  def fix_work_log_error
    if errors.include?('work_logs')
      errors.delete('work_logs')
      self.work_logs.last.errors.each_full do |msg|
        self.errors.add(:base, msg)
      end
    end
  end

  def self.reschedule_open_tasks
    User.includes(:work_plan).find_each do |user|
      user.schedule_tasks(save: true)
    end
  end

end

# == Schema Information
#
# Table name: tasks
#
#  id                 :integer(4)      not null, primary key
#  name               :string(200)     default(""), not null
#  project_id         :integer(4)      default(0), not null
#  position           :integer(4)      default(0), not null
#  created_at         :datetime        not null
#  due_at             :datetime
#  updated_at         :datetime        not null
#  completed_at       :datetime
#  duration           :integer(4)      default(1)
#  hidden             :integer(4)      default(0)
#  milestone_id       :integer(4)
#  description        :text
#  company_id         :integer(4)
#  priority           :integer(4)      default(0)
#  updated_by_id      :integer(4)
#  severity_id        :integer(4)      default(0)
#  type_id            :integer(4)      default(0)
#  task_num           :integer(4)      default(0)
#  status             :integer(4)      default(0)
#  creator_id         :integer(4)
#  hide_until         :datetime
#  worked_minutes     :integer(4)      default(0)
#  type               :string(255)     default("Task")
#  weight             :integer(4)      default(0)
#  weight_adjustment  :integer(4)      default(0)
#  wait_for_customer  :boolean(1)      default(FALSE)
#
# Indexes
#
#  index_tasks_on_type_and_task_num_and_company_id  (type,task_num,company_id) UNIQUE
#  tasks_company_id_index                           (company_id)
#  tasks_due_at_idx                                 (due_at)
#  index_tasks_on_milestone_id                      (milestone_id)
#  tasks_project_completed_index                    (project_id,completed_at)
#  tasks_project_id_index                           (project_id,milestone_id)
#