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 @@

  • Create from scratch
  • You can create a new assessment from scratch using our <%= link_to "Assessment Builder", new_course_assessment_path %>.
    + +

  • Import Multiple Assessments
  • +
    You can Multiple Assessmets Using The + <%= link_to "Assessment Importer", multiImport_course_assessments_path %>.
    diff --git a/app/views/assessments/multiImport.html.erb b/app/views/assessments/multiImport.html.erb new file mode 100644 index 000000000..13341893e --- /dev/null +++ b/app/views/assessments/multiImport.html.erb @@ -0,0 +1,43 @@ +<% @title = "Mutliple Assessment Importer" %> + +<% content_for :javascripts do %> + +<% end %> + + + +
    +

  • Import from tarball
  • +
    You can import a tarball with the assessment directory.
    + <%= form_tag({action: :multiImport}, multipart: true, id: "import-tarball-form") do %> +
    +
    + Browse + <%= file_field_tag "tarFile" %> +
    +
    + <%= text_field_tag "tarFile-name", "Upload Assessment Tarball", class: "file-path validate" %> +
    +
    + <% end %> +
    + +
    +

    Guide for making the tar ball

    +

    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

    +
    diff --git a/config/routes.rb b/config/routes.rb index c03a3c28d..96a4a8f58 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -126,6 +126,8 @@ get "installAssessment" post "importAssessment" post "importAsmtFromTar" + post "multiImport" + get "multiImport" end end diff --git a/examples/README b/examples/README deleted file mode 100644 index 6239019ae..000000000 --- a/examples/README +++ /dev/null @@ -1,4 +0,0 @@ -This directory contains examples of autograded labs. - -Files: -hello/ The simplest autograded lab diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..11ba30c61 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,117 @@ +# Examples + +this directory contains examples for a few features of the autograder + +## Assessments + +The Hello folder and Hello.tar contain various examples of different styles of Assessments + + +## Importing Assessments + +whether you are importing one or more assessments the procedure is the same + +multiple assessment import starter.tar is the starter file for importing one or more assessments + + +```bash +multiple assessment import starter.tar +├── assessment1 +│ ├── properties.yml +│ ├── autograde.tar +│ └── autograde-Makefile +└── assessment2 + └── properties.yml + +``` + +every folder in the tar represents a separate assessment + +assessment1 is an example of an assessment with an autograding component + +properties.yml +```YAML +--- +general: + name: assessment1 + description: '' + display_name: ASSESSMENT WITH AUTOGRADING + handin_filename: handin.zip + handin_directory: handin + max_grace_days: 0 + handout: '' + writeup: '' + max_submissions: -1 + disable_handins: false + max_size: 2 + has_svn: false + category_name: Lab Activities +handin: + start_at: '2018-04-12T14:32:00-04:00' + visible_at: '2018-04-12T14:32:00-04:00' + due_at: '2018-12-01T14:32:00-05:00' + end_at: '2018-12-01T16:32:00-05:00' + grading_deadline: '2018-12-01T14:48:00-05:00' +problems: +- name: Score + description: '' + max_score: 3.0 + optional: false +autograder: + autograde_timeout: 180 + autograde_image: autograding_image + release_score: true + makefile: autograde-Makefile + tarfile: autograde.tar +version: 1 + + +``` +this file must be named properties.yml + +name: must be url safe + +handin: times are in ISO 8601 format + +version: the version number will be used to provided migration steps if the system changes too much to support old versions + +category_name: will be created if not already present + +problems even if there is only 1 must be denoted as a list + +~~~ +autograde-Makefile, autograde.tar +~~~ +will contain your assessment files to ship to tango and be graded, names of these must be provided in the YAML file + +assessment2 is an assessment without autograding same rules apply as assessment1 the only difference being the assessment.yml file does not contain the autograder section + +```YAML +--- +general: + name: assessment2 + description: '' + display_name: ASSESSMENT WITH OUT AUOTGRADING + handin_filename: objects.zip + handin_directory: handin + max_grace_days: 0 + handout: '' + writeup: '' + max_submissions: -1 + disable_handins: false + max_size: 5 + has_svn: false + category_name: RecitationQuiz +handin: + start_at: '2018-04-12T14:32:00-04:00' + visible_at: '2018-04-12T14:32:00-04:00' + due_at: '2018-12-01T14:32:00-05:00' + end_at: '2018-12-01T16:32:00-05:00' + grading_deadline: '2018-12-01T14:48:00-05:00' +problems: +- name: quiz02 + description: '' + max_score: 100.0 + optional: false +version: 1 +``` diff --git a/examples/multiple assessment import starter.tar b/examples/multiple assessment import starter.tar new file mode 100644 index 000000000..ec3c59710 Binary files /dev/null and b/examples/multiple assessment import starter.tar differ diff --git a/lib/utilities.rb b/lib/utilities.rb index bb9b9ad6a..344b60634 100755 --- a/lib/utilities.rb +++ b/lib/utilities.rb @@ -1,4 +1,9 @@ module Utilities + + def self.interface_number() + return 1 + end + def self.serializable(attributes, serializable) attributes.keep_if { |f, _| serializable.include? f } attributes.delete_if { |_, v| v.nil? }