From 007aae7572fe8b02389cfe10a644576c8ebb5c0b Mon Sep 17 00:00:00 2001 From: David Schuster Date: Fri, 19 Jun 2020 15:35:22 -0400 Subject: [PATCH 01/10] added ip to settings file (#71) --- app/models/submission.rb | 1 + 1 file changed, 1 insertion(+) 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 From 376e711483351010db164be49f951cb90d7c9509 Mon Sep 17 00:00:00 2001 From: David Schuster Date: Sat, 9 Jan 2021 16:01:11 -0500 Subject: [PATCH 02/10] start file for import --- examples/multiple assessment import starter.tar | Bin 0 -> 10240 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/multiple assessment import starter.tar diff --git a/examples/multiple assessment import starter.tar b/examples/multiple assessment import starter.tar new file mode 100644 index 0000000000000000000000000000000000000000..b7f6a82d0b44731c2922f8fb03bf33882bcf8685 GIT binary patch literal 10240 zcmeHK-%r~x5YFrRS6I9tO-ixzt5cp<3B-^FXtZdLk(;=!xBS9(811(Iedjbl5M2ps znb2@v+MMq$ci-oW^HC|8lo`tvIj~;n!f*|I-@*YV1K+8CF(#I_H{6%`0shxD2!XcW)Ez{CTW|M`Zt_;3C1FS2xJZEKDL&-*L?1JAwV z-^1WX0@yGX!aJ*AL!vj%zir#S8OxcVY1l(3rx^=TYa@UyVKNrHRJ_PTw7;({T$U+a zL@V#%Xmm6hogSTy(btplCp5eqpC4Zg4^PgH!FNvcgy+$er)=fEGW3_cykAYYV6iI1 zB5eF&DWf;hOwgD`30=q#;l{nFR8z(e!4<2@=1%*|YLam&p;8F22}D4tbds`Y^;ED< zX)0NhUh*Y$>(q&5B(I^r+vS)lHiLqi9zW9w8peuWbH$nLm7;*8nGAb&1KB7pgiiYy zthz3}8X~u^n<#210q^Evv{D5$nlPamxVwuK&sg0o`Jlt@7kEZznx!U==d-9bfsD)thDR{0h`h{+&m`P+~WS^j)zfqOLI;0A*A0czh& z?=~dsg9Ys6Ld}cZMPnw_MU}%#re7`~E1a@04#<|s-Ml%s=*xt5LtyRvYyP^0ck+5_ z{$c(0{C9l<--pEa0>{jMCvdlPYV*H3BX=(YH2)pH|DA!>1t0@_^Zmaiv)#h)`FD0t zd{e?R`1d>zep>&*YryEgZWM0i)#ktT|DWo=fVA{qfQ|m!iqYo$H6@CwX$J!CBvNHAWj1D{9S zj=WnlfAFf#fAjv|5yK{pLBJqj5HJWB1PlTO0fT@+z#w1{FbEg~3<3rLgTPxx;13rF B8O#6x literal 0 HcmV?d00001 From 843a989a1e6dc3fb0ca513b28878bbf742253de1 Mon Sep 17 00:00:00 2001 From: David Schuster Date: Sat, 9 Jan 2021 16:29:59 -0500 Subject: [PATCH 03/10] import multiple assessments from tar --- app/controllers/assessments_controller.rb | 85 +++++++++++++------ .../assessments/installAssessment.html.erb | 4 + app/views/assessments/multiImport.html.erb | 43 ++++++++++ config/routes.rb | 2 + 4 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 app/views/assessments/multiImport.html.erb diff --git a/app/controllers/assessments_controller.rb b/app/controllers/assessments_controller.rb index b360d5c98..491cbbc53 100755 --- a/app/controllers/assessments_controller.rb +++ b/app/controllers/assessments_controller.rb @@ -27,7 +27,7 @@ class AssessmentsController < ApplicationController # 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. @@ -113,31 +113,45 @@ def importAsmtFromTar 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 + assessmentNames = [] + tar_extract.each do |entry| + if entry.directory? && entry.full_name.chomp!("/").count("/") == 0 + assessmentNames.push(entry.full_name) + end end + assessmentNames.each do |asmt_name| + unless @course.assessments.find_by(name: asmt_name).nil? + flash[:error] = "An assessment with the same name #{asmt_name} already exists for the course. Please use a different name." + redirect_to(action: "installAssessment") && return + end + + tar_extract.rewind + is_valid_tar, current = valid_asmt_tar(tar_extract,asmt_name) + + unless is_valid_tar + flash[:error] = "Invalid assessment. Please verify the existence of configuration files. in the #{current} folder" + 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 + # abort relative_pathname.inspect + next unless entry.full_name.split("/")[0] == asmt_name if entry.directory? FileUtils.mkdir_p(File.join(course_root, relative_pathname), mode: entry.header.mode, verbose: false) @@ -154,13 +168,29 @@ def importAsmtFromTar end end tar_extract.close + + 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 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 + importAssessment + end + redirect_to(@course) end # importAssessment - Imports an existing assessment from local file. @@ -172,7 +202,13 @@ def importAssessment @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 + + action_auth_level :multiImport, :instructor + def multiImport + if request.post? + importAsmtFromTar + end end # create - Creates an assessment from an assessment directory @@ -352,6 +388,7 @@ def export + action_auth_level :destroy, :instructor def destroy for submission in @assessment.submissions do @@ -784,25 +821,20 @@ def edit_assessment_params # 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 + def valid_asmt_tar(tar_extract, asmt_name) asmt_yml_exists = false tar_extract.each do |entry| pathname = entry.full_name next if pathname.start_with? "." pathname.chomp!("/") if entry.directory? + next if !entry.full_name.split("/")[0] == asmt_name + next if pathname == asmt_name + # 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 + return false unless asmt_name + asmt_yml_exists = true if pathname == "#{asmt_name}/#{asmt_name}.yml" end - [asmt_rb_exists && asmt_yml_exists && (!asmt_name.nil?), asmt_name] + [asmt_yml_exists && (!asmt_name.nil?), asmt_name] end # methods for sending different file packages depending on what button was clicked @@ -905,3 +937,4 @@ def exportStudentFiles() end end 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 From 40aa7e551e81ce818de7b24f58be86c7cbced448 Mon Sep 17 00:00:00 2001 From: David Schuster Date: Sat, 9 Jan 2021 17:20:40 -0500 Subject: [PATCH 04/10] Update README --- examples/README | 96 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/examples/README b/examples/README index 6239019ae..e7ebd0725 100644 --- a/examples/README +++ b/examples/README @@ -1,4 +1,94 @@ -This directory contains examples of autograded labs. +# Examples -Files: -hello/ The simplest autograded lab +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 +│ ├── handin +│ ├── assessment1.yml +│ ├── autograde.tar +│ └── autograde-Makefile +└── assessment2 + ├── handin + └── assessment1.yml + +``` + +the top level must consist of folders named the same as what you will be naming the course (url name not display name) + +assessment1 is an example of an assessment with an autograding component + +the handin folder while has to remain present and named the same is not currently used for anything but will be in the future for adding submissions to new assessments. + +assessment1.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 +problems: +- name: Score + description: '' + max_score: 3.0 + optional: false +autograder: + autograde_timeout: 180 + autograde_image: autograding_image + release_score: true +``` +the only hiccup on this file is that the name field must be the same as the file and top level folder name + +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 with naming is standardized to avoid complications. + +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 +problems: +- name: quiz02 + description: '' + max_score: 100.0 + optional: false +``` From 48b29a846682584aa74726b636e95e4e4b85b6f8 Mon Sep 17 00:00:00 2001 From: David Schuster Date: Sat, 9 Jan 2021 17:23:16 -0500 Subject: [PATCH 05/10] Rename README to README.md --- examples/{README => README.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{README => README.md} (100%) diff --git a/examples/README b/examples/README.md similarity index 100% rename from examples/README rename to examples/README.md From 020a8f2869dbec2329edead69730e43917a850ee Mon Sep 17 00:00:00 2001 From: David Schuster Date: Sun, 17 Jan 2021 21:37:06 -0500 Subject: [PATCH 06/10] updated how imports work no sections yet but much more robust --- app/controllers/assessment/import.rb | 152 ++++++++++++++++++++++ app/controllers/assessments_controller.rb | 152 ++++------------------ app/models/assessment.rb | 24 +++- lib/utilities.rb | 5 + 4 files changed, 202 insertions(+), 131 deletions(-) create mode 100644 app/controllers/assessment/import.rb diff --git a/app/controllers/assessment/import.rb b/app/controllers/assessment/import.rb new file mode 100644 index 000000000..bd1da03e5 --- /dev/null +++ b/app/controllers/assessment/import.rb @@ -0,0 +1,152 @@ +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")) + 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 + diff --git a/app/controllers/assessments_controller.rb b/app/controllers/assessments_controller.rb index 491cbbc53..21d0d2b27 100755 --- a/app/controllers/assessments_controller.rb +++ b/app/controllers/assessments_controller.rb @@ -24,6 +24,9 @@ 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, @@ -100,117 +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 - rescue StandardError => e - flash[:error] = "Error while reading the tarball -- #{e.message}." - redirect_to(action: "installAssessment") - return - end - - assessmentNames = [] - tar_extract.each do |entry| - if entry.directory? && entry.full_name.chomp!("/").count("/") == 0 - assessmentNames.push(entry.full_name) - end - end - - assessmentNames.each do |asmt_name| - unless @course.assessments.find_by(name: asmt_name).nil? - flash[:error] = "An assessment with the same name #{asmt_name} already exists for the course. Please use a different name." - redirect_to(action: "installAssessment") && return - end - - tar_extract.rewind - is_valid_tar, current = valid_asmt_tar(tar_extract,asmt_name) - - unless is_valid_tar - flash[:error] = "Invalid assessment. Please verify the existence of configuration files. in the #{current} folder" - 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 - # abort relative_pathname.inspect - next unless entry.full_name.split("/")[0] == asmt_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 - - 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 - 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 - end - redirect_to(@course) - 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 - end - end - # create - Creates an assessment from an assessment directory # residing in the course directory. action_auth_level :create, :instructor @@ -386,6 +278,25 @@ 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 + flash[:success] = "Assessments created" + redirect_to(course_path(@course)) && return + end + end @@ -817,25 +728,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) - asmt_yml_exists = false - tar_extract.each do |entry| - pathname = entry.full_name - next if pathname.start_with? "." - pathname.chomp!("/") if entry.directory? - next if !entry.full_name.split("/")[0] == asmt_name - next if pathname == asmt_name - # nested directories are okay - return false unless asmt_name - asmt_yml_exists = true if pathname == "#{asmt_name}/#{asmt_name}.yml" - end - [asmt_yml_exists && (!asmt_name.nil?), asmt_name] - end # methods for sending different file packages depending on what button was clicked def exportEverything @@ -936,5 +829,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..4fb1e29db 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["book_keeping"] = {"interface_number" => 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.strftime('%s') + s["visible_at"] = self.visible_at.strftime('%s') + s["due_at"] = self.due_at.strftime('%s') + s["end_at"] = self.end_at.strftime('%s') + s["grading_deadline"] = self.grading_deadline.strftime('%s') + 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.at(s["handin"]["visible_at"].to_i) + self.visible_at = Time.at(s["handin"]["start_at"].to_i) + self.due_at = Time.at(s["handin"]["due_at"].to_i) + self.end_at = Time.at(s["handin"]["end_at"].to_i) + self.grading_deadline = Time.at(s["handin"]["grading_deadline"].to_i) + 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/lib/utilities.rb b/lib/utilities.rb index bb9b9ad6a..f468237f6 100755 --- a/lib/utilities.rb +++ b/lib/utilities.rb @@ -1,4 +1,9 @@ module Utilities + + def self.interface_number() + return 1000 + end + def self.serializable(attributes, serializable) attributes.keep_if { |f, _| serializable.include? f } attributes.delete_if { |_, v| v.nil? } From 4a60bf865cf5aa451458d5b3b8b90f2afe7beda6 Mon Sep 17 00:00:00 2001 From: David Schuster Date: Sun, 17 Jan 2021 21:51:13 -0500 Subject: [PATCH 07/10] updated examples --- .../multiple assessment import starter.tar | Bin 10240 -> 10240 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/examples/multiple assessment import starter.tar b/examples/multiple assessment import starter.tar index b7f6a82d0b44731c2922f8fb03bf33882bcf8685..559fdf6ad7567faf7d5a67e27c4a51746a7ea120 100644 GIT binary patch delta 387 zcmZ9IO-{ow5Jr>^s_lKn}O3+3@HGay{ zx6|83_O31oCCnk;dO)dtFA8{vWW9?gGzfjZF(8*e(_I{HA6jT(yB4toAybuuZ8{{Zo1mT8u>MDkP$m+X ztJG3ao1$~z=5CwWO8d)ybZjGURuoB`0F<_%+eYOE^jzFSoY&b5bc6`T*lS+~)O{cR YIx4eQp%(3qda~Yc@x|v^yBc8k3lM#5W&i*H delta 232 zcmZn&Xb70lDPnAHZf0U=Vq|8{U|?u$U}VUkU^-cmF=aC&BM&2AadB#Kac*i}iJ@L) zZq7tO&&erF3X Date: Sun, 17 Jan 2021 21:51:44 -0500 Subject: [PATCH 08/10] Update README.md --- examples/README.md | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/examples/README.md b/examples/README.md index e7ebd0725..6e1973322 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,23 +17,19 @@ multiple assessment import starter.tar is the starter file for importing one or ```bash multiple assessment import starter.tar ├── assessment1 -│ ├── handin │ ├── assessment1.yml │ ├── autograde.tar │ └── autograde-Makefile └── assessment2 - ├── handin └── assessment1.yml ``` -the top level must consist of folders named the same as what you will be naming the course (url name not display name) +every folder in the tar represents a separate assessment assessment1 is an example of an assessment with an autograding component -the handin folder while has to remain present and named the same is not currently used for anything but will be in the future for adding submissions to new assessments. - -assessment1.yml +properties.yml ```YAML --- general: @@ -50,6 +46,12 @@ general: max_size: 2 has_svn: false category_name: Lab Activities +handin: + start_at: '1523557920' + visible_at: '1523557967' + due_at: '1543692720' + end_at: '1543699920' + grading_deadline: '1543693720' problems: - name: Score description: '' @@ -59,14 +61,25 @@ autograder: autograde_timeout: 180 autograde_image: autograding_image release_score: true + makefile: autograde-Makefile + tarfile: autograde.tar +book_keeping: + interface_number: 1000 + ``` -the only hiccup on this file is that the name field must be the same as the file and top level folder name +this file must be named properties.yml + +name: must be url safe + +handin: times are seconds after epoch in UTC + +book_keeping: the interface number will be used to provided migration steps if the system changes too much to support old versions 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 with naming is standardized to avoid complications. +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 @@ -86,6 +99,12 @@ general: max_size: 5 has_svn: false category_name: RecitationQuiz +handin: + start_at: '1523557920' + visible_at: '1523557967' + due_at: '1543692720' + end_at: '1543699920' + grading_deadline: '1543693720' problems: - name: quiz02 description: '' From e7cd1637ef9286b5d32fc9f0d2ee1362fcbd0c85 Mon Sep 17 00:00:00 2001 From: David Schuster Date: Sun, 17 Jan 2021 22:03:43 -0500 Subject: [PATCH 09/10] Update README.md --- examples/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/README.md b/examples/README.md index 6e1973322..94ac5c96b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,11 +17,11 @@ multiple assessment import starter.tar is the starter file for importing one or ```bash multiple assessment import starter.tar ├── assessment1 -│ ├── assessment1.yml +│ ├── properties.yml │ ├── autograde.tar │ └── autograde-Makefile └── assessment2 - └── assessment1.yml + └── properties.yml ``` From 20a1bb5d6f0f895edfb070d28515d273f649254e Mon Sep 17 00:00:00 2001 From: David Schuster Date: Mon, 18 Jan 2021 14:41:51 -0500 Subject: [PATCH 10/10] changed version numbers to be more useable, examples and readme updated, date format changed to iso 8601 --- app/controllers/assessment/import.rb | 6 ++-- app/controllers/assessments_controller.rb | 6 ++-- app/models/assessment.rb | 22 ++++++------ examples/README.md | 32 ++++++++++-------- .../multiple assessment import starter.tar | Bin 10240 -> 10240 bytes lib/utilities.rb | 2 +- 6 files changed, 38 insertions(+), 30 deletions(-) diff --git a/app/controllers/assessment/import.rb b/app/controllers/assessment/import.rb index bd1da03e5..62539cc30 100644 --- a/app/controllers/assessment/import.rb +++ b/app/controllers/assessment/import.rb @@ -145,8 +145,10 @@ def moveFiles(props, directory) FileUtils.mv(directory,newAssessmentPath) FileUtils.mv(Rails.root.join(newAssessmentPath, "properties.yml"), Rails.root.join(newAssessmentPath, props["general"]["name"] + ".yml")) - 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" + 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 21d0d2b27..cb3ffb55b 100755 --- a/app/controllers/assessments_controller.rb +++ b/app/controllers/assessments_controller.rb @@ -293,8 +293,10 @@ def importAssessment def multiImport if request.post? importAsmtFromTar - flash[:success] = "Assessments created" - redirect_to(course_path(@course)) && return + if flash[:error].nil? + flash[:success] = "Assessments created" + redirect_to(course_path(@course)) and return + end end end diff --git a/app/models/assessment.rb b/app/models/assessment.rb index 4fb1e29db..4341f8d52 100755 --- a/app/models/assessment.rb +++ b/app/models/assessment.rb @@ -437,7 +437,7 @@ def serialize 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["book_keeping"] = {"interface_number" => Utilities.interface_number} + s["version"] = Utilities.interface_number s end @@ -449,20 +449,20 @@ def serialize_general def serialize_time_data s = {} - s["start_at"] = self.start_at.strftime('%s') - s["visible_at"] = self.visible_at.strftime('%s') - s["due_at"] = self.due_at.strftime('%s') - s["end_at"] = self.end_at.strftime('%s') - s["grading_deadline"] = self.grading_deadline.strftime('%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.start_at = Time.at(s["handin"]["visible_at"].to_i) - self.visible_at = Time.at(s["handin"]["start_at"].to_i) - self.due_at = Time.at(s["handin"]["due_at"].to_i) - self.end_at = Time.at(s["handin"]["end_at"].to_i) - self.grading_deadline = Time.at(s["handin"]["grading_deadline"].to_i) + 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 = "" diff --git a/examples/README.md b/examples/README.md index 94ac5c96b..11ba30c61 100644 --- a/examples/README.md +++ b/examples/README.md @@ -47,11 +47,11 @@ general: has_svn: false category_name: Lab Activities handin: - start_at: '1523557920' - visible_at: '1523557967' - due_at: '1543692720' - end_at: '1543699920' - grading_deadline: '1543693720' + 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: '' @@ -63,19 +63,22 @@ autograder: release_score: true makefile: autograde-Makefile tarfile: autograde.tar -book_keeping: - interface_number: 1000 +version: 1 + ``` this file must be named properties.yml name: must be url safe -handin: times are seconds after epoch in UTC +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 -book_keeping: the interface 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 ~~~ @@ -100,14 +103,15 @@ general: has_svn: false category_name: RecitationQuiz handin: - start_at: '1523557920' - visible_at: '1523557967' - due_at: '1543692720' - end_at: '1543699920' - grading_deadline: '1543693720' + 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 index 559fdf6ad7567faf7d5a67e27c4a51746a7ea120..ec3c59710941190ac1cd3baa6cc7307178759356 100644 GIT binary patch delta 518 zcmZn&Xb9NQ${}HH%wT9@U|?u$W?*b=$Y5Y-YHn!6pkOdrkuha6Bc~Try^(>Tg|2~# zuAxzgp^25Tk(Ge~kP9T$xfB%2GK({la#G_HORN;s@v2NIMOI~Kq-$V^WS%M1ywtoD zB(p%O_<_pIVAcYaO`gjwApy1tZia~k*i9hGecUg&%2JDpGxPJT6bv^vvam8v;_DRv z`p(GI2H08Z03rg)3wX)F)dB2O@{fMB%Bz06&#^2mk;8 delta 287 zcmX|+Jx;?w5QW!P3LFr@N{Y}el{+-EQY2Z=5el!%|b2$JtZ zi8uzfqNIG^n|XR~n{1Qs*P}T~h9Ll=A`-v=r3T43*bArV9Piq_K`BT|9SI1=oIjOK znJ-P6wJ{I&mePY&Yz{ai6$!ojFmP)-