fatfreecrm/fat_free_crm

View on GitHub
app/models/polymorphic/task.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# frozen_string_literal: true

# Copyright (c) 2008-2013 Michael Dvorkin and contributors.
#
# Fat Free CRM is freely distributable under the terms of MIT license.
# See MIT-LICENSE file or http://www.opensource.org/licenses/mit-license.php
#------------------------------------------------------------------------------
# == Schema Information
#
# Table name: tasks
#
#  id              :integer         not null, primary key
#  user_id         :integer
#  assigned_to     :integer
#  completed_by    :integer
#  name            :string(255)     default(""), not null
#  asset_id        :integer
#  asset_type      :string(255)
#  priority        :string(32)
#  category        :string(32)
#  bucket          :string(32)
#  due_at          :datetime
#  completed_at    :datetime
#  deleted_at      :datetime
#  created_at      :datetime
#  updated_at      :datetime
#  background_info :string(255)
#

class Task < ActiveRecord::Base
  include ActiveModel::Serializers::Xml

  attr_accessor :calendar

  ALLOWED_VIEWS = %w[pending assigned completed]

  belongs_to :user
  belongs_to :assignee, class_name: "User", foreign_key: :assigned_to, optional: true # TODO: Is this really optional?
  belongs_to :completor, class_name: "User", foreign_key: :completed_by, optional: true # TODO: Is this really optional?
  belongs_to :asset, polymorphic: true, optional: true # TODO: Is this really optional?

  serialize :subscribed_users, Array

  # Tasks created by the user for herself, or assigned to her by others. That's
  # what gets shown on Tasks/Pending and Tasks/Completed pages.
  scope :my, lambda { |*args|
    options = args[0] || {}
    user_option = (options.is_a?(Hash) ? options[:user] : options) || User.current_user
    includes(:assignee)
      .where('(user_id = ? AND assigned_to IS NULL) OR assigned_to = ?', user_option, user_option)
      .order(options[:order] || 'name ASC')
      .limit(options[:limit]) # nil selects all records
  }

  scope :created_by,  ->(user) { where(user_id: user.id) }
  scope :assigned_to, ->(user) { where(assigned_to: user.id) }

  # Tasks assigned by the user to others. That's what we see on Tasks/Assigned.
  scope :assigned_by, lambda { |user|
    includes(:assignee)
      .where('user_id = ? AND assigned_to IS NOT NULL AND assigned_to != ?', user.id, user.id)
  }

  # Tasks created by the user or assigned to the user, i.e. the union of the two
  # scopes above. That's the tasks the user is allowed to see and track.
  scope :tracked_by, lambda { |user|
    includes(:assignee)
      .where('user_id = ? OR assigned_to = ?', user.id, user.id)
  }

  # Show tasks which either belong to the user and are unassigned, or are assigned to the user
  scope :visible_on_dashboard, lambda { |user|
    where('(user_id = :user_id AND assigned_to IS NULL) OR assigned_to = :user_id', user_id: user.id).where('completed_at IS NULL')
  }

  scope :by_due_at, lambda {
    order({
      "MySQL"      => "due_at NOT NULL, due_at ASC",
      "PostgreSQL" => "due_at ASC NULLS FIRST"
    }[ActiveRecord::Base.connection.adapter_name] || :due_at)
  }

  # Status based scopes to be combined with the due date and completion time.
  scope :pending,       -> { where('completed_at IS NULL').order('tasks.due_at, tasks.id') }
  scope :assigned,      -> { where('completed_at IS NULL AND assigned_to IS NOT NULL').order('tasks.due_at, tasks.id') }
  scope :completed,     -> { where('completed_at IS NOT NULL').order('tasks.completed_at DESC') }

  # Due date scopes.
  scope :due_asap,      -> { where("due_at IS NULL AND bucket = 'due_asap'").order('tasks.id DESC') }
  scope :overdue,       -> { where('due_at IS NOT NULL AND due_at < ?', Time.zone.now.midnight.utc).order('tasks.id DESC') }
  scope :due_today,     -> { where('due_at >= ? AND due_at < ?', Time.zone.now.midnight.utc, Time.zone.now.midnight.tomorrow.utc).order('tasks.id DESC') }
  scope :due_tomorrow,  -> { where('due_at >= ? AND due_at < ?', Time.zone.now.midnight.tomorrow.utc, Time.zone.now.midnight.tomorrow.utc + 1.day).order('tasks.id DESC') }
  scope :due_this_week, -> { where('due_at >= ? AND due_at < ?', Time.zone.now.midnight.tomorrow.utc + 1.day, Time.zone.now.next_week.utc).order('tasks.id DESC') }
  scope :due_next_week, -> { where('due_at >= ? AND due_at < ?', Time.zone.now.next_week.utc, Time.zone.now.next_week.end_of_week.utc + 1.day).order('tasks.id DESC') }
  scope :due_later,     -> { where("(due_at IS NULL AND bucket = 'due_later') OR due_at >= ?", Time.zone.now.next_week.end_of_week.utc + 1.day).order('tasks.id DESC') }

  # Completion time scopes.
  scope :completed_today,      -> { where('completed_at >= ? AND completed_at < ?', Time.zone.now.midnight.utc, Time.zone.now.midnight.tomorrow.utc) }
  scope :completed_yesterday,  -> { where('completed_at >= ? AND completed_at < ?', Time.zone.now.midnight.yesterday.utc, Time.zone.now.midnight.utc) }
  scope :completed_this_week,  -> { where('completed_at >= ? AND completed_at < ?', Time.zone.now.beginning_of_week.utc, Time.zone.now.midnight.yesterday.utc) }
  scope :completed_last_week,  -> { where('completed_at >= ? AND completed_at < ?', Time.zone.now.beginning_of_week.utc - 7.days, Time.zone.now.beginning_of_week.utc) }
  scope :completed_this_month, -> { where('completed_at >= ? AND completed_at < ?', Time.zone.now.beginning_of_month.utc, Time.zone.now.beginning_of_week.utc - 7.days) }
  scope :completed_last_month, -> { where('completed_at >= ? AND completed_at < ?', (Time.zone.now.beginning_of_month.utc - 1.day).beginning_of_month.utc, Time.zone.now.beginning_of_month.utc) }

  scope :text_search, lambda { |query|
    query = query.gsub(/[^\w\s\-\.'\p{L}]/u, '').strip
    where('upper(name) LIKE upper(?)', "%#{query}%")
  }

  acts_as_commentable
  has_paper_trail versions: { class_name: 'Version' }, meta: { related: :asset },
                  ignore: [:subscribed_users]
  has_fields
  exportable

  validates_presence_of :user
  validates_presence_of :name, message: :missing_task_name
  validates_presence_of :calendar, if: -> { bucket == 'specific_time' && !completed_at }
  validate :specific_time, unless: :completed?

  before_create :set_due_date
  before_update :set_due_date, unless: :completed?
  before_save :notify_assignee

  # Matcher for the :my named scope.
  #----------------------------------------------------------------------------
  def my?(user)
    (self.user == user && assignee.nil?) || assignee == user
  end

  # Matcher for the :assigned_by named scope.
  #----------------------------------------------------------------------------
  def assigned_by?(user)
    self.user == user && assignee && assignee != user
  end

  #----------------------------------------------------------------------------
  def completed?
    !!completed_at
  end

  # Matcher for the :tracked_by? named scope.
  #----------------------------------------------------------------------------
  def tracked_by?(user)
    self.user == user || assignee == user
  end

  # Check whether the due date has specific time ignoring 23:59:59 timestamp
  # set by Time.now.end_of_week.
  #----------------------------------------------------------------------------
  def at_specific_time?
    due_at.present? && !due_end_of_day? && !due_beginning_of_day?
  end

  # Convert specific due_date to "due_today", "due_tomorrow", etc. bucket name.
  #----------------------------------------------------------------------------
  def computed_bucket
    return bucket if bucket != "specific_time"

    if overdue?
      "overdue"
    elsif due_today?
      "due_today"
    elsif due_tomorrow?
      "due_tomorrow"
    elsif due_this_week? && !due_today? && !due_tomorrow?
      "due_this_week"
    elsif due_next_week?
      "due_next_week"
    else
      "due_later"
    end
  end

  # Returns list of tasks grouping them by due date as required by tasks/index.
  #----------------------------------------------------------------------------
  def self.find_all_grouped(user, view)
    return {} unless ALLOWED_VIEWS.include?(view)

    settings = (view == "completed" ? Setting.task_completed : Setting.task_bucket)
    Hash[
      settings.map do |key, _value|
        [key, view == "assigned" ? assigned_by(user).send(key).pending : my(user).send(key).send(view)]
      end
    ]
  end

  # Returns bucket if it's empty (i.e. we have to hide it), nil otherwise.
  #----------------------------------------------------------------------------
  def self.bucket_empty?(bucket, user, view = "pending")
    return false if bucket.blank? || !ALLOWED_VIEWS.include?(view)
    return false unless Setting.task_bucket.map(&:to_s).include?(bucket.to_s)

    if view == "assigned"
      assigned_by(user).send(bucket).pending.count
    else
      my(user).send(bucket).send(view).count
    end == 0
  end

  # Returns task totals for each of the views as needed by tasks sidebar.
  #----------------------------------------------------------------------------
  def self.totals(user, view = "pending")
    return {} unless ALLOWED_VIEWS.include?(view)

    settings = (view == "completed" ? Setting.task_completed : Setting.task_bucket)
    settings.each_with_object(HashWithIndifferentAccess[all: 0]) do |key, hash|
      hash[key] = (view == "assigned" ? assigned_by(user).send(key).pending.count : my(user).send(key).send(view).count)
      hash[:all] += hash[key]
      hash
    end
  end

  private

  #----------------------------------------------------------------------------
  def set_due_date
    self.due_at = case bucket
                  when "overdue"
                    due_at || Time.zone.now.midnight.yesterday
                  when "due_today"
                    Time.zone.now.midnight
                  when "due_tomorrow"
                    Time.zone.now.midnight.tomorrow
                  when "due_this_week"
                    Time.zone.now.end_of_week
                  when "due_next_week"
                    Time.zone.now.next_week.end_of_week
                  when "due_later"
                    Time.zone.now.midnight + 100.years
                  when "specific_time"
                    calendar ? parse_calendar_date : nil
    end
  end

  #----------------------------------------------------------------------------
  def due_end_of_day?
    due_at.present? && (due_at.change(usec: 0) == due_at.end_of_day.change(usec: 0))
  end

  #----------------------------------------------------------------------------
  def due_beginning_of_day?
    due_at.present? && (due_at == due_at.beginning_of_day)
  end

  #----------------------------------------------------------------------------
  def overdue?
    due_at < Time.zone.now.midnight
  end

  #----------------------------------------------------------------------------
  def due_today?
    due_at.between?(Time.zone.now.midnight, Time.zone.now.end_of_day)
  end

  #----------------------------------------------------------------------------
  def due_tomorrow?
    due_at.between?(Time.zone.now.midnight.tomorrow, Time.zone.now.tomorrow.end_of_day)
  end

  #----------------------------------------------------------------------------
  def due_this_week?
    due_at.between?(Time.zone.now.beginning_of_week, Time.zone.now.end_of_week)
  end

  #----------------------------------------------------------------------------
  def due_next_week?
    due_at.between?(Time.zone.now.next_week, Time.zone.now.next_week.end_of_week)
  end

  #----------------------------------------------------------------------------
  def notify_assignee
    if assigned_to
      # Notify assignee.
    end
  end

  #----------------------------------------------------------------------------
  def specific_time
    parse_calendar_date if bucket == "specific_time"
  rescue ArgumentError
    errors.add(:calendar, :invalid_date)
  end

  #----------------------------------------------------------------------------
  def parse_calendar_date
    # always in 2012-10-28 06:28 format regardless of language
    Time.parse(calendar)
  end

  ActiveSupport.run_load_hooks(:fat_free_crm_task, self)
end