TracksApp/tracks

View on GitHub
app/models/user.rb

Summary

Maintainability
A
3 hrs
Test Coverage
require 'digest/sha1'
require 'bcrypt'

class User < ApplicationRecord
  # Virtual attribute for the unencrypted password
  attr_accessor :password

  cattr_accessor :per_page
  @@per_page = 25

  has_many(:contexts, -> { order 'position ASC' }, dependent: :delete_all) do
             def find_by_params(params)
               find(params['id'] || params['context_id']) || nil
             end

             def update_positions(context_ids)
               context_ids.each_with_index do |id, position|
                 context = detect { |c| c.id == id.to_i }
                 raise I18n.t('models.user.error_context_not_associated', :context => id, :user => @user.id) if context.nil?
                 context.update_attribute(:position, position + 1)
               end
             end
           end

  has_many(:projects, -> { order 'projects.position ASC' }, dependent: :delete_all) do
              def find_by_params(params)
                find(params['id'] || params['project_id'])
              end

              def update_positions(project_ids)
                project_ids.each_with_index do |id, position|
                  project = find_by(id: id.to_i)
                  raise I18n.t('models.user.error_project_not_associated', :project => id, :user => @user.id) if project.nil?
                  project.update_attribute(:position, position + 1)
                end
              end

              def projects_in_state_by_position(state)
                select { |p| p.state == state }.sort_by { |p| p.position }
              end

              def next_from(project)
                offset_from(project, 1)
              end

              def previous_from(project)
                offset_from(project, -1)
              end

              def offset_from(project, offset)
                projects = projects_in_state_by_position(project.state)
                position = projects.index(project)
                return nil if position == 0 && offset < 0
                projects.at(position + offset)
              end

              def cache_note_counts
                project_note_counts = Note.group(:project_id).count
                each do |project|
                  project.cached_note_count = project_note_counts[project.id] || 0
                end
              end

              def alphabetize(scope_conditions = {})
                projects = where(scope_conditions)
                projects = projects.sort_by { |project| project.name.downcase }
                update_positions(projects.map(&:id))
                return projects
              end

              def actionize(scope_conditions = {})
                todos_in_project = where(scope_conditions).includes(:todos)
                todos_in_project = todos_in_project.sort_by { |x| [-x.todos.active.count, -x.id] }
                todos_in_project.reject { |p| p.todos.active.count > 0 }
                sorted_project_ids = todos_in_project.map(&:id)

                all_project_ids = map(&:id)
                other_project_ids = all_project_ids - sorted_project_ids

                update_positions(sorted_project_ids + other_project_ids)

                return where(scope_conditions)
              end
            end

  has_many(:todos, -> { order 'todos.completed_at DESC, todos.created_at DESC' }, dependent: :delete_all) do
             def count_by_group(g)
               except(:order).group(g).count
             end
           end

  has_many :recurring_todos,
           -> { order 'recurring_todos.completed_at DESC, recurring_todos.created_at DESC' },
           dependent: :delete_all

  has_many(:deferred_todos,
           -> { where('state = ?', 'deferred')
                .order('show_from ASC, todos.created_at DESC') },
           :class_name => 'Todo') do
             def find_and_activate_ready
               where('show_from <= ?', Time.current).collect { |t| t.activate! }
             end
           end

  has_many :tags, dependent: :delete_all
  has_many :notes, -> { order "created_at DESC" }, dependent: :delete_all
  has_one :preference, dependent: :destroy
  has_many :attachments, through: :todos

  validates :login, presence: true, length: { within: 3..80 }
  validates_uniqueness_of :login, on: :create, :case_sensitive => false
  validates :password, presence: true, length: { within: 5..72 }, if: :password_required?
  validates :password_confirmation, presence: true, if: :password_required?
  validates :password, confirmation: true
  validates :email, allow_blank: true, format: { with: URI::MailTo::EMAIL_REGEXP }

  validate :validate_auth_type

  before_create :crypt_password, :generate_token
  before_update :crypt_password
  # Run before deleting todos, projects, contexts, etc.
  before_destroy :destroy_dependencies, :delete_taggings, prepend: true

  def validate_auth_type
    unless Tracks::Config.auth_schemes.include?(auth_type)
      errors.add("auth_type", "not a valid authentication type (#{auth_type})")
    end
  end

  alias_method :prefs, :preference

  def self.authenticate(login, pass)
    return nil if login.blank?
    candidate = where("login = ?", login).first
    return nil if candidate.nil?

    if Tracks::Config.auth_schemes.include?('database')

      return candidate if (candidate.auth_type == 'database' && candidate.password_matches?(pass))
    end

    return nil
  end

  def self.no_users_yet?
    count == 0
  end

  def self.find_admin
    where(:is_admin => true).first
  end

  def to_param
    login
  end

  def display_name
    if first_name.blank? && last_name.blank?
      return login
    elsif first_name.blank?
      return last_name
    elsif last_name.blank?
      return first_name
    end
    "#{first_name} #{last_name}"
  end

  def change_password(pass, pass_confirm)
    self.password = pass
    self.password_confirmation = pass_confirm
    save!
  end

  def date
    Date.current
  end

  def generate_token
    self.token = Digest::SHA1.hexdigest "#{Time.zone.now.to_i}#{rand}"
  end

  def remember_token?
    remember_token_expires_at && Time.zone.now.utc < remember_token_expires_at
  end

  # These create and unset the fields required for remembering users between browser closes
  def remember_me
    self.remember_token_expires_at = 2.weeks.from_now.utc
    self.remember_token ||= Digest::SHA1.hexdigest("#{login}--#{remember_token_expires_at}")
    save
  end

  def forget_me
    self.remember_token_expires_at = nil
    self.remember_token            = nil
    save
  end

  def password_matches?(pass)
    BCrypt::Password.new(crypted_password) == pass
  end

  def create_hash(s)
    BCrypt::Password.create(s)
  end

  protected

  def crypt_password
    return if password.blank?
    write_attribute("crypted_password", create_hash(password)) if password == password_confirmation
  end

  def password_required?
    auth_type == 'database' && crypted_password.blank? || password.present?
  end

  def destroy_dependencies
    ids = todos.pluck(:id)
    pred_deps = Dependency.where(predecessor_id: ids).destroy_all
    succ_deps = Dependency.where(predecessor_id: ids).destroy_all
  end

  def delete_taggings
    ids = todos.pluck(:id)
    taggings = Tagging.where(taggable_id: ids).pluck(:id)
    Tagging.where(id: taggings).delete_all
  end
end