pupilfirst/pupilfirst

View on GitHub
app/models/target.rb

Summary

Maintainability
A
2 hrs
Test Coverage
B
87%
# frozen_string_literal: true

class Target < ApplicationRecord
  # Use to allow changing visibility of a target. See Targets::UpdateVisibilityService.
  attr_accessor :safe_to_change_visibility

  STATUS_COMPLETE = :complete
  STATUS_NEEDS_IMPROVEMENT = :needs_improvement
  STATUS_SUBMITTED = :submitted
  STATUS_PENDING = :pending
  STATUS_UNAVAILABLE = :unavailable # This handles two cases: targets that are not submittable, and ones with prerequisites pending.
  STATUS_NOT_ACCEPTED = :not_accepted
  STATUS_SUBMISSION_LIMIT_LOCKED = :submission_limit_locked # There are more pending submissions than the submission limit for the course
  STATUS_PENDING_MILESTONE = :pending_milestone # Milestone targets of the previous level are incomplete

  UNSUBMITTABLE_STATUSES = [
    STATUS_UNAVAILABLE,
    STATUS_SUBMISSION_LIMIT_LOCKED,
    STATUS_PENDING_MILESTONE
  ].freeze

  has_many :timeline_events, dependent: :restrict_with_error
  has_many :assignments, dependent: :restrict_with_error
  has_many :page_reads, dependent: :restrict_with_error
  belongs_to :target_group
  has_many :evaluation_criteria, through: :assignments
  has_one :level, through: :target_group
  has_one :course, through: :target_group
  has_many :topics, dependent: :restrict_with_error
  has_many :resource_versions, as: :versionable, dependent: :restrict_with_error
  has_many :target_versions, dependent: :destroy
  has_many :content_blocks, through: :target_versions
  has_many :text_versions, as: :versionable, dependent: :restrict_with_error

  acts_as_taggable

  scope :live, -> { where(visibility: VISIBILITY_LIVE) }
  scope :draft, -> { where(visibility: VISIBILITY_DRAFT) }
  scope :student, -> { where(role: ROLE_STUDENT) }
  scope :not_student, -> { where.not(role: ROLE_STUDENT) }
  scope :team, -> { where(role: ROLE_TEAM) }
  scope :sessions, -> { where.not(session_at: nil) }
  scope :non_assignment, -> { where.missing(:assignments) }
  scope :milestone,
        -> do
          joins(:assignments).where(
            assignments: {
              milestone: true,
              archived: false
            }
          )
        end

  TYPE_TODO = "Todo"
  TYPE_ATTEND = "Attend"
  TYPE_READ = "Read"
  TYPE_LEARN = "Learn"

  VISIBILITY_LIVE = "live"
  VISIBILITY_ARCHIVED = "archived"
  VISIBILITY_DRAFT = "draft"

  def self.valid_target_action_types
    [TYPE_TODO, TYPE_ATTEND, TYPE_READ, TYPE_LEARN].freeze
  end

  def self.valid_visibility_types
    [VISIBILITY_LIVE, VISIBILITY_ARCHIVED, VISIBILITY_DRAFT].freeze
  end

  validates :target_action_type,
            inclusion: {
              in: valid_target_action_types
            },
            allow_nil: true
  validates :title, presence: true
  validates :call_to_action, length: { maximum: 20 }
  validates :visibility,
            inclusion: {
              in: valid_visibility_types
            },
            allow_nil: true

  validate :days_to_complete_or_session_at_should_be_present
  validates_with RateLimitValidator, limit: 100, scope: :target_group_id

  def days_to_complete_or_session_at_should_be_present
    return if days_to_complete.blank? && session_at.blank?
    return if [days_to_complete, session_at].one?

    errors.add(:base, "One of days_to_complete, or session_at should be set.")
    errors.add(:days_to_complete, "if blank, session_at should be set")
    errors.add(:session_at, "if blank, days_to_complete should be set")
  end

  validate :avoid_level_mismatch_with_group

  def avoid_level_mismatch_with_group
    return if target_group.blank? || level.blank?
    return if level == target_group.level

    errors.add(:level, "should match level of target group")
  end

  validate :must_be_safe_to_change_visibility

  def must_be_safe_to_change_visibility
    unless visibility_changed? &&
             (visibility.in? [VISIBILITY_DRAFT, VISIBILITY_ARCHIVED])
      return
    end
    return if safe_to_change_visibility

    errors.add(:visibility, "cannot be modified unsafely")
  end

  validate :same_course_for_target_and_evaluation_criteria

  def same_course_for_target_and_evaluation_criteria
    return if evaluation_criteria.blank?

    evaluation_criteria.each do |ec|
      next if ec.course_id == course.id

      errors.add(
        :base,
        "Target and evaluation criterion must belong to same course"
      )
    end
  end

  normalize_attribute :slideshow_embed,
                      :video_embed,
                      :youtube_video_id,
                      :link_to_complete,
                      :completion_instructions

  def display_name
    if target_group.present?
      "#{course.short_name}##{level.number}: #{title}"
    else
      title
    end
  end

  def title_with_milestone
    assignment = assignments.not_archived.first
    return title unless assignment
    return title unless assignment.milestone?

    "#{I18n.t("shared.m")}#{assignment.milestone_number} - #{title}"
  end

  def status(student)
    @status ||= {}
    @status[student.id] ||= Targets::StatusService.new(self, student).status
  end

  def pending?(student)
    status(student) == STATUS_PENDING
  end

  def verified?(student)
    status(student) == STATUS_COMPLETE
  end

  def session?
    session_at.present?
  end

  def target?
    session_at.blank?
  end

  def quiz?
    quiz.present?
  end

  def mark_as_complete?
    not (quiz.present? or checklist.present? or link_to_complete.present?)
  end

  def team_target?
    assignment = assignments.not_archived.first
    assignment && assignment.role == Assignment::ROLE_TEAM
  end

  def individual_target?
    assignment = assignments.not_archived.first
    assignment && assignment.role == Assignment::ROLE_STUDENT
  end

  # Returns the latest submission linked to this target from a student
  def latest_submission(student)
    student.latest_submissions.where(target: self).last
  end

  def live?
    visibility == VISIBILITY_LIVE
  end

  def current_target_version
    target_versions.order(created_at: :desc).first
  end

  def current_content_blocks
    current_target_version.content_blocks
  end
end