ari/jobsworth

View on GitHub
app/models/user.rb

Summary

Maintainability
D
1 day
Test Coverage
# encoding: UTF-8
# A user from a company
require 'digest/md5'

class User < ActiveRecord::Base

  devise :database_authenticatable, :registerable, :encryptable,
         :recoverable, :rememberable, :trackable

  # Setup accessible (or protected) attributes for your model
  ACCESS_CONTROL_ATTRIBUTES = [:create_projects, :use_resources, :read_clients, :create_clients, :edit_clients, :can_approve_work_logs, :admin, :access_level_id]
  DEFAULT_TIMEZONE = 'Australia/Sydney'

  attr_accessor :subdomain

  has_many(:custom_attribute_values, :as => :attributable, :dependent => :destroy,
           # set validate = false because validate method is over-ridden and does that for us
           :validate => false)
  include CustomAttributeMethods

  belongs_to :company
  belongs_to :customer
  belongs_to :access_level
  has_many :projects, -> { where('projects.completed_at IS NULL').order('projects.customer_id, projects.name').readonly(false) }, :through => :project_permissions, :source => :project
  has_many :completed_projects, -> { where('projects.completed_at IS NOT NULL').order('projects.customer_id, projects.name').readonly(false) }, :through => :project_permissions, :source => :project
  has_many :all_projects, -> { order('projects.customer_id, projects.name').readonly(false) }, :through => :project_permissions, :source => :project
  has_many :project_permissions, :dependent => :destroy

  has_many :tasks, :through => :task_owners, :class_name => 'TaskRecord'
  has_many :task_owners, :dependent => :destroy
  has_many :work_logs

  has_many :notifications, :class_name => 'TaskWatcher', :dependent => :destroy
  has_many :notifies, :through => :notifications, :source => :task

  has_many :widgets, -> { order('widgets.column, widgets.position') }, :dependent => :destroy

  has_many :task_filters, :dependent => :destroy
  has_many :visible_task_filters, -> { order('task_filters.name') }, :source => 'task_filter', :through => :task_filter_users
  has_many :task_filter_users, :dependent => :delete_all

  has_many :sheets, :dependent => :destroy

  has_many :preferences, :as => :preferencable
  has_many :email_addresses, -> { order('email_addresses.default DESC') }, :dependent => :destroy

  has_many :email_deliveries

  has_one :work_plan, :dependent => :destroy

  has_attached_file :avatar, :whiny => false, :styles => {:small => '25x25>', :large => '50x50>'}, :path => File.join(Setting.store_root, ':company_id', 'avatars', ':id_:basename_:style.:extension')

  validates_attachment_content_type :avatar, content_type: /\Aimage/

  Paperclip.interpolates :company_id do |attachment, style|
    attachment.instance.company_id
  end

  accepts_nested_attributes_for :work_plan, :customer

  include PreferenceMethods

  validates_length_of :name, :maximum => 200, :allow_nil => true
  validates_presence_of :name

  validates :username,
            :presence => true,
            :length => {:minimum => 2, :maximum => 200},
            :uniqueness => {:case_sensitive => false, :scope => 'company_id'}

  validates :password, :confirmation => true, :if => :password_required?
  validates_presence_of :email


  validates_presence_of :company
  validates :date_format, :presence => true, :inclusion => {:in => %w(%m/%d/%Y %d/%m/%Y %Y-%m-%d)}
  validates :time_format, :presence => true, :inclusion => {:in => %w(%H:%M %I:%M%p)}
  validate :validate_custom_attributes

  before_create :generate_uuid
  after_create :generate_widgets
  before_create :set_default_values
  before_validation :set_date_time_formats, :on => :create
  before_destroy :reject_destroy_if_exist
  after_create :update_orphaned_email_addresses
  before_save :set_timezone

  scope :active, -> { where(:active => true) }
  scope :auto_add, -> { active.where(:auto_add_to_customer_tasks => true) }
  scope :by_email, lambda { |email|
    where('email_addresses.email' => email, 'email_addresses.default' => true).joins(:email_addresses).readonly(false)
  }
  scope :from_this_year, -> { where('created_at > ?', Time.zone.now.beginning_of_year - 1.month) }
  scope :recent_users, -> { limit(50).order('created_at desc') }
  scope :name_not_in, -> (names) { where.not(name: names) }
  scope :task_is_not, -> (task) { where.not(id: task.user_ids) }

  ###
  # Searches the users for company and returns
  # any that have names or ids that match at least one of
  # the given strings
  ###
  def self.search(company, strings)
    conds = Search.search_conditions_for(strings, [:name], :start_search_only => true)
    return company.users.where(conds)
  end

  def self.search_by_name(term)
    name = arel_table[:name]
    where(name.matches("#{term}%").or(name.matches("%#{term}%"))).order('name')
  end

  def has_projects?
    projects.any?
  end

  def set_access_control_attributes(params)
    ACCESS_CONTROL_ATTRIBUTES.each do |attr|
      next if params[attr].nil?
      self.send(attr.to_s + '=', params[attr])
    end
  end

  def avatar_path
    avatar.path(:small)
  end

  def avatar_large_path
    avatar.path(:large)
  end

  def avatar?
    !self.avatar_path.nil? and File.exist?(self.avatar_path)
  end

  def set_default_values
    self.work_plan ||= WorkPlan.new(:monday => 8, :tuesday => 8, :wednesday => 8, :thursday => 8, :friday => 8, :saturday => 0, :sunday => 0)
    self.time_zone ||= DEFAULT_TIMEZONE
    self.date_format ||= '%d/%m/%Y'
    self.time_format ||= '%H:%M'
    self.locale ||= 'en_US'
  end

  def generate_uuid
    self.uuid ||= Digest::MD5.hexdigest(rand(100000000).to_s + Time.now.to_s)
    self.autologin ||= Digest::MD5.hexdigest(rand(100000000).to_s + Time.now.to_s)
  end

  def new_widget
    Widget.new(:user => self, :company_id => self.company_id, :collapsed => 0, :configured => true)
  end

  def generate_widgets
    w = new_widget
    w.name = I18n.t('widgets.top_tasks')
    w.widget_type = 0
    w.number = 5
    w.mine = true
    w.order_by = 'priority'
    w.column = 0
    w.position = 0
    w.save

    w = new_widget
    w.name = I18n.t('widgets.newest_tasks')
    w.widget_type = 0
    w.number = 5
    w.mine = false
    w.order_by = 'date'
    w.column = 0
    w.position = 1
    w.save

    w = new_widget
    w.name = I18n.t('widgets.open_tasks')
    w.widget_type = 3
    w.number = 7
    w.mine = true
    w.column = 1
    w.position = 0
    w.save
  end

  def avatar_url(size=32, secure = false)
    if avatar?
      if size > 25 && File.exist?(avatar_large_path)
        '/users/avatar/'+id.to_s+'?large=1'
      else
        '/users/avatar/'+id.to_s
      end
    elsif email
      if secure
        "https://secure.gravatar.com/avatar.php?gravatar_id=#{Digest::MD5.hexdigest(self.email.downcase)}&rating=PG&size=#{size}"
      else
        "http://www.gravatar.com/avatar.php?gravatar_id=#{Digest::MD5.hexdigest(self.email.downcase)}&rating=PG&size=#{size}"
      end
    end
  end

  # label and value are used for the json formatting used for autocomplete
  def label
    name
  end

  alias_method :value, :label
  alias_method :display_name, :label

  def display_login
    name + ' / ' + (customer.nil? ? company.name : customer.name)
  end

  def can?(project, perm)
    return true if project.nil? or admin?

    @perm_cache ||= {}
    unless @perm_cache[project.id]
      @perm_cache[project.id] ||= {}
      self.project_permissions.each do |p|
        @perm_cache[p.project_id] ||= {}
        ProjectPermission.permissions.each do |p_perm|
          @perm_cache[p.project_id][p_perm] = p.can?(p_perm)
        end
      end
    end

    (@perm_cache[project.id][perm])
  end

  def can_all?(projects, perm)
    projects.all? { |p| can?(p, perm) }
  end

  def can_any?(project, perm)
    projects.any? { |p| can?(p, perm) }
  end

  def admin?
    !self.admin.nil? && self.admin > 0
  end


  def can_use_billing?
    company.use_billing
  end

  ###
  # Returns true if this user is allowed to view the clients section
  # of the website.
  ###
  def can_view_clients?
    self.admin? or self.read_clients?
  end

  # Returns true if this user is allowed to view the given task.
  def can_view_task?(task)
    !TaskRecord.accessed_by(self).find_by(:id => task).nil?
  end

  # Returns a fragment of sql to restrict tasks to only the ones this
  # user can see
  def user_tasks_sql
    res = []
    if self.projects.any?
      res << "tasks.project_id in (#{ all_project_ids.join(',') })"
    end

    res << "task_users.user_id = #{ self.id }"

    res = res.join(' or ')
    return "(#{ res })"
  end

  # Returns an array of all milestone this user has access to
  # (through projects).
  # If options is passed, those options will be passed to the find.
  def milestones
    company.milestones.where(['projects.id in (?)', all_project_ids]).joins(:project).order('lower(milestones.name)')
  end

  def tz
    @tz ||= TZInfo::Timezone.get(self.time_zone)
  end

  # Get date formatter in a form suitable for jQuery-UI
  def dateFormat
    return 'mm/dd/yy' if self.date_format == '%m/%d/%Y'
    return 'dd/mm/yy' if self.date_format == '%d/%m/%Y'
    return 'yy/mm/dd' if self.date_format == '%Y-%m-%d'
  end

  def to_s
    str = [name]
    str << "(#{ customer.name })" if customer

    str.join(' ')
  end

  def private_task_filters
    task_filters.visible.where('user_id = ?', self.id)
  end

  def shared_task_filters
    company.task_filters.shared.visible.where('user_id != ?', self.id)
  end

  # return as a string the default email address for this user
  # return nil if this user has no default email address
  def email
    email_addresses.detect { |ea| ea.default? }.try(:email)
  end

  alias_method :primary_email, :email

  def email=(new_email)
    ea = EmailAddress.where(:email => new_email).first || EmailAddress.new(:email => new_email, :company => self.company)
    if ea.user
      errors.add(:email, I18n.t('errors.messages.taken_by', email: ea.email, user: ea.user.name))
    else
      self.email_addresses.update_all(:default => false)
      ea.default = true
      self.email_addresses << ea
    end
  end

  def get_projects
    (admin?) ? company.projects : all_projects
  end

  # Get the next tasks for the nextTasks panel
  def next_tasks(count = 5)
    self.tasks.open_only.not_snoozed.includes(:milestone).order('tasks.weight DESC').limit(count)
  end

  def workday_length(date)
    (self.work_plan.send(self.tz.utc_to_local(date.utc).strftime('%A').downcase) * 60).to_i
  end

  def top_next_task
    next_tasks(1).first
  end

  def schedule_tasks(options={})
    options[:limit] ||= 1000000
    options[:save] = true unless options.key?(:save)

    if options[:save]
      self.update_column(:need_schedule, false)
    end

    acc_total = self.work_logs.where('started_at > ? AND started_at < ?', self.tz.local_to_utc(self.tz.now.beginning_of_day), self.tz.local_to_utc(self.tz.now.end_of_day)).sum(:duration)

    due_date_num = 0
    self.next_tasks(options[:limit]).includes(:project).each do |task|
      while acc_total >= self.workday_length(Time.now + due_date_num.days)
        acc_total -= self.workday_length(Time.now + due_date_num.days)
        due_date_num += 1
      end

      if options[:save]
        task.update_column(:estimate_date, Time.now + due_date_num.days)
      else
        task.estimate_date = Time.now + due_date_num.days
      end

      yield task if block_given?

      # show when the task begins, instead of the finish date
      acc_total += task.minutes_left
    end
  end

  def self.schedule_tasks
    User.where(:need_schedule => true).each do |user|
      user.schedule_tasks
    end
  end

  # +conditions+ is guaranteed by Devise to have +:subdomain+ and +:username+ keys
  def self.find_for_authentication(conditions={})
    company = Company.where(:subdomain => conditions.delete(:subdomain)).first
    return false if company.blank?
    super(conditions.merge :company_id => company.id)
  end

  def use_resources?
    use_resources && company.use_resources
  end

  protected

  def password_required?
    new_record? || !password.nil? || !password_confirmation.nil?
  end

  private

  def set_timezone
    timezone = TZInfo::Timezone.get(time_zone) rescue nil
    self.time_zone = DEFAULT_TIMEZONE unless timezone.present?
  end

  # This user may have been automatically linked to orphaned emails,
  # update work_logs and tasks that are used to be linked to the orphaned emails.
  def update_orphaned_email_addresses
    self.email_addresses.each do |ea|
      ea.link_to_user(self.id)
    end
  end

  def reject_destroy_if_exist
    [:work_logs].each do |association|
      errors.add(:base, I18n.t('errors.messages.reject_destroy_if_exist', association: association.to_s.humanize)) unless eval("#{association}.count").zero?
    end
    if errors.count.zero?
      ActiveRecord::Base.connection.execute("UPDATE tasks set creator_id = NULL WHERE company_id = #{self.company_id} AND creator_id = #{self.id}")
      return true
    else
      return false
    end
  end

  # Sets the date time format for this user to a sensible default
  # if it hasn't already been set
  # TODO: this railses error 'undefined method `users' for nil:NilClass'
  # if company is not set and valid? is called...
  def set_date_time_formats
    first_user = company.users.detect { |u| u != self }

    if first_user and first_user.time_format and first_user.date_format
      self.time_format = first_user.time_format
      self.date_format = first_user.date_format
    else
      self.date_format = '%d/%m/%Y'
      self.time_format = '%H:%M'
    end
  end
end


# == Schema Information
#
# Table name: users
#
#  id                         :integer(4)      not null, primary key
#  name                       :string(200)     default(""), not null
#  username                   :string(200)     default(""), not null
#  company_id                 :integer(4)      default(0), not null
#  created_at                 :datetime
#  updated_at                 :datetime
#  admin                      :integer(4)      default(0)
#  time_zone                  :string(255)
#  option_tracktime           :integer(4)
#  seen_news_id               :integer(4)      default(0)
#  last_project_id            :integer(4)
#  last_seen_at               :datetime
#  last_ping_at               :datetime
#  last_milestone_id          :integer(4)
#  last_filter                :integer(4)
#  date_format                :string(255)     default("%d/%m/%Y"), not null
#  time_format                :string(255)     default("%H:%M"), not null
#  receive_notifications      :integer(4)      default(1)
#  uuid                       :string(255)     not null
#  seen_welcome               :integer(4)      default(0)
#  locale                     :string(255)     default("en_US")
#  option_avatars             :integer(4)      default(1)
#  autologin                  :string(255)     not null
#  remember_until             :datetime
#  option_floating_chat       :boolean(1)      default(TRUE)
#  create_projects            :boolean(1)      default(TRUE)
#  receive_own_notifications  :boolean(1)      default(TRUE)
#  use_resources              :boolean(1)
#  customer_id                :integer(4)
#  active                     :boolean(1)      default(TRUE)
#  read_clients               :boolean(1)      default(FALSE)
#  create_clients             :boolean(1)      default(FALSE)
#  edit_clients               :boolean(1)      default(FALSE)
#  can_approve_work_logs      :boolean(1)
#  auto_add_to_customer_tasks :boolean(1)
#  access_level_id            :integer(4)      default(1)
#  avatar_file_name           :string(255)
#  avatar_content_type        :string(255)
#  avatar_file_size           :integer(4)
#  avatar_updated_at          :datetime
#  encrypted_password         :string(128)     default(""), not null
#  password_salt              :string(255)     default(""), not null
#  reset_password_token       :string(255)
#  remember_token             :string(255)
#  remember_created_at        :datetime
#  sign_in_count              :integer(4)      default(0)
#  current_sign_in_at         :datetime
#  last_sign_in_at            :datetime
#  current_sign_in_ip         :string(255)
#  last_sign_in_ip            :string(255)
#  reset_password_sent_at     :datetime
#
# Indexes
#
#  index_users_on_username_and_company_id  (username,company_id) UNIQUE
#  index_users_on_reset_password_token     (reset_password_token) UNIQUE
#  index_users_on_autologin                (autologin)
#  users_company_id_index                  (company_id)
#  index_users_on_customer_id              (customer_id)
#  index_users_on_last_seen_at             (last_seen_at)
#  users_uuid_index                        (uuid)
#