pupilfirst/pupilfirst

View on GitHub
app/services/concerns/course_exportable.rb

Summary

Maintainability
A
1 hr
Test Coverage
A
96%
module CourseExportable
  extend ActiveSupport::Concern

  def initialize(course_export)
    @course_export = course_export
    @cohorts = course_export.cohorts

    add_custom_styles
  end

  def finalize(tables)
    tables.each do |table|
      spreadsheet.table(table[:title]) do |rodf_table|
        rodf_table.add_rows table[:rows]
      end
    end

    io = StringIO.new(spreadsheet.bytes)

    @course_export.json_data = tables.to_json
    @course_export.file.attach(
      io: io,
      filename: filename,
      content_type: "application/vnd.oasis.opendocument.spreadsheet"
    )
    @course_export.save!
  end

  def spreadsheet
    @spreadsheet ||= RODF::Spreadsheet.new
  end

  def add_custom_styles
    spreadsheet.office_style "passing-grade", family: :cell do
      property :cell, "background-color" => "#9AE6B4"
    end

    spreadsheet.office_style "failing-grade", family: :cell do
      property :cell, "background-color" => "#FEB2B2"
    end

    spreadsheet.office_style "pending-grade", family: :cell do
      property :cell, "background-color" => "#FAF089"
    end
  end

  def course
    @course_export.course
  end

  def filename
    name = "#{course.name}-#{Time.zone.now.iso8601}".parameterize
    "#{name}.ods"
  end

  def assign_styled_grade(grade_index, grading, submission)
    evaluation_grade = submission.timeline_event_grades.pluck(:grade).join(",")

    # Determine the grade and style based on submission and evaluation status
    grade, style =
      case [submission.passed?, evaluation_grade.present?]
      when [true, true]
        # Case when the submission has passed and there is an evaluation grade
        # Use the evaluation grade and mark it as a passing grade
        [evaluation_grade, "passing-grade"]
      when [true, false]
        # Case when the submission has passed and there is no evaluation grade
        # Use the quiz score as the grade, or a checkmark if the score is not available
        [submission.quiz_score || "✓", "default"]
      when [false, false]
        if submission.evaluated?
          # Case when the submission has not passed, but has been evaluated
          # Mark it as failing
          %w[x failing-grade]
        else
          # Case when the submission has not passed and has not been evaluated
          # Mark it as pending grading
          %w[RP pending-grade]
        end
      when [false, true]
        # Case when the submission has not passed and there is an evaluation grade
        # This should be impossible - submissions can be graded only after they're accepted; crash if this happens
        raise "Submission #{submission.id} responded `false` to `#passed?` but has one or more evaluation grades"
      end

    append_grade(grading, grade_index, grade, style)
  end

  # If a grade has already been stored, separate it from the next one with a semi-colon in the same cell.
  def append_grade(grading, grade_index, grade, style)
    # Store the grade as a number if we're not dealing with a complex grade.
    parsed_grade =
      begin
        integer_grade = grade.to_i
        integer_grade.to_s == grade ? integer_grade : grade
      end

    value =
      if grading[grade_index].present?
        "#{grading[grade_index][:value]};#{parsed_grade}"
      else
        parsed_grade
      end

    grading[grade_index] = { value: value, style: style }

    grading
  end

  def targets(role: nil)
    @targets ||=
      begin
        scope =
          course.targets.live.includes(
            :level,
            :target_group,
            assignments: %i[quiz evaluation_criteria]
          )

        scope =
          case role
          when Assignment::ROLE_STUDENT
            scope.where(assignments: { role: Assignment::ROLE_STUDENT })
          when Assignment::ROLE_TEAM
            scope.where(assignments: { role: Assignment::ROLE_TEAM })
          else
            scope
          end

        scope =
          (
            if @course_export.reviewed_only
              scope.where.not(assignments: { evaluation_criteria: { id: nil } })
            else
              scope
            end
          )

        scope.order(
          "levels.number ASC, target_groups.sort_index ASC, targets.sort_index ASC"
        ).load
      end
  end

  def target_id(target)
    "L#{target.level.number}T#{target.id}"
  end

  def target_type(target)
    assignment = target.assignments.not_archived.first
    if assignment
      if assignment.evaluation_criteria.present?
        "Graded"
      elsif assignment.quiz.present?
        "Take Quiz"
      elsif assignment.checklist.present?
        "Submit Form"
      else
        "Mark as Read"
      end
    else
      "Mark as Read"
    end
  end

  def milestone?(target)
    assignment = target.assignments.not_archived.first
    if assignment
      assignment.milestone? ? "Yes" : "No"
    else
      "No"
    end
  end

  def tags
    @course_export.tags.pluck(:name)
  end
end