Coursemology/coursemology2

View on GitHub
app/models/course/assessment/answer/programming.rb

Summary

Maintainability
A
25 mins
Test Coverage
# frozen_string_literal: true
class Course::Assessment::Answer::Programming < ApplicationRecord
  # The table name for this model is singular.
  self.table_name = table_name.singularize

  acts_as :answer, class_name: 'Course::Assessment::Answer'

  has_many :files, class_name: 'Course::Assessment::Answer::ProgrammingFile',
                   foreign_key: :answer_id, dependent: :destroy, inverse_of: :answer

  # @!attribute [r] job
  #   This might be null if the job has been cleared.
  belongs_to :codaveri_feedback_job, class_name: 'TrackableJob::Job', inverse_of: nil, optional: true

  accepts_nested_attributes_for :files, allow_destroy: true

  def to_partial_path
    'course/assessment/answer/programming/programming'
  end

  # Specific implementation of Course::Assessment::Answer#reset_answer
  def reset_answer
    self.class.transaction do
      files.clear
      question.specific.copy_template_files_to(self)
      raise ActiveRecord::Rollback unless save
    end
    acting_as
  end

  MAX_ATTEMPTING_TIMES = 1000
  # Returns the attempting times left for current answer.
  # The max attempting times will be returned if question don't have the limit.
  #
  # @return [Integer]
  def attempting_times_left
    return MAX_ATTEMPTING_TIMES unless question.actable.attempt_limit

    times = question.actable.attempt_limit - submission.evaluated_or_graded_answers(question).size
    times = 0 if times < 0
    times
  end

  # Programming answers should be graded in a job.
  def grade_inline?
    false
  end

  def download(dir)
    files.each do |src_file|
      dst_path = File.join(dir, src_file.filename)
      File.open(dst_path, 'w') do |dst_file|
        dst_file.write(src_file.content)
      end
    end
  end

  def csv_download
    files.first.content
  end

  def assign_params(params)
    acting_as.assign_params(params)

    params[:files_attributes]&.each do |file_attributes|
      file = files.find { |f| f.id == file_attributes[:id].to_i }
      file.content = file_attributes[:content] if file.present?
    end
  end

  def create_and_update_files(params)
    params[:files_attributes]&.each do |file_attributes|
      file = files.find { |f| f.id == file_attributes[:id].to_i }
      if file.present?
        file.content = file_attributes[:content]
      else
        files.build(filename: file_attributes[:filename], content: file_attributes[:content])
      end
    end
    save
  end

  def delete_file(file_id)
    file = files.find { |f| f.id == file_id }
    file.mark_for_destruction if file.present?
    save
  end

  def generate_feedback
    codaveri_feedback_job&.status == 'submitted' ? codaveri_feedback_job : retrieve_codaveri_code_feedback&.job
  end

  def generate_live_feedback
    question = self.question.actable
    assessment = submission.assessment

    should_retrieve_feedback = submission.attempting? &&
                               current_answer? &&
                               question.live_feedback_enabled
    return unless should_retrieve_feedback

    feedback_config = Course::Assessment::Answer::ProgrammingCodaveriAsyncFeedbackService.default_config.merge(
      revealLevel: 'guidance',
      language: Course::Assessment::Answer::ProgrammingCodaveriAsyncFeedbackService.language_from_locale(
        answer.submission.creator.locale
      ),
      customPrompt: question.live_feedback_custom_prompt
    )
    feedback_service = Course::Assessment::Answer::ProgrammingCodaveriAsyncFeedbackService.
                       new(assessment, question, self, true, feedback_config)
    response_status, response_body, _feedback_job_id = feedback_service.run_codaveri_feedback_service
    unless [200, 201].include?(response_status) && response_body['success']
      raise CodaveriError,
            { status: response_status, body: response_body }
    end

    [response_status, response_body]
  end

  def retrieve_codaveri_code_feedback
    question = self.question.actable
    assessment = submission.assessment

    should_retrieve_feedback = question.is_codaveri && !submission.attempting? && current_answer?
    return unless should_retrieve_feedback

    feedback_job = Course::Assessment::Answer::ProgrammingCodaveriFeedbackJob.perform_later(
      assessment, question, self
    )
    update_column(:codaveri_feedback_job_id, feedback_job.job_id)
    feedback_job
  end

  def compare_answer(other_answer)
    return false unless other_answer.is_a?(Course::Assessment::Answer::Programming)

    same_file_length = files.length == other_answer.files.length
    answer_filename_content = files.pluck(:filename, :content).map { |elem| elem.join('_') }
    other_answer_filename_content = other_answer.files.pluck(:filename, :content).map { |elem| elem.join('_') }

    same_file = Set.new(answer_filename_content) == Set.new(other_answer_filename_content)
    same_file_length && same_file
  end
end