Newer
Older
dmpopidor / app / models / user.rb
# frozen_string_literal: true
# == Schema Information
#
# Table name: users
#
#  id                     :integer          not null, primary key
#  firstname              :string
#  surname                :string
#  email                  :string(80)       default(""), not null
#  created_at             :datetime         not null
#  updated_at             :datetime         not null
#  encrypted_password     :string           default("")
#  reset_password_token   :string
#  reset_password_sent_at :datetime
#  remember_created_at    :datetime
#  sign_in_count          :integer          default("0")
#  current_sign_in_at     :datetime
#  last_sign_in_at        :datetime
#  current_sign_in_ip     :string
#  last_sign_in_ip        :string
#  confirmation_token     :string
#  confirmed_at           :datetime
#  confirmation_sent_at   :datetime
#  invitation_token       :string
#  invitation_created_at  :datetime
#  invitation_sent_at     :datetime
#  invitation_accepted_at :datetime
#  other_organisation     :string
#  dmponline3             :boolean
#  accept_terms           :boolean
#  org_id                 :integer
#  api_token              :string
#  invited_by_id          :integer
#  invited_by_type        :string
#  language_id            :integer
#  recovery_email         :string
#  active                 :boolean          default("true")
#  department_id          :integer
#
# Indexes
#
#  users_email_key        (email) UNIQUE
#  users_language_id_idx  (language_id)
#  users_org_id_idx       (org_id)
#

class User < ActiveRecord::Base

  include ConditionalUserMailer
  include ValidationMessages
  include ValidationValues
  extend UniqueRandom
  prepend Dmpopidor::Models::User

  ##
  # Devise
  #   Include default devise modules. Others available are:
  #   :token_authenticatable, :confirmable,
  #   :lockable, :timeoutable and :omniauthable
  devise :invitable, :database_authenticatable, :registerable, :recoverable,
         :rememberable, :trackable, :validatable, :omniauthable,
         omniauth_providers: [:shibboleth, :orcid]


  ##
  # User Notification Preferences
  serialize :prefs, Hash

  # ================
  # = Associations =
  # ================


  has_and_belongs_to_many :perms, join_table: :users_perms

  belongs_to :language

  belongs_to :org

  belongs_to :department, required: false

  has_one  :pref

  has_many :answers

  has_many :notes

  has_many :exported_plans

  has_many :roles, dependent: :destroy

  has_many :plans, through: :roles


  has_many :user_identifiers

  has_many :identifier_schemes, through: :user_identifiers

  has_and_belongs_to_many :notifications, dependent: :destroy,
                          join_table: "notification_acknowledgements"


  # ===============
  # = Validations =
  # ===============

  validates :active, inclusion: { in: BOOLEAN_VALUES, message: INCLUSION_MESSAGE }

  validates :firstname, presence: { message: PRESENCE_MESSAGE }

  validates :surname, presence: { message: PRESENCE_MESSAGE }

  validates :org, presence: { message: PRESENCE_MESSAGE }

  # ==========
  # = Scopes =
  # ==========

  default_scope { includes(:org, :perms) }

  # Retrieves all of the org_admins for the specified org
  scope :org_admins, -> (org_id) {
    joins(:perms).where("users.org_id = ? AND perms.name IN (?) AND " +
                        "users.active = ?",
                        org_id,
                        ["grant_permissions",
                         "modify_templates",
                         "modify_guidance",
                         "change_org_details"],
                         true)
  }

  scope :search, -> (term) {
    search_pattern = "%#{term}%"
    # MySQL does not support standard string concatenation and since concat_ws
    # or concat functions do not exist for sqlite, we have to come up with this
    # conditional
    if ActiveRecord::Base.connection.adapter_name == "Mysql2"
      where("lower(concat_ws(' ', firstname, surname)) LIKE lower(?) OR " +
            "lower(email) LIKE lower(?)",
            search_pattern, search_pattern)
    else
      where("lower(firstname || ' ' || surname) LIKE lower(?) OR " +
            "email LIKE lower(?)", search_pattern, search_pattern)
    end
  }

  # =============
  # = Callbacks =
  # =============

  before_update :clear_other_organisation, :if => proc { org_id_changed? &&  org_id != Org.find_by(is_other: true).id }

  before_update :clear_department_id, if: :org_id_changed?

  after_update :delete_perms!, if: :org_id_changed?, unless: :can_change_org?

  after_update :remove_token!, if: :org_id_changed?, unless: :can_change_org?

  # =================
  # = Class methods =
  # =================

  ##
  # Load the user based on the scheme and id provided by the Omniauth call
  def self.from_omniauth(auth)
    joins(user_identifiers: :identifier_scheme)
      .where(user_identifiers: { identifier: auth.uid },
             identifier_schemes: { name: auth.provider.downcase }).first
  end

  def self.to_csv(users)
    User::AtCsv.new(users).to_csv
  end
  # ===========================
  # = Public instance methods =
  # ===========================

  # This method uses Devise's built-in handling for inactive users
  #
  # Returns Boolean
  def active_for_authentication?
    super && active?
  end

  # EVALUATE CLASS AND INSTANCE METHODS BELOW
  #
  # What do they do? do they do it efficiently, and do we need them?

  # Determines the locale set for the user or the organisation he/she belongs
  #
  # Returns String
  # Returns nil
  def get_locale
    if !self.language.nil?
      self.language.abbreviation
    elsif !self.org.nil?
      self.org.get_locale
    else
      nil
    end
  end

  # Gives either the name of the user, or the email if name unspecified
  #
  # user_email - Use the email if there is no firstname or surname (defaults: true)
  #
  # Returns String
  def name(use_email = true)
    if (firstname.blank? && surname.blank?) || use_email then
      email
    else
      name = "#{firstname} #{surname}"
      name.strip
    end
  end

  # The user's identifier for the specified scheme name
  #
  # scheme - The identifier scheme name (e.g. ORCID)
  #
  # Returns UserIdentifier
  def identifier_for(scheme)
    user_identifiers.where(identifier_scheme: scheme).first
  end

  # Checks if the user is a super admin. If the user has any privelege which requires
  # them to see the super admin page then they are a super admin.
  #
  # Returns Boolean
  def can_super_admin?
    self.can_add_orgs? || self.can_grant_api_to_orgs? || self.can_change_org?
  end

  # Checks if the user is an organisation admin if the user has any privlege which
  # requires them to see the org-admin pages then they are an org admin.
  #
  # Returns Boolean
  def can_org_admin?
    self.can_grant_permissions? || self.can_modify_guidance? ||
      self.can_modify_templates? || self.can_modify_org_details? ||
      self.can_review_plans?
  end

  # Can the User add new organisations?
  #
  # Returns Boolean
  def can_add_orgs?
    perms.include? Perm.add_orgs
  end

  # Can the User change their organisation affiliations?
  #
  # Returns Boolean
  def can_change_org?
    perms.include? Perm.change_affiliation
  end

  # Can the User can grant their permissions to others?
  #
  # Returns Boolean
  def can_grant_permissions?
    perms.include? Perm.grant_permissions
  end

  # Can the User modify organisation templates?
  #
  # Returns Boolean
  def can_modify_templates?
    self.perms.include? Perm.modify_templates
  end

  # Can the User modify organisation guidance?
  #
  # Returns Boolean
  def can_modify_guidance?
    perms.include? Perm.modify_guidance
  end

  # Can the User use the API?
  #
  # Returns Boolean
  def can_use_api?
    perms.include? Perm.use_api
  end

  # Can the User modify their org's details?
  #
  # Returns Boolean
  def can_modify_org_details?
    perms.include? Perm.change_org_details
  end

  ##
  # Can the User grant the api to organisations?
  #
  # Returns Boolean
  def can_grant_api_to_orgs?
    perms.include? Perm.grant_api
  end


  ##
  # Can the user review their organisation's plans?
  #
  # Returns Boolean
  def can_review_plans?
    perms.include? Perm.review_plans
  end

  # Removes the api_token from the user
  #
  # Returns nil
  # Returns Boolean
  def remove_token!
    return if new_record?
    update_column(:api_token, nil)
  end

  # Generates a new token for the user unless the user already has a token.
  #
  # Returns nil
  # Returns Boolean
  def keep_or_generate_token!
    if api_token.nil? || api_token.empty?
      new_token = User.unique_random(field_name: 'api_token')
      update_column(:api_token, new_token)  unless new_record?
    end
  end

  # The User's preferences for a given base key
  #
  # Returns Hash
  def get_preferences(key)
    defaults = Pref.default_settings[key.to_sym] || Pref.default_settings[key.to_s]

    if pref.present?
      existing = pref.settings[key.to_s].deep_symbolize_keys

      # Check for new preferences
      defaults.keys.each do |grp|
        defaults[grp].keys.each do |pref, v|
          # If the group isn't present in the saved values add all of it's preferences
          existing[grp] = defaults[grp] if existing[grp].nil?
          # If the preference isn't present in the saved values add the default
          existing[grp][pref] = defaults[grp][pref] if existing[grp][pref].nil?
        end
      end
      existing
    else
      defaults
    end
  end

  # Override devise_invitable email title
  def deliver_invitation(options = {})
    super(options.merge(subject: d_('dmpopidor', '%{user_name} has shared a Data Management Plan with you in %{tool_name}') % {
      user_name: self.invited_by.name(false),
      tool_name: Rails.configuration.branding[:application][:name]
      }))
  end

  # Case insensitive search over User model
  #
  # field - The name of the field being queried
  # val   - The String to search for, case insensitive. val is duck typed to check
  #         whether or not downcase method exist.
  #
  # Returns ActiveRecord::Relation
  # Raises ArgumentError
  def self.where_case_insensitive(field, val)
    unless columns.map(&:name).include?(field.to_s)
      raise ArgumentError, "Field #{field} is not present on users table"
    end
    User.where("LOWER(#{field}) = :value", value: val.to_s.downcase)
  end

  # Acknowledge a Notification
  #
  # notification - Notification to acknowledge
  #
  # Returns ActiveRecord::Associations::CollectionProxy
  # Returns nil
  def acknowledge(notification)
    notifications << notification if notification.dismissable?
  end

  
  # remove personal data from the user account and save
  # leave account in-place, with org for statistics (until we refactor those)
  #
  # Returns boolean
  # SEE MODULE
  def archive
    self.firstname = 'Deleted'
    self.surname = 'User'
    self.email = User.unique_random(field_name: 'email',
      prefix: 'user_',
      suffix: Rails.configuration.branding[:application].fetch(:archived_accounts_email_suffix, '@example.org'),
      length: 5)
    self.recovery_email = nil
    self.api_token = nil
    self.encrypted_password = nil
    self.last_sign_in_ip = nil
    self.current_sign_in_ip =  nil
    self.active = false
    return self.save
  end

  def merge(to_be_merged)
    # merge logic
    # => answers -> map id
    to_be_merged.answers.update_all(user_id: self.id)
    # => notes -> map id
    to_be_merged.notes.update_all(user_id: self.id)
    # => plans -> map on id roles
    to_be_merged.roles.update_all(user_id: self.id)
    # => prefs -> Keep's from self
    # => auths -> map onto keep id only if keep does not have the identifier
    to_be_merged.user_identifiers.
          where.not(identifier_scheme_id: self.identifier_scheme_ids)
          .update_all(user_id: self.id)
    # => ignore any perms the deleted user has
    to_be_merged.destroy
  end

  private

  # ============================
  # = Private instance methods =
  # ============================

  def delete_perms!
    perms.destroy_all
  end

  def clear_other_organisation
    self.other_organisation = nil
  end

  def clear_department_id
    self.department_id = nil
  end

end