# 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 attr_accessor :calendar ALLOWED_VIEWS = %w(pending assigned completed) belongs_to :user belongs_to :assignee, :class_name => "User", :foreign_key => :assigned_to belongs_to :completor, :class_name => "User", :foreign_key => :completed_by belongs_to :asset, :polymorphic => true serialize :subscribed_users, Set # 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, ->(*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, ->(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, ->(user) { includes(:assignee). where('user_id = ? OR assigned_to = ?', user.id, user.id) } # Show opportunities which either belong to the user and are unassigned, or are assigned to the user scope :visible_on_dashboard, ->(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, -> { 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, ->(query) { query = query.gsub(/[^\w\s\-\.'\p{L}]/u, '').strip where('upper(name) LIKE upper(?)', "%#{query}%") } acts_as_commentable has_paper_trail :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 => "self.bucket == 'specific_time' && !self.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? !!self.completed_at end # Matcher for the :tracked_by? named scope. #---------------------------------------------------------------------------- def tracked_by?(user) self.user == user || self.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? self.due_at && (self.due_at.hour != 0 || self.due_at.min != 0 || self.due_at.sec != 0) && (self.due_at.hour != 23 && self.due_at.min != 59 && self.due_at.sec != 59) end # Convert specific due_date to "due_today", "due_tomorrow", etc. bucket name. #---------------------------------------------------------------------------- def computed_bucket return self.bucket if self.bucket != "specific_time" case when self.due_at < Time.zone.now.midnight "overdue" when self.due_at >= Time.zone.now.midnight && self.due_at < Time.zone.now.midnight.tomorrow "due_today" when self.due_at >= Time.zone.now.midnight.tomorrow && self.due_at < Time.zone.now.midnight.tomorrow + 1.day "due_tomorrow" when self.due_at >= (Time.zone.now.midnight.tomorrow + 1.day) && self.due_at < Time.zone.now.next_week "due_this_week" when self.due_at >= Time.zone.now.next_week && self.due_at < (Time.zone.now.next_week.end_of_week + 1.day) "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? or !ALLOWED_VIEWS.include?(view) 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.inject({ :all => 0 }) do |hash, key| 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 self.bucket when "overdue" self.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" self.calendar ? parse_calendar_date : nil else # due_later or due_asap nil end end #---------------------------------------------------------------------------- def notify_assignee if self.assigned_to # Notify assignee. end end #---------------------------------------------------------------------------- def specific_time parse_calendar_date if self.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(self.calendar) end ActiveSupport.run_load_hooks(:fat_free_crm_task, self) end