sudara/alonetone

View on GitHub
app/models/user.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

class User < ApplicationRecord
  include SoftDeletion
  include Rakismet::Model

  rakismet_attrs  author: proc { name },
                  author_email: proc { email },
                  user_ip: proc { current_login_ip },
                  content: proc { profile&.bio },
                  user_agent: proc { profile&.user_agent },
                  comment_type: 'signup'

  include User::Findability
  include User::Statistics

  validates_length_of :display_name, within: 3..50, allow_blank: true

  validates :email,
    format: {
      with: URI::MailTo::EMAIL_REGEXP,
      message: "should look like an email address."
    },
    length: { maximum: 100 },
    uniqueness: {
      case_sensitive: false,
      if: :will_save_change_to_email?
    }

  validates :login,
    format: {
      with: /\A\w+\z/,
      message: "should use only letters and numbers."
    },
    length: { within: 3..100 },
    uniqueness: {
      case_sensitive: false,
      if: :will_save_change_to_login?
    }

  validates :password,
    confirmation: { if: :require_password? },
    length: {
      minimum: 8,
      if: :require_password?
    }

  validates :password_confirmation,
    length: {
      minimum: 8,
      if: :require_password?
    }

  acts_as_authentic do |c|
    c.crypto_provider = Authlogic::CryptoProviders::SCrypt
    c.disable_perishable_token_maintenance = true # we will handle tokens
    c.ignore_blank_passwords = false
  end

  scope :activated,     -> { where(perishable_token: nil).recent }
  scope :alpha,         -> { order('display_name ASC') }
  scope :geocoded,      -> { where(['users.lat != ""']).recent }
  scope :musicians,     -> { where(['assets_count > ?', 0]).order('assets_count DESC') }
  scope :random_order,  -> { order(Arel.sql('RAND()')) }
  scope :recent,        -> { order('users.id DESC') }
  scope :recently_seen, -> { order('last_request_at DESC') }
  scope :with_location, -> { where(['users.country != ""']).recently_seen }
  scope :with_preloads, -> { includes(:profile, :avatar_image_blob) }

  # The before destroy has to be declared *before* has_manys
  # This ensures User#efficiently_destroy_relations executes first
  before_create :make_first_user_admin
  before_destroy :destroy_with_relations
  before_save { |u| u.display_name = u.login if u.display_name.blank? }
  after_create :create_profile, :create_settings

  has_one :profile, dependent: :destroy
  accepts_nested_attributes_for :profile, update_only: true # we don't want user to be printing new profiles

  has_one :settings, dependent: :destroy, class_name: 'Settings'
  delegate(*Settings::AVAILABLE, to: :settings)

  has_one :account_request
  belongs_to :invited_by, optional: true, class_name: 'User'
  has_many :invitees, foreign_key: 'invited_by_id'
  has_many :assets,
    -> { order('assets.id DESC') },
    dependent: :destroy

  has_many :playlists, -> { order('playlists.position') },
    dependent: :destroy

  # this only covers comments received by the user
  # not comments made by the user
  has_many :comments_received, -> { order('comments.id DESC') },
    foreign_key: 'user_id',
    class_name: 'Comment',
    dependent: :destroy

  has_many :comments_made, -> { order('comments.id DESC') },
    foreign_key: 'commenter_id',
    class_name: 'Comment',
    dependent: :destroy

  has_many :tracks,
    dependent: :destroy

  # alonetone plus
  has_many :memberships
  has_many :groups, through: :membership

  # Can listen to music, and have that tracked
  has_many :listens, -> { order('listens.created_at DESC') }, foreign_key: 'listener_id'

  has_many :listened_to_tracks,
    -> { order('listens.created_at DESC') },
    through: :listens,
    source: :asset

  # Can have their music listened to
  has_many :track_plays,
    -> { order('listens.created_at DESC').includes(:asset) },
    foreign_key: 'track_owner_id',
    class_name: 'Listen'

  # And therefore have listeners
  has_many :listeners,
    -> { distinct },
    through: :track_plays

  has_many :followings, dependent: :destroy
  has_many :follows, dependent: :destroy, class_name: 'Following', foreign_key: 'follower_id'

  # people who are following this musician
  has_many :followers, through: :followings

  # musicians who this person follows
  has_many :followees, through: :follows, source: :user

  # We have to define attachments last to make the Active Record callbacks
  # fire in the right order.
  has_one_attached :avatar_image
  validates :avatar_image, attached: {
    content_type: %w[image/png image/jpeg image/jpg image/gif],
    byte_size: { less_than: 20.megabytes }
  }, if: :avatar_image_present?

  has_one :mass_invite_signup
  has_one :mass_invite, through: :mass_invite_signup

  has_one :patron

  # tokens and activation
  def clear_token!
    update_attribute(:perishable_token, nil)
  end

  def active?
    perishable_token.nil?
  end

  def never_activated?
    last_request_at.nil? && perishable_token.present?
  end

  def update_account_request!
    account_request&.claimed!
  end

  def moderator?
    self[:moderator] || self[:admin]
  end

  def activate!
    !active? ? clear_token! : false
  end

  def self.destroy_deleted_accounts_older_than_30_days
    User.destroyable.find_each(&:destroy)
  end

  def self.with_same_ip_as(user)
    User.where(current_login_ip: user.current_login_ip).where('id != ?', user.id)
  end

  def self.find_by_login_or_email(login_or_email)
    User.where(login: login_or_email).or(User.where(email: login_or_email)).first
  end

  def listened_to_today_ids
    listens.select('listens.asset_id').where(['listens.created_at > ?', 1.day.ago]).pluck(:asset_id)
  end

  def listened_to_ids
    listens.select('listens.asset_id').pluck(:asset_id).uniq
  end

  def top_tracks
    assets.order('listens_count DESC').limit(10)
  end

  def favorites
    playlists.favorites.first
  end

  def to_param
    login.to_s
  end

  def listened_more_than?(n)
    listens.count > n
  end

  def hasnt_been_here_in(hours)
    last_login_at &&
      last_login_at < hours.ago.utc
  end

  def is_following?(user)
    follows.find_by_user_id(user)
  end

  def new_tracks_from_followees(limit)
    Asset.new_tracks_from_followees(self, limit)
  end

  def follows_user_ids
    follows.pluck(:user_id)
  end

  def has_followees?
    follows.count > 0
  end

  def add_or_remove_followee(followee_id)
    return if followee_id == id # following yourself would be a pointless affair!

    if is_following?(followee_id)
      is_following?(followee_id).destroy
    else
      follows.where(user_id: followee_id).first_or_create
    end
  end

  # convenince shortcut
  def ip
    current_login_ip
  end

  def similar_users_by_ip
    User.where('last_login_ip = ? or last_login_ip = ? or current_login_ip = ? or current_login_ip = ?',
      last_login_ip, current_login_ip, last_login_ip, current_login_ip).pluck(:id)
  end

  def toggle_favorite(asset)
    existing_track = tracks.favorites.where(asset_id: asset.id).first
    if existing_track
      existing_track.destroy && Asset.decrement_counter(:favorites_count, asset.id)
    else
      tracks.favorites.create(asset_id: asset.id)
      Asset.increment_counter(:favorites_count, asset.id, touch: true)
    end
  end

  def brand_new?
    created_at > 24.hours.ago
  end

  # Returns true when the user has a usable avatar.
  def avatar_image_present?
    avatar_image.attached?
  end

  # Generates a location to user's avatar with the requested variant. Returns nil when the user
  # does not have a usable avatar.
  #
  # As of Rails 6.0 attachables aren't persisted to storage until save
  # https://github.com/rails/rails/pull/33303
  # Which means we don't want to try and display variants from unpersisted records with invalid attachments
  def avatar_image_location(variant:)
    return unless avatar_image.attached? && avatar_image.persisted?

    Storage::Location.new(
      ImageVariant.variant(avatar_image, variant: variant),
      signed: false
    )
  end

  def deleted?
    deleted_at != nil
  end

  def destroy_with_relations
    UserCommand.new(self).destroy_with_relations
  end

  def self.filter_by(filter)
    case filter
    when "deleted"
      only_deleted.where(is_spam: false).order('deleted_at DESC')
    when "is_spam"
      with_deleted.where(is_spam: true).order('deleted_at DESC')
    when "spam_musicians"
      musicians.with_deleted.where(is_spam: true).order('deleted_at DESC')
    when "not_spam"
      with_deleted.where(is_spam: false).recent
    when "invited"
      joins(:mass_invite_signup).order('created_at DESC')
    when String
      with_deleted.where("email like '%#{filter}%' or login like '%#{filter}%' or display_name like '%#{filter}%'").recent
    else
      with_deleted.recent
    end
  end

  def has_public_playlists?
    playlists.only_public.count >= 1
  end

  def has_tracks?
    assets_count > 0
  end

  def has_as_favorite?(asset)
    favorite_asset_ids.include?(asset.id)
  end

  def favorite_asset_ids
    Track.where(playlist_id: favorites).pluck(:asset_id)
  end

  def favorites
    playlists.favorites.first
  end

  def name
    self[:display_name] || login
  end

  protected

  def make_first_user_admin
    self.admin = true if User.count.zero?
  end
end

# == Schema Information
#
# Table name: users
#
#  id                :integer          not null, primary key
#  admin             :boolean          default(FALSE)
#  assets_count      :integer          default(0), not null
#  bandwidth_used    :integer          default(0)
#  comments_count    :integer          default(0)
#  crypted_password  :string(128)      default(""), not null
#  current_login_at  :datetime
#  current_login_ip  :string(255)
#  dark_theme        :boolean          default(FALSE)
#  deleted_at        :datetime
#  display_name      :string(255)
#  email             :string(100)
#  followers_count   :integer          default(0)
#  is_spam           :boolean          default(FALSE)
#  itunes            :string(255)
#  last_login_at     :datetime
#  last_login_ip     :string(255)
#  last_request_at   :datetime
#  listens_count     :integer          default(0)
#  login             :string(40)
#  login_count       :integer          default(0), not null
#  moderator         :boolean          default(FALSE)
#  perishable_token  :string(255)
#  persistence_token :string(255)
#  playlists_count   :integer          default(0), not null
#  salt              :string(128)      default(""), not null
#  settings          :text(4294967295)
#  created_at        :datetime
#  updated_at        :datetime
#  invited_by_id     :integer
#
# Indexes
#
#  index_users_on_updated_at  (updated_at)
#