OfficeMomsandDads/scheduler

View on GitHub
app/models/user.rb

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
# frozen_string_literal: true

class User < ApplicationRecord
  extend DateRangeFilterHelper

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :invitable, :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :confirmable, :trackable, :lockable, validate_on_invite: true

  ADMIN         = 'admin'
  COORDINATOR   = 'coordinator'
  SOCIAL_WORKER = 'social_worker'
  VOLUNTEER     = 'volunteer'

  ROLES         = [ADMIN, COORDINATOR, SOCIAL_WORKER, VOLUNTEER].freeze
  PROFILE_ATTRS = [:first_name,
                   :last_name,
                   :phone,
                   :time_zone,
                   :race_id,
                   :first_language_id,
                   :second_language_id,
                   { age_range_ids: [] },
                   :birth_date,
                   :resident_since,
                   :discovered_omd_by,
                   :medical_limitations,
                   :medical_limitations_desc,
                   :conviction,
                   :conviction_desc,
                   :receive_email_notifications,
                   :receive_sms_notifications,
                   { office_notification_ids: [] }].freeze

  has_one :address, as: :addressable, dependent: :destroy
  belongs_to :race, optional: true
  has_and_belongs_to_many :age_ranges
  has_many :announcements,
           dependent:   :restrict_with_error,
           foreign_key: 'author_id',
           inverse_of:  :author
  has_many :needs, dependent: :restrict_with_error
  has_many :shifts, dependent: :restrict_with_error
  has_many :shift_surveys
  has_many :served_needs, -> { distinct },
           through:    :shifts,
           class_name: 'Need',
           source:     'need'
  has_many :office_users, dependent: :destroy
  has_many :offices, through: :office_users
  has_and_belongs_to_many :social_worker_needs,
                          class_name:              'Need',
                          join_table:              'needs_social_workers',
                          foreign_key:             'social_worker_id',
                          association_foreign_key: 'need_id'

  belongs_to :first_language,
             optional:   true,
             class_name: 'Language',
             inverse_of: :primary_speakers

  belongs_to :second_language,
             optional:   true,
             class_name: 'Language',
             inverse_of: :secondary_speakers

  validates :first_name,
            :last_name,
            :phone,
            presence: true,
            if:       :invitation_accepted_at?

  validates :phone,
            telephone_number: { country: :us, types: %i(mobile) },
            if:               :phone?

  validates :birth_date,
            :discovered_omd_by,
            :first_language,
            :race,
            :resident_since,
            presence: true,
            if:       :require_volunteer_profile_attributes?

  validates :conviction,
            :medical_limitations,
            inclusion: { in: [true, false], message: "can't be blank" },
            if:        :require_volunteer_profile_attributes?

  validates :medical_limitations_desc,
            presence: true,
            if:       -> { require_volunteer_profile_attributes? && medical_limitations? }

  validates :conviction_desc,
            presence: true,
            if:       -> { require_volunteer_profile_attributes? && conviction? }

  validates :role,
            inclusion: { in: ROLES, message: '%<value>s is not a valid role' }
  validates :time_zone, presence: true, if: :invitation_accepted_at?
  validate :at_least_one_office
  validate :at_least_one_notification_preference
  validate :at_least_one_age_range,
           if: :require_volunteer_profile_attributes?

  scope :coordinators, -> { where(role: COORDINATOR) }
  scope :social_workers, -> { where(role: SOCIAL_WORKER) }
  scope :volunteers, -> { where(role: VOLUNTEER) }
  scope :volunteerable, -> { volunteers.or(coordinators) }
  scope :deactivated, -> { where(deactivated: true) }
  scope :active, -> { where(deactivated: false) }
  scope :notifiable, ->(base = volunteerable) { base.with_phone.active }
  scope :schedulers, -> { coordinators.or(social_workers) }
  scope :with_phone, -> { where.not(phone: nil) }
  scope :verified, -> { where(verified: true) }
  scope :announceable, -> { with_phone.verified }

  scope :speaks_language, lambda { |language|
    where(first_language: language).or(where(second_language: language))
  }

  before_save :check_phone_verification

  def self.menu
    where(nil).map { |u| [u.to_s, u.id] }.sort_by(&:first)
  end

  def self.total_volunteers_by_spoken_language(current_user, start_at, end_at)
    filter_by_office_users(current_user, true)
      .joins('INNER JOIN languages ON languages.id IN ' \
             '(users.first_language_id, users.second_language_id)')
      .joins(:needs)
      .then { |scope| filter_by_date_range(scope, start_at, end_at) }
      .group('languages.name')
      .count
  end

  def self.total_volunteer_hours_by_user(current_user, start_at, end_at)
    filter_by_office_users(current_user, false)
      .joins(shifts: :need)
      .then { |scope| filter_by_date_range(scope, start_at, end_at) }
      .group(:id, :first_name, :last_name)
      .sum('shifts.duration / 60.0')
  end

  def active_for_authentication?
    super && !deactivated
  end

  def at_least_one_office
    return if offices.any?

    errors.add(:base, 'At least one office assignment is required')
  end

  def at_least_one_notification_preference
    return if receive_email_notifications? || receive_sms_notifications?

    errors.add(:base, 'At least one notification preference is required')
  end

  def at_least_one_age_range
    return if age_ranges.any?

    errors.add(:base, 'At least one age range selection is required')
  end

  ROLES.each do |role|
    define_method "#{role}?" do
      self.role.eql?(role)
    end
  end

  def name
    "#{first_name} #{last_name}".presence || email
  end

  alias to_s name

  def notifiable?
    volunteerable? && phone.present?
  end

  def scheduler?
    role.in? [COORDINATOR, SOCIAL_WORKER, ADMIN]
  end

  def volunteerable?
    role.in? [COORDINATOR, VOLUNTEER]
  end

  def role_display
    I18n.t("user.roles.#{role}", default: ->(*_args) { role.titleize })
  end

  # standard E.164 format used by Twilio
  def e164_phone
    "+1#{phone.gsub(/\D/, '')}"
  end

  def office_notification_ids
    office_users.notifiable.pluck(:office_id)
  end

  def office_notification_ids=(ids)
    ids = ids.compact_blank.map(&:to_i)
    office_users.each do |ou|
      ou.update!(send_notifications: ou.office_id.in?(ids))
    end
  end

  def self.filter_by_office_users(current_user, use_volunteerable_scope)
    if current_user.admin?
      use_volunteerable_scope ? volunteerable : all
    elsif current_user.coordinator? || current_user.social_worker?
      (use_volunteerable_scope ? volunteerable : User).where(
        id: current_user.offices.map(&:users).flatten.map(&:id)
      )
    else
      raise "#{current_user} does not have the proper permissions"
    end
  end

  private

  def require_volunteer_profile_attributes?
    (volunteer? || coordinator?) && invitation_accepted_at?
  end

  def check_phone_verification
    return unless phone_changed? && phone_was.present?

    self.verified = false
  end
end