mumuki/mumuki-domain

View on GitHub
app/models/user.rb

Summary

Maintainability
C
1 day
Test Coverage
class User < ApplicationRecord
  include Mumuki::Domain::Syncable
  include WithProfile,
          WithUserNavigation,
          WithReminders,
          WithNotifications,
          WithDiscussionCreation,
          Awardee,
          WithTermsAcceptance,
          WithPreferences,
          Onomastic,
          Mumuki::Domain::Helpers::User

  prepend WithDeletedUser

  serialize :permissions, Mumukit::Auth::Permissions
  serialize :ignored_notifications, Array

  before_destroy :clean_belongings!

  has_many :api_clients,                             dependent: :delete_all
  has_many :assignments, foreign_key: :submitter_id, dependent: :delete_all
  has_many :certificates,                            dependent: :delete_all
  has_many :exam_authorizations,                     dependent: :delete_all
  has_many :exam_authorization_requests,             dependent: :delete_all
  has_many :notifications,                           dependent: :delete_all
  has_many :indicators,                              dependent: :delete_all
  has_many :user_stats, class_name: 'UserStats',     dependent: :delete_all

  has_many :discussions, foreign_key: :initiator_id
  has_many :forum_messages, -> { where.not(discussion_id: nil)  }, class_name: 'Message', foreign_key: :sender_id
  has_many :direct_messages, -> { order(created_at: :desc) }, through: :assignments, source: :messages

  has_many :submitted_exercises, through: :assignments, class_name: 'Exercise', source: :exercise

  has_many :solved_exercises,
           -> { where('assignments.submission_status' => Mumuki::Domain::Status::Submission::Passed.to_i) },
           through: :assignments,
           class_name: 'Exercise',
           source: :exercise

  belongs_to :last_exercise, class_name: 'Exercise', optional: true
  belongs_to :last_organization, class_name: 'Organization', optional: true

  has_one :last_guide, through: :last_exercise, source: :guide

  has_many :exams, through: :exam_authorizations

  enum gender: %i(female male other unspecified)
  belongs_to :avatar, polymorphic: true, optional: true

  before_validation :set_uid!
  validates :uid, presence: true

  validates :terms_of_service, acceptance: true
  after_save :welcome_to_new_organizations!, if: :gained_access_to_new_orga?
  after_initialize :init
  PLACEHOLDER_IMAGE_URL = 'user_shape.png'.freeze

  resource_fields :uid, :social_id, :email, :permissions, :verified_first_name, :verified_last_name, *profile_fields
  with_temporary_token :delete_account_token

  organic_on :notifications, :assignments

  def last_lesson
    last_guide.try(:lesson)
  end

  def messages_in_organization(organization = Organization.current)
    direct_messages.where('assignments.organization': organization)
  end

  def passed_submissions_count_in(organization)
    assignments.where(top_submission_status: Mumuki::Domain::Status::Submission::Passed.to_i, organization: organization).count
  end

  def submissions_count
    assignments.pluck(:submissions_count).sum
  end

  def passed_submissions_count
    passed_assignments.count
  end

  def submitted_exercises_count
    submitted_exercises.count
  end

  def solved_exercises_count
    solved_exercises.count
  end

  def passed_assignments
    assignments.where(status: Mumuki::Domain::Status::Submission::Passed.to_i)
  end

  def visit!(organization)
    update!(last_organization: organization) if organization != last_organization
  end

  def to_s
    "#{id}:#{name}:#{uid}"
  end

  def never_submitted?
    last_submission_date.nil?
  end

  def clear_progress!
    transaction do
      update! last_submission_date: nil, last_exercise: nil
      assignments.destroy_all
    end
  end

  def restore_organization_progress!(organization)
    assignments_in_organization(organization).each do |assignment|
      assignment.tap(&:parent).save!
    end
  end

  def has_assignments_in_organization?(organization)
    assignments_in_organization(organization).exists?
  end

  def accept_invitation!(invitation)
    make_student_of! invitation.course_slug
  end

  def transfer_progress_to!(another)
    transaction do
      assignments.update_all(submitter_id: another.id)
      if another.never_submitted? || last_submission_date.try { |it| it > another.last_submission_date }
        another.update! last_submission_date: last_submission_date,
                        last_exercise: last_exercise,
                        last_organization: last_organization
      end
    end
    reload
  end

  def import_from_resource_h!(json)
    update! self.class.slice_resource_h json
  end

  def to_resource_h
    super.merge(image_url: profile_picture)
  end

  def verify_name!(force: false)
    if force
      self.verified_first_name = first_name
      self.verified_last_name = last_name
    else
      self.verified_first_name ||= first_name
      self.verified_last_name ||= last_name
    end
    save!
  end

  def unsubscribe_from_reminders!
    update! accepts_reminders: false
  end

  def attach!(role, course)
    add_permission! role, course.slug
    save_and_notify!
  end

  def detach!(role, course, deep: false)
    make_ex_student_of! course.slug if !deep && student_of?(course.slug) && solved_any_exercises?(course.organization)
    remove_permission! role, course.slug
    save_and_notify!
  end

  def interpolations
    {
        'user_email' => email,
        'user_first_name' => first_name,
        'user_last_name' => last_name
    }
  end

  def currently_in_exam?
    exams.any? { |e| e.in_progress_for? self }
  end

  def custom_profile_picture
    avatar&.image_url || image_url
  end

  def profile_picture
    custom_profile_picture || placeholder_image_url
  end

  def bury!
    # TODO change avatar
    update! self.class.buried_profile.merge(accepts_reminders: false, gender: nil, birthdate: nil)
  end

  # Takes a didactic - ordered - sequence of content containers
  # and returns those that have been completed
  def completed_containers(sequence, organization)
    sequence.take_while { |it| it.content.completed_for?(self, organization) }
  end

  # Like `completed_containers`, returns a slice of the completed containers
  # in the sequence, but adding a configurable number of trailing, non-completed contaienrs
  def completed_containers_with_lookahead(sequence, organization, lookahead: 1)
    raise 'invalid lookahead' if lookahead < 1

    count = completed_containers(sequence, organization).size
    sequence[0..count + lookahead - 1]
  end

  def can_discuss_in?(organization)
    organization.access_mode(self).discuss_here?
  end

  def trusted_as_discusser_in?(organization)
    trusted_for_forum? || !organization.forum_only_for_trusted?
  end

  def can_discuss_here?
    can_discuss_in? Organization.current
  end

  def can_access_teacher_info_in?(organization)
    teacher_of?(organization) || organization.teacher_training?
  end

  def name_initials
    name.split.map(&:first).map(&:capitalize).join(' ')
  end

  def abbreviated_name
    "#{first_name} #{last_name.first.capitalize + '.' if last_name.present?}".strip
  end

  def progress_at(content, organization)
    Indicator.find_or_initialize_by(user: self, organization: organization, content: content)
  end

  def build_assignment(exercise, organization)
    Assignment.build_for(self, exercise, organization)
  end

  def pending_siblings_at(content)
    content.pending_siblings_for(self)
  end

  def next_exercise_at(guide)
    guide.pending_exercises(self).order('exercises.number asc').first
  end

  def run_submission!(submission, assignment, evaluation)
    submission.run! assignment, evaluation
  end

  def incognito?
    false
  end

  def current_audience
    current_organic_context&.target_audience
  end

  def placeholder_image_url
    PLACEHOLDER_IMAGE_URL
  end

  def age
    if birthdate.present?
      @age ||= Time.current.round_years_since(birthdate.in_time_zone)
    end
  end

  def current_organic_context
    Organization.current? ?  Organization.current : main_organization
  end

  def current_immersive_context_at(path_item)
    if Organization.current?
      immersive_organization_at(path_item) || Organization.current
    else
      main_organization
    end
  end

  def notify_permissions_changed!
    return if permissions_before_last_save == permissions
    Mumukit::Nuntius.notify! 'user-permissions-changed', user: {
      uid: uid,
      old_permissions: permissions_before_last_save.as_json,
      new_permissions: permissions.as_json
    }
  end

  def solved_any_exercises?(organization = Organization.current)
    Assignment.exists?(organization: organization, submitter: self)
  end

  def save_and_notify!
    save!
    notify_permissions_changed!
    self
  end

  def current_immersive_context_and_content_at(path_item)
    immersive_organization_with_content_at(path_item).tap do |orga, _|
      return [Organization.current, path_item] unless orga.present?
    end
  end

  def certificates_in_organization(organization = Organization.current)
    certificates.where certificate_program: CertificateProgram.where(organization: organization)
  end

  def certificated_in?(certificate_program)
    certificates.where(certificate_program: certificate_program).exists?
  end

  def certificate_in(certificate_program, certificate_h)
    return if certificated_in?(certificate_program)
    certificate = certificates.create certificate_h.merge(certificate_program: certificate_program)
    UserMailer.certificate(certificate).post!
  end

  def clear_progress_for!(organization)
    location = { organization: organization }.compact

    target_assignments = assignments.where(location)

    direct_messages.where(assignment: target_assignments).delete_all

    target_assignments.delete_all
    indicators.where(location).delete_all
    user_stats.where(location).delete_all
  end

  def notify_via_email!(notification)
    UserMailer.notification(notification).post! unless ignores_notification? notification
  end

  def ignores_notification?(notification)
    ignored_notifications.include? notification.subject
  end

  def clean_belongings!
    discussions.update_all initiator_id: User.deleted_user.id
    forum_messages.update_all sender_id: User.deleted_user.id
    direct_messages.where(sender: self).delete_all
  end

  private

  def welcome_to_new_organizations!
    new_accessible_organizations.each do |organization|
      UserMailer.welcome_email(self, organization).post! rescue nil if organization.greet_new_users?
    end
  end

  def gained_access_to_new_orga?
    new_accessible_organizations.present?
  end

  def new_accessible_organizations
    return [] unless saved_change_to_permissions?

    old, new = saved_change_to_permissions
    new_organizations = (new.any_granted_organizations - old.any_granted_organizations).to_a
    Organization.where(name: new_organizations)
  end

  def set_uid!
    self.uid ||= email
  end

  def init
    if custom_profile_picture.blank?
      self.avatar = Avatar.sample_for(self)
      save if persisted?
    end
  end

  def self.sync_key_id_field
    :uid
  end

  def self.create_if_necessary(user)
    user[:uid] ||= user[:email]
    where(uid: user[:uid]).first_or_create(user)
  end

  # Call this method once as part of application initialization
  # in order to enable user profile override as part of disabling process
  def self.configure_buried_profile!(profile)
    @buried_profile = profile
  end

  def self.buried_profile
    (@buried_profile || {}).slice(:first_name, :last_name, :email)
  end
end