Skip to content
This repository was archived by the owner on Jan 7, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions app/controllers/assessment/import.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
module AssessmentImport

def cleanTemp
begin
FileUtils.remove_dir(Rails.root.join("courses", @course.name, "importTemp"))
rescue Errno::ENOENT => e
return
end
end

def writeRBFiles(asmt_name)
course_root = Rails.root.join("courses", @course.name, "importTemp")
File.open(File.join(course_root, asmt_name, asmt_name + ".rb"), "wb") do |f|
f.write "require \"AssessmentBase.rb\"

module #{asmt_name.capitalize}
include AssessmentBase

def assessmentInitialize(course)
super(\"#{asmt_name}\",course)
@problems = []
end

end"
end
end

def importAsmtFromTar
cleanTemp
tarFile = params["tarFile"]
if tarFile.nil?
flash[:error] = "Please select an assessment tarball for uploading."
return
end

begin
tarFile = File.new(tarFile.open, "rb")
tar_extract = Gem::Package::TarReader.new(tarFile)
tar_extract.rewind
rescue StandardError => e
flash[:error] = "Error while reading the tarball -- #{e.message}."
return
end

# If all requirements are satisfied, extract assessment files.
begin
FileUtils.mkdir_p(Rails.root.join("courses", @course.name, "importTemp"))
course_root = Rails.root.join("courses", @course.name, "importTemp")
tar_extract.rewind
tar_extract.each do |entry|
relative_pathname = entry.full_name
if entry.directory?
FileUtils.mkdir_p(File.join(course_root, relative_pathname),
mode: entry.header.mode, verbose: false)
if !entry.full_name.chomp("/").include? "/"
writeRBFiles(entry.full_name.chomp("/"))
end
elsif entry.file?
FileUtils.mkdir_p(File.join(course_root, File.dirname(relative_pathname)),
mode: entry.header.mode, verbose: false)
File.open(File.join(course_root, relative_pathname), "wb") do |f|
f.write entry.read
end
FileUtils.chmod entry.header.mode, File.join(course_root, relative_pathname),
verbose: false
elsif entry.header.typeflag == "2"
File.symlink entry.header.linkname, File.join(course_root, relative_pathname)
end
end
tar_extract.close

rescue StandardError => e
flash[:error] = "Error while extracting tarball to server -- #{e.message}."
cleanTemp && return
end

Dir[Rails.root.join(course_root, "*")].select { |e| File.directory?(e) }.each do |directory|
props = YAML.load(File.open(Rails.root.join(directory, "properties.yml"), "r") { |f| f.read })

assessmentYAMLOk(props, directory)

return if !flash[:error].nil?

moveFiles(props, directory)

params["assessment_name"] = props["general"]["name"]

importAssessment
end

cleanTemp && return
end


def assessmentYAMLOk(props, directory)
unless !props["general"]["name"].nil?
flash[:error] = "the assessment in the folder #{directory.split("/")[-1]} has a YAML file which is missing the name property"
redirect_to(action: "multiImport") && return
end

if not @course.assessments.find_by(name: props["general"]["name"]).nil?
flash[:error] = "An assessment with the same name #{props["general"]["name"]} already exists for the course. Please use a different name."
redirect_to(action: "multiImport") and return
end

if props.has_key?("autograder")

if props["autograder"].has_key?("makefile") && props["autograder"]["makefile"] != ""
make = Dir[Rails.root.join(directory, props["autograder"]["makefile"])].select { |e| File.file?(e) }

if make.length != 1
flash[:error] = "the assessment in the folder #{directory.split("/")[-1]} has a YAML file which has the autograder property but no makefile provided"
redirect_to(action: "multiImport") && return
end

else
flash[:error] = "the assessment in the folder #{directory.split("/")[-1]} has a YAML file which has the autograder property but no makefile defined"
redirect_to(action: "multiImport") && return
end

if props["autograder"].has_key?("tarfile") && props["autograder"]["tarfile"] != ""
tar = Dir[Rails.root.join(directory, props["autograder"]["tarfile"])].select { |e| File.file?(e) }

if tar.length != 1
flash[:error] = "the assessment in the folder #{directory.split("/")[-1]} has a YAML file which has the autograder property but no tarfile provided"
redirect_to(action: "multiImport") && return
end

else
flash[:error] = "the assessment in the folder #{directory.split("/")[-1]} has a YAML file which has the autograder property but no tarfile defined"
redirect_to(action: "multiImport") && return
end
end
return true
end

def moveFiles(props, directory)
newAssessmentPath = Rails.root.join("courses", @course.name, directory.split("/")[-1]).to_s

begin
FileUtils.remove_dir(newAssessmentPath)
rescue Errno::ENOENT => e

end

FileUtils.mv(directory,newAssessmentPath)
FileUtils.mv(Rails.root.join(newAssessmentPath, "properties.yml"), Rails.root.join(newAssessmentPath, props["general"]["name"] + ".yml"))
if props.has_key?("autograder")
FileUtils.mv(Rails.root.join(newAssessmentPath, props["autograder"]["tarfile"]), Rails.root.join(newAssessmentPath, "autograde.tar")) unless props["autograder"]["tarfile"] == "autograde.tar"
FileUtils.mv(Rails.root.join(newAssessmentPath, props["autograder"]["makefile"]), Rails.root.join(newAssessmentPath, "autograde-Makefile")) unless props["autograder"]["makefile"] == "autograde-Makefile"
end
end
end

129 changes: 29 additions & 100 deletions app/controllers/assessments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ class AssessmentsController < ApplicationController
autolab_require Rails.root.join("app", "controllers", "assessment", "autograde.rb")
include AssessmentAutograde

autolab_require Rails.root.join("app", "controllers", "assessment", "import.rb")
include AssessmentImport

# this is inherited from ApplicationController
before_action :set_assessment, except: [:index, :new, :create, :installAssessment,
:importAsmtFromTar, :importAssessment,
:log_submit, :local_submit, :autograde_done]
:log_submit, :local_submit, :autograde_done, :multiImport]
before_action :set_submission, only: [:viewFeedback]

# We have to do this here, because the modules don't inherit ApplicationController.
Expand Down Expand Up @@ -100,81 +103,6 @@ def installAssessment
@unused_config_files.sort!
end

action_auth_level :importAsmtFromTar, :instructor
def importAsmtFromTar
tarFile = params["tarFile"]
if tarFile.nil?
flash[:error] = "Please select an assessment tarball for uploading."
redirect_to(action: "installAssessment")
return
end

begin
tarFile = File.new(tarFile.open, "rb")
tar_extract = Gem::Package::TarReader.new(tarFile)
tar_extract.rewind
is_valid_tar, asmt_name = valid_asmt_tar(tar_extract)
tar_extract.close
unless is_valid_tar
flash[:error] = "Invalid tarball. Please verify the existence of configuration files."
redirect_to(action: "installAssessment")
return
end
rescue StandardError => e
flash[:error] = "Error while reading the tarball -- #{e.message}."
redirect_to(action: "installAssessment")
return
end

# Check if the assessment already exists.
unless @course.assessments.find_by(name: asmt_name).nil?
flash[:error] = "An assessment with the same name already exists for the course. Please use a different name."
redirect_to(action: "installAssessment") && return
end

# If all requirements are satisfied, extract assessment files.
begin
course_root = Rails.root.join("courses", @course.name)
tar_extract.rewind
tar_extract.each do |entry|
relative_pathname = entry.full_name
if entry.directory?
FileUtils.mkdir_p(File.join(course_root, relative_pathname),
mode: entry.header.mode, verbose: false)
elsif entry.file?
FileUtils.mkdir_p(File.join(course_root, File.dirname(relative_pathname)),
mode: entry.header.mode, verbose: false)
File.open(File.join(course_root, relative_pathname), "wb") do |f|
f.write entry.read
end
FileUtils.chmod entry.header.mode, File.join(course_root, relative_pathname),
verbose: false
elsif entry.header.typeflag == "2"
File.symlink entry.header.linkname, File.join(course_root, relative_pathname)
end
end
tar_extract.close
rescue StandardError => e
flash[:error] = "Error while extracting tarball to server -- #{e.message}."
redirect_to(action: "installAssessment") && return
end

params[:assessment_name] = asmt_name
importAssessment && return
end

# importAssessment - Imports an existing assessment from local file.
# The main task of this function is to decide what category a newly
# installed assessment should be assigned to.
action_auth_level :importAssessment, :instructor
def importAssessment
@assessment = @course.assessments.new(name: params[:assessment_name])
@assessment.load_yaml # this will save the assessment
@assessment.construct_folder # make sure there's a handin folder, just in case
@assessment.load_config_file # only call this on saved assessments
redirect_to([@course, @assessment])
end

# create - Creates an assessment from an assessment directory
# residing in the course directory.
action_auth_level :create, :instructor
Expand Down Expand Up @@ -350,6 +278,28 @@ def export
end


# importAssessment - Imports an existing assessment from local file.
# The main task of this function is to decide what category a newly
# installed assessment should be assigned to.
action_auth_level :importAssessment, :instructor
def importAssessment
@assessment = @course.assessments.new(name: params[:assessment_name])
@assessment.load_yaml # this will save the assessment
@assessment.construct_folder # make sure there's a handin folder, just in case
@assessment.load_config_file # only call this on saved assessments
end

action_auth_level :multiImport, :instructor
def multiImport
if request.post?
importAsmtFromTar
if flash[:error].nil?
flash[:success] = "Assessments created"
redirect_to(course_path(@course)) and return
end
end
end



action_auth_level :destroy, :instructor
Expand Down Expand Up @@ -780,30 +730,7 @@ def edit_assessment_params
ass.permit!
end

##
# a valid assessment tar has a single root directory that's named after the
# assessment, containing an assessment yaml file and an assessment ruby file
#
def valid_asmt_tar(tar_extract)
asmt_name = nil
asmt_rb_exists = false
asmt_yml_exists = false
tar_extract.each do |entry|
pathname = entry.full_name
next if pathname.start_with? "."
pathname.chomp!("/") if entry.directory?
# nested directories are okay
if entry.directory? && pathname.count("/") == 0
return false if asmt_name
asmt_name = pathname
else
return false unless asmt_name
asmt_rb_exists = true if pathname == "#{asmt_name}/#{asmt_name}.rb"
asmt_yml_exists = true if pathname == "#{asmt_name}/#{asmt_name}.yml"
end
end
[asmt_rb_exists && asmt_yml_exists && (!asmt_name.nil?), asmt_name]
end


# methods for sending different file packages depending on what button was clicked
def exportEverything
Expand Down Expand Up @@ -904,4 +831,6 @@ def exportStudentFiles()
redirect_to(:action => 'exportOptions') && return
end
end

end

24 changes: 22 additions & 2 deletions app/models/assessment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -429,11 +429,15 @@ def config!
def serialize
s = {}
s["general"] = serialize_general
s["handin"] = serialize_time_data
s["problems"] = problems.map(&:serialize)
s["autograder"] = autograder.serialize if has_autograder?
s["autograder"]["makefile"] = "autograde-Makefile" if has_autograder?
s["autograder"]["tarfile"] = "autograde.tar" if has_autograder?
s["scoreboard"] = scoreboard.serialize if has_scoreboard?
s["late_penalty"] = late_penalty.serialize if late_penalty
s["version_penalty"] = version_penalty.serialize if version_penalty
s["version"] = Utilities.interface_number
s
end

Expand All @@ -443,14 +447,30 @@ def serialize_general
Utilities.serializable attributes, GENERAL_SERIALIZABLE
end

def serialize_time_data
s = {}
s["start_at"] = self.start_at.iso8601()
s["visible_at"] = self.visible_at.iso8601()
s["due_at"] = self.due_at.iso8601()
s["end_at"] = self.end_at.iso8601()
s["grading_deadline"] = self.grading_deadline.iso8601()
s
end

def deserialize(s)
self.due_at = self.end_at = self.visible_at = self.start_at = self.grading_deadline = Time.now
self.start_at = Time.iso8601(s["handin"]["visible_at"])
self.visible_at = Time.iso8601(s["handin"]["start_at"])
self.due_at = Time.iso8601(s["handin"]["due_at"])
self.end_at = Time.iso8601(s["handin"]["end_at"])
self.grading_deadline = Time.iso8601(s["handin"]["grading_deadline"])

self.quiz = false
self.quizData = ""
update!(s["general"])
Problem.deserialize_list(self, s["problems"]) if s["problems"]
if s["autograder"]
Autograder.find_or_initialize_by(assessment_id: id).update(s["autograder"])
ag = Autograder.find_or_initialize_by(assessment_id: id)
ag.update( s["autograder"].reject{|k,v| !ag.attributes.keys.member?(k.to_s) })
end
if s["scoreboard"]
Scoreboard.find_or_initialize_by(assessment_id: id).update(s["scoreboard"])
Expand Down
1 change: 1 addition & 0 deletions app/models/submission.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def save_additional_form_fields(params)
if assessment.is_section_dependant
form_hash["section"] = (assessment.lecture?) ? course_user_datum.lecture : course_user_datum.section
end
form_hash["ip"] = self.submitter_ip
self.settings = form_hash.to_json
self.save!
end
Expand Down
4 changes: 4 additions & 0 deletions app/views/assessments/installAssessment.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,9 @@
<h4><li>Create from scratch</h4>
<h5>You can create a new assessment from scratch using our
<%= link_to "Assessment Builder", new_course_assessment_path %>. </h5>

<h4><li>Import Multiple Assessments</h4>
<h5>You can Multiple Assessmets Using The
<%= link_to "Assessment Importer", multiImport_course_assessments_path %>. </h5>
</div>

Loading