autolab/Autolab

View on GitHub
app/controllers/assessment/handin.rb

Summary

Maintainability
F
3 days
Test Coverage
require "pathname"

##
# Handles different handin methods, including web form, local_submit and log_submit
#
module AssessmentHandin
  include AssessmentHandinCore

  # handin - The generic default handin function.
  # This function calls out to smaller helper functions which provide for
  # specific functionality.
  #
  # validateHandin_forHTML() : Returns true or false if the handin is valid.
  # saveHandin() : Does the actual process of saving the handin to the
  #     database and writing the handin file to Disk.
  # sendJob_AddHTMLMessages(course, assessment, submissions): Autogrades the submission.
  #
  # validateHandin_forHTML() cannot modify the state of the world in any way. And it should
  # call super() to enable any other functionality.  The only reason to not call super()
  # is if you want to prevent other functionality. You should be very careful about this.
  #
  # Any errors should be added to flash[:error] and return false or nil.
  def handin
    if @assessment.disable_handins?
      flash[:error] = "Sorry, handins are disabled for this assessment."
      redirect_to(action: :show)
      return false
    end

    if @assessment.embedded_quiz
      contents = params[:submission]["embedded_quiz_form_answer"].to_s
      out_file = Tempfile.new('out.txt-')
      out_file.puts(contents)
      params[:submission]["file"] = out_file
    elsif @assessment.github_submission_enabled && params["github_submission"].present?
      # get code from Github
      github_integration = current_user.github_integration

      begin
        @tarfile_path = github_integration.clone_repo(params["repo"], params["branch"], params["commit"], @assessment.max_size * (2 ** 20))
      rescue StandardError => msg
        flash[:error] = msg
        redirect_to(action: :show)
        return
      end

      # Populate submission field for validation
      params[:submission] = { "tar" => @tarfile_path }
      git_tarfile_cleanup_path = @tarfile_path

      redirect_to(action: :show) && return unless validateHandin_forGit
    else
      # validate the handin
      redirect_to(action: :show) && return unless validateHandin_forHTML
    end

    # save the submissions
    begin
      submissions = saveHandin(params[:submission])
      if git_tarfile_cleanup_path
        system *%W(rm #{git_tarfile_cleanup_path})
      end
    rescue StandardError => exception
      ExceptionNotifier.notify_exception(exception, env: request.env,
                                                    data: {
                                                      user: current_user,
                                                      course: @course,
                                                      assessment: @assessment,
                                                    })

      COURSE_LOGGER.log("could not save handin: #{exception.class} (#{exception.message})")
      flash[:error] = exception.message
      submissions = nil
    end

    if @assessment.embedded_quiz
      out_file.close
      out_file.unlink
    end

    # make sure submission was correctly constructed and saved
    unless submissions
      flash[:error] ||= "There was an error handing in your submission."
      redirect_to(action: :show) && return
    end

    # autograde the submissions only if there are problems defined
    if @assessment.problems.length == 0
      flash[:error] = "There are no problems in this assessment."
    elsif @assessment.has_autograder?
      begin
        sendJob_AddHTMLMessages(@course, @assessment, submissions)
      rescue AssessmentAutogradeCore::AutogradeError => e
        # error message already filled in by sendJob_AddHTMLMessages, we just
        # log the error message
        COURSE_LOGGER.log("SendJob failed for #{submissions[0].id}\n
          User error message: #{flash[:error]}\n
          error name: #{e.error_code}\n
          additional error data: #{e.additional_data}")
      end
    end

    redirect_to([:history, @course, @assessment]) && return
  end

  # method called when student makes
  # unofficial submission in the database
  def local_submit
    @user = User.find_by(email: params[:user])
    @cud = @user ? @course.course_user_data.find_by(user_id: @user.id) : nil
    unless @cud
      err = "ERROR: invalid username (#{params[:user]}) for class #{@course.id}"
      render(plain: err, status: :bad_request) && return
    end

    @assessment = @course.assessments.find_by(name: params[:name])
    if !@assessment
      err = "ERROR: Invalid Assessment (#{params[:id]}) for course #{@course.id}"
      render(plain: err, status: :bad_request) && return
    elsif @assessment.remote_handin_path.nil? || @assessment.remote_handin_path.empty?
      err = "ERROR: Remote handins have not been enabled by the instructor."
      render(plain: err, status: :bad_request) && return
    end

    personal_directory = @user.email + "_remote_handin_" + @assessment.name
    remote_handin_dir = File.join(@assessment.remote_handin_path, personal_directory)
    remote_handin_path = Pathname.new(@assessment.remote_handin_path).expand_path
    remote_handin_dir_path = Pathname.new(remote_handin_dir).expand_path

    # https://stackoverflow.com/questions/39581798/check-if-file-folder-is-in-a-subdirectory-in-ruby
    # Validate that the handin directory lies strictly within remote handin path
    # Note: The fnmatch? check is ALMOST sufficient, except when the paths are /
    is_dir_underneath = ((remote_handin_dir_path.fnmatch? File.join(remote_handin_path.to_s, "**")) and
      (remote_handin_dir_path != remote_handin_path))

    unless is_dir_underneath
      # No way to reasonably handle this, so return an error
      render(plain: "Unable to create handin directory for user", status: :bad_request) && return
    end

    if params[:submit]
      # They've copied their handin over, lets go grab it.
      begin
        handin_file = params[:submit]

        if @assessment.max_submissions != -1
          submission_count = @cud.submissions.where(assessment: @assessment).size
          if submission_count >= @assessment.max_submissions
            render(plain: "You have no remaining submissions for this assessment",
                   status: :bad_request) && return
          end
        end

        render(plain: flash[:error], status: :bad_request) && return unless validateHandinForGroups_forHTML

        remote_handin_file = File.join(remote_handin_dir, handin_file)
        remote_handin_file_path = Pathname.new(remote_handin_file).expand_path

        # Validate that the handin file lies strictly within the handin directory
        is_file_underneath = ((remote_handin_file_path.fnmatch? File.join(remote_handin_dir_path.to_s, "**")) and
          (remote_handin_file_path != remote_handin_dir_path))

        unless is_file_underneath
          render(plain: "Invalid path to file", status: :bad_request) && return
        end

        # save the submissions
        begin
          submissions = saveHandin("local_submit_file" => remote_handin_file_path)
        rescue StandardError => e
          ExceptionNotifier.notify_exception(e, env: request.env,
                                                data: {
                                                  user: current_user,
                                                  course: @course,
                                                  assessment: @assessment,
                                                })
          COURSE_LOGGER.log("Error Saving Submission:\n#{e}")
          flash[:error] = exception.message
          submissions = nil
        end

        # make sure submission was correctly constructed and saved
        unless submissions
          flash[:error] ||= "There was an error handing in your submission."
          render(plain: flash[:error], status: :bad_request) && return
        end

        # autograde the submissions
        sendJob_AddHTMLMessages(@course, @assessment, submissions) if @assessment.has_autograder?
      rescue StandardError => e
        ExceptionNotifier.notify_exception(e, env: request.env,
                                              data: {
                                                user: current_user,
                                                course: @course,
                                                assessment: @assessment,
                                                submission: submissions[0],
                                              })
        COURSE_LOGGER.log(e.to_s)
      end

      if submissions
        COURSE_LOGGER.log("Submission received, ID##{submissions[0].id}")
      else
        err = "There was an error saving your submission. Please contact your course staff\n"
        render(plain: err, status: :bad_request) && return
      end

      if @assessment.max_submissions != -1
        remaining = @assessment.max_submissions - submissions.count
        render(plain: " - You have #{remaining} submissions left\n") && return
      end

      render(plain: "Successfully submitted\n") && return
    else

      # Create a handin directory for them.

      # The handin Directory really should not exist, as this script deletes it
      # when it's done.  However, if it's there, we'll try to remove an empty
      # folder, else fail w/ error message.
      if Dir.exist?(remote_handin_dir)
        begin
          FileUtils.rm_rf(remote_handin_dir)
        rescue SystemCallError => exception
          ExceptionNotifier.notify_exception(exception, env: request.env,
                                                        data: {
                                                          user: current_user,
                                                          course: @course,
                                                          assessment: @assessment,
                                                        })
          render(plain: "WARNING: could not clear previous handin directory, please") && return
        end
      end

      begin
        Dir.mkdir(remote_handin_dir)
      rescue SystemCallError
        ExceptionNotifier.notify_exception(exception, env: request.env,
                                                      data: {
                                                        user: current_user,
                                                        course: @course,
                                                        assessment: @assessment,
                                                      })
        COURSE_LOGGER.log("ERROR: Could not create handin directory. Please contact
        #{Rails.configuration.school["support_email"]} with this error")
      end

      system("fs sa #{remote_handin_dir} #{@user.email} rlidw")
    end

    render(plain: remote_handin_dir) && return
  end

  # method called when student makes
  # log submission in the database
  def log_submit
    @user = User.find_by(email: params[:user])
    @cud = @user ? @course.course_user_data.find_by(user_id: @user.id) : nil
    unless @cud
      err = "ERROR: invalid username (#{params[:user]}) for class #{@course.id}"
      render(plain: err, status: :bad_request) && return
    end

    @assessment = @course.assessments.find_by(name: params[:name])
    if !@assessment
      err = "ERROR: Invalid Assessment (#{params[:id]}) for course #{@course.id}"
      render(plain: err, status: :bad_request) && return
    elsif !@assessment.allow_unofficial
      err = "ERROR: This assessment does not allow Log Submissions"
      render(plain: err, status: :bad_request) && return
    end

    @result = params[:result]
    render(plain: "ERROR: No result!", status: :bad_request) && return unless @result

    # Everything looks OK, so append the autoresult to the log.txt file for this lab
    ASSESSMENT_LOGGER.setAssessment(@assessment)
    ASSESSMENT_LOGGER.log("#{@user.email},0,#{@result}")

    # Load up the lab.rb file
    mod_name = @assessment.name + (@course.name).gsub(/[^A-Za-z0-9]/, "")
    require(Rails.root.join("assessmentConfig", "#{@course.name}-#{@assessment.name}.rb"))
    eval("extend #{mod_name.camelcase}")

    begin
      # Call the parseAutoresult function defined in the lab.rb file.  If
      # the list of scores it returns is empty, then we the lab developer is
      # asking us not to create an unofficial submission in the
      # database. Simply return a successful status string to the client and
      # exit.
      scores = parseAutoresult(@result, false)

      render(plain: "OK", status: 200) && return if scores.keys.length == 0

      # Try to find an existing submission (always version 0).
      submission = @assessment.submissions.find_by(version: 0, course_user_datum_id: @cud.id)
      if !submission
        submission = @assessment.submissions.new(
          version: 0,
          autoresult: @result,
          user_id: @cud.id,
          submitted_by_id: 0,
        )
        submission.save!
      else
        # update this one
        submission.autoresult = @result
        submission.created_at = Time.now
        submission.save!
      end

      # Update the scores in the db's unofficial submission using the list
      # returned by the parseAutoresult function
      scores.keys.each do |key|
        problem = @assessment.problems.find_by(name: key)
        score = submission.scores.find_or_initialize_by(problem_id: problem.id)
        score.score = scores[key]
        score.released = true
        score.grader_id = 0
        score.save!
      end
    rescue StandardError => e
      ExceptionNotifier.notify_exception(e, env: request.env,
                                            data: {
                                              user: current_user,
                                              course: @course,
                                              assessment: @assessment,
                                              submission: submission,
                                            })
      COURSE_LOGGER.log(e.to_s)
    end

    render(plain: "OK", status: 200) && return
  end

  private

  ##
  # this function checks that now is a valid time to submit and that the
  # submission file is okay to submit.
  #
  def validateHandin_forHTML
    if params[:submission].blank?
      flash[:error] = "Submission was blank - please upload again."
      return false
    end

    if params[:submission]["file"].blank? and params["repo"].blank?
      flash[:error] = "Submission was blank (file upload/Github repository missing) - please try again."
      return false
    end

    validity = validateHandin(params[:submission]["file"].size,
                              params[:submission]["file"].content_type,
                              params[:submission]["file"].original_filename)

    return handle_validity(validity)
  end

  ##
  # Validates Git tarfile
  #
  def validateHandin_forGit
    if @tarfile_path.blank?
      flash[:error] = "Git submission error"
      return false
    end

    validity = validateHandin(File.size(@tarfile_path),
                              MimeMagic.by_magic(File.open(@tarfile_path)).type,
                              @tarfile_path) # TODO probably want filename instead of path

    return handle_validity(validity)
  end

  ##
  # this function makes sure that the submitter's group can submit.
  # If the assessment does not have groups, or the user has no group,
  # this returns true.  Otherwise, it checks that everyone is confirmed
  # to be in the group and that no one is over the submission limit.
  #
  def validateHandinForGroups_forHTML
    validity = validateHandinForGroups

    case validity
    when :valid
      return true
    when :awaiting_member_confirmation
      msg = "You cannot submit until all group members confirm their group membership"
    when :group_submission_limit_exceeded
      msg = "A member of your group has reached the submission limit for this assessment"
    end

    flash[:error] = msg
    return false
  end

  def handle_validity(validity)
    case validity
    when :valid
      return validateHandinForGroups_forHTML
    when :handin_disabled
      msg = "Sorry, handins are disabled for this assessment."
    when :submission_empty
      msg = "Submission was blank - please upload again."
    when :file_too_large
      msg = "Your submission is larger than the max allowed " \
            "size (#{@assessment.max_size} MB) - please remove any " \
            "unnecessary logfiles and binaries."
    when :fail_type_check
      flash[:error] = "" if flash[:error].nil?
      msg = "Submission failed Filetype Check. " + flash[:error]
    end

    flash[:error] = msg
    return false
  end

  def set_handin
    submission_count = @assessment.submissions.where(course_user_datum_id: @cud.id).count
    @left_count = [@assessment.max_submissions - submission_count, 0].max
    @aud = AssessmentUserDatum.get @assessment.id, @cud.id
    @can_submit, @why_not = @aud.can_submit? Time.now

    @submission = Submission.new
  end
end