BathHacked/energy-sparks

View on GitHub
app/models/user.rb

Summary

Maintainability
B
5 hrs
Test Coverage
A
94%
# == Schema Information
#
# Table name: users
#
#  confirmation_sent_at   :datetime
#  confirmation_token     :string
#  confirmed_at           :datetime
#  created_at             :datetime         not null
#  current_sign_in_at     :datetime
#  current_sign_in_ip     :inet
#  email                  :string           default(""), not null
#  encrypted_password     :string           default(""), not null
#  failed_attempts        :integer          default(0), not null
#  id                     :bigint(8)        not null, primary key
#  last_sign_in_at        :datetime
#  last_sign_in_ip        :inet
#  locked_at              :datetime
#  name                   :string
#  preferred_locale       :string           default("en"), not null
#  pupil_password         :string
#  remember_created_at    :datetime
#  reset_password_sent_at :datetime
#  reset_password_token   :string
#  role                   :integer          default("guest"), not null
#  school_group_id        :bigint(8)
#  school_id              :bigint(8)
#  sign_in_count          :integer          default(0), not null
#  staff_role_id          :bigint(8)
#  unlock_token           :string
#  updated_at             :datetime         not null
#
# Indexes
#
#  index_users_on_confirmation_token            (confirmation_token) UNIQUE
#  index_users_on_email                         (email) UNIQUE
#  index_users_on_reset_password_token          (reset_password_token) UNIQUE
#  index_users_on_school_group_id               (school_group_id)
#  index_users_on_school_id                     (school_id)
#  index_users_on_school_id_and_pupil_password  (school_id,pupil_password) UNIQUE
#  index_users_on_staff_role_id                 (staff_role_id)
#
# Foreign Keys
#
#  fk_rails_...  (school_group_id => school_groups.id) ON DELETE => restrict
#  fk_rails_...  (school_id => schools.id) ON DELETE => cascade
#  fk_rails_...  (staff_role_id => staff_roles.id) ON DELETE => restrict
#

require 'securerandom'

class User < ApplicationRecord
  attribute :pupil_password, EncryptedField::Type.new

  belongs_to :school, optional: true
  belongs_to :staff_role, optional: true
  belongs_to :school_group, optional: true
  has_many :contacts
  has_many :consent_grants, inverse_of: :user, dependent: :nullify

  has_many :school_onboardings, inverse_of: :created_user, foreign_key: :created_user_id

  has_and_belongs_to_many :cluster_schools, class_name: 'School', join_table: :cluster_schools_users

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

  enum role: [:guest, :staff, :admin, :school_admin, :school_onboarding, :pupil, :group_admin, :analytics, :volunteer]

  scope :alertable, -> { where(role: [User.roles[:staff], User.roles[:school_admin], User.roles[:volunteer]]) }

  scope :recently_logged_in, ->(date) { where('last_sign_in_at >= ?', date) }
  validates :email, presence: true

  validates :pupil_password, presence: true, if: :pupil?
  validate :pupil_password_unique, if: :pupil?

  validates :staff_role_id, :school_id, presence: true, if: :staff?
  validates :staff_role_id, :school_id, presence: true, if: :school_admin?
  validates :staff_role_id, presence: true, if: :school_onboarding?

  validates :school_id, presence: true, if: :pupil?

  validates :school_group_id, presence: true, if: :group_admin?

  validate :preferred_locale_presence_in_available_locales

  after_save :update_contact

  def default_scoreboard
    if group_admin? && school_group.default_scoreboard
      school_group.default_scoreboard
    elsif school
      school.scoreboard
    end
  end

  def display_name
    name.present? ? name : email
  end

  def staff_role_as_symbol
    return nil unless staff_role
    staff_role.as_symbol
  end

  def cluster_schools_for_switching
    cluster_schools.visible.by_name.excluding(school)
  end

  def add_cluster_school(school)
    cluster_schools << school unless cluster_schools.include?(school)
  end

  def has_other_schools?
    cluster_schools.excluding(school).any?
  end

  def self.find_school_users_linked_to_other_schools(school_id:, user_ids:)
    User.joins(:cluster_schools_users)
        .where.not('cluster_schools_users.school_id' => school_id)
        .where('cluster_schools_users.user_id in (?)', user_ids)
  end

  def remove_school(school_to_remove)
    cluster_schools.delete(school_to_remove)
    if school == school_to_remove
      if cluster_schools.any?
        update!(school: cluster_schools.last)
      else
        update!(school: nil, role: :school_onboarding)
      end
    end
  end

  def schools
    return School.visible.by_name if self.admin?
    return school_group.schools.visible.by_name if self.school_group
    [school].compact
  end

  def school_name
    school.name if school
  end

  def school_group_name
    default_school_group.try(:name)
  end

  def default_school_group
    school.try(:school_group) || school_group
  end

  def self.new_pupil(school, attributes)
    new(
      attributes.merge(
        role: :pupil,
        school: school,
        email: "#{school.id}-#{SecureRandom.uuid}@pupils.#{ENV['APPLICATION_HOST']}",
        password: SecureRandom.uuid,
        confirmed_at: Time.zone.now
      )
    )
  end

  def self.new_staff(school, attributes)
    new(
      attributes.merge(
        role: :staff,
        school: school
      )
    )
  end

  def self.new_school_admin(school, attributes)
    new(
      attributes.merge(
        role: :school_admin,
        school: school
      )
    )
  end

  def self.new_school_onboarding(attributes)
    new(
      attributes.merge(
        role: :school_onboarding,
        confirmed_at: Time.zone.now
      )
    )
  end

  def contact_for_school
    contacts.for_school(school).first
  end

  def after_confirmation
    OnboardingMailer.with_user_locales(users: [self], school: school) { |mailer| mailer.welcome_email.deliver_now } if self.school.present?
  end

  def self.admin_user_export_csv
    CSV.generate do |csv|
      csv << [
        'School Group',
        'School',
        'School type',
        'Funder',
        'Region',
        'Name',
        'Email',
        'Role',
        'Staff Role',
        'Locked'
      ]
      where.not(role: [:pupil, :admin]).order(:email).each do |user|
        school_group_name = if user.group_admin?
                              user.school_group.name
                            elsif user.school && user.school.school_group
                              user.school_group_name
                            else
                              ''
                            end

        csv << [
          school_group_name,
          user.school&.name || '',
          user.school&.school_type&.humanize || '',
          user.school&.funder&.name || '',
          user.school&.region&.to_s&.titleize || '',
          user.name,
          user.email,
          user.role.titleize,
          user.staff_role&.title || '',
          user.access_locked? ? 'Yes' : 'No'
        ]
      end
    end
  end

protected

  def preferred_locale_presence_in_available_locales
    return if I18n.available_locales.include? preferred_locale&.to_sym

    errors.add(:preferred_locale, 'must be present in the list of availale locales')
  end

  def password_required?
    confirmed? ? super : false
  end

  def update_contact
    if (contact = contact_for_school)
      contact.populate_from_user(self)
      contact.save
    end
  end

  def pupil_password_unique
    return if pupil_password.blank?
    existing_user = school.authenticate_pupil(pupil_password)
    if existing_user && existing_user != self
      errors.add(:pupil_password, "is already in use for '#{existing_user.name}'")
    end
  end
end