diff --git a/app/controllers/assessment/import.rb b/app/controllers/assessment/import.rb new file mode 100644 index 000000000..62539cc30 --- /dev/null +++ b/app/controllers/assessment/import.rb @@ -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 + diff --git a/app/controllers/assessments_controller.rb b/app/controllers/assessments_controller.rb index b360d5c98..cb3ffb55b 100755 --- a/app/controllers/assessments_controller.rb +++ b/app/controllers/assessments_controller.rb @@ -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. @@ -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 @@ -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 @@ -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 @@ -904,4 +831,6 @@ def exportStudentFiles() redirect_to(:action => 'exportOptions') && return end end + end + diff --git a/app/models/assessment.rb b/app/models/assessment.rb index 0b45b3af3..4341f8d52 100755 --- a/app/models/assessment.rb +++ b/app/models/assessment.rb @@ -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 @@ -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"]) diff --git a/app/models/submission.rb b/app/models/submission.rb index d9756e36a..8136d2787 100755 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -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 diff --git a/app/views/assessments/installAssessment.html.erb b/app/views/assessments/installAssessment.html.erb index efaeb9906..a9e751caf 100755 --- a/app/views/assessments/installAssessment.html.erb +++ b/app/views/assessments/installAssessment.html.erb @@ -18,5 +18,9 @@
a starter tarball for this method of importing can be found here
+please do not rename the handin directory in the YAML file
+the top level folder names must match the name of the assessment in the YAML file as well as the name of the YAML file itself
+if the assessment does not have an autograded componet do not include autograde-Makefile or autograde.tar
+if the assessment does have an autograded componet include autograde-Makefile and autograde.tar
+please do not add any other files or directories than are what included in the starter
+