diff --git a/Gemfile.lock b/Gemfile.lock index 166109fe..67860f20 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -443,6 +443,3 @@ DEPENDENCIES uglifier (>= 1.3.0) web-console (~> 2.0) will_paginate (~> 3.0) - -BUNDLED WITH - 1.13.6 diff --git a/app/assets/remote_scripts/macos.sh b/app/assets/remote_scripts/macos.sh new file mode 100644 index 00000000..4632d421 --- /dev/null +++ b/app/assets/remote_scripts/macos.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# run like this: +# cd path/to/project_root +# .scripts/macos.sh . + +# CodeOcean Remote Client v0.6 + +#file_info format: = (src/frog.java=34) +#file_path format: + + +project_root="${1%/}" +declare -a file_array + +function get_valid_file_path { + file_path="$project_root/$1" + if [ -e "$file_path" ]; then + valid_file_path="$file_path" + else + file_name="${1##*/}" + valid_file_path="$(find "$project_root" -name "$file_name" | head -1)" + if ! [ "$valid_file_path" ]; then + path_to_file="$(echo "$1" | pcregrep -o '^.+/')" + echo "Error: $file_name is not in $project_root/$path_to_file and could not be found under $project_root." + exit 1 + fi + fi + echo "$valid_file_path" +} + +function get_escaped_file_content { + file_path="$1" + cat "$file_path" | + perl -p -e 's@\\@\\\\@g' | + perl -p -e 's@\r\n@\\n@g' | + perl -p -e 's@\n@\\n@g' | + perl -p -e 's@"@\\"@g' +} + +function get_file_attributes { + file_info="$1" + file_path="$(get_valid_file_path "${file_info%=*}")" + escaped_file_content="$(get_escaped_file_content "$file_path")" + file_id="${file_info##*=}" + echo "{\"file_id\": $file_id,\"content\": \"$escaped_file_content\"}" +} + +function read_file_to_array { + let i=0 + while IFS=$'\n' read -r line_data; do + file_array[i]="${line_data}" + ((++i)) + done < $1 +} + + +co_file_path="$(get_valid_file_path '.co')" +read_file_to_array $co_file_path + +validation_token="${file_array[0]}" + +target_url="${file_array[1]}" + +files_attributes="$(get_file_attributes "${file_array[2]}")" + +for ((i = 3; i < ${#file_array[@]}; i++)); do + files_attributes+=", $(get_file_attributes "${file_array[i]}")" +done + +post_data="{\"remote_evaluation\": {\"validation_token\": \"$validation_token\",\"files_attributes\": [$files_attributes]}}" + +curl -H 'Content-Type: application/json' --data "$post_data" "$target_url" +echo diff --git a/app/assets/remote_scripts/ubuntu.sh b/app/assets/remote_scripts/ubuntu.sh new file mode 100644 index 00000000..db5a970d --- /dev/null +++ b/app/assets/remote_scripts/ubuntu.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# run like this: +# cd path/to/project_root +# .scripts/ubuntu.sh . + +# CodeOcean Remote Client v0.6 + +#file_info format: = (src/frog.java=34) +#file_path format: + + +project_root="${1%/}" + +function get_valid_file_path { + file_path="$project_root/$1" + if [ -e "$file_path" ]; then + valid_file_path="$file_path" + else + file_name="${1##*/}" + valid_file_path="$(find "$project_root" -name "$file_name" | head -1)" + if ! [ "$valid_file_path" ]; then + path_to_file="$(echo "$1" | grep -oP '^.+/')" + echo "Error: $file_name is not in $project_root/$path_to_file and could not be found under $project_root." + exit 1 + fi + fi + echo "$valid_file_path" +} + +function get_escaped_file_content { + file_path="$1" + cat "$file_path" | + perl -p -e 's@\\@\\\\@g' | + perl -p -e 's@\r\n@\\n@g' | + perl -p -e 's@\n@\\n@g' | + perl -p -e 's@"@\\"@g' +} + +function get_file_attributes { + file_info="$1" + file_path="$(get_valid_file_path "${file_info%=*}")" + escaped_file_content="$(get_escaped_file_content "$file_path")" + file_id="${file_info##*=}" + echo "{\"file_id\": $file_id,\"content\": \"$escaped_file_content\"}" +} + + +co_file_path="$(get_valid_file_path '.co')" +mapfile -t file_array < "$co_file_path" + +validation_token="${file_array[0]}" + +target_url="${file_array[1]}" + +files_attributes="$(get_file_attributes "${file_array[2]}")" + +for ((i = 3; i < ${#file_array[@]}; i++)); do + files_attributes+=", $(get_file_attributes "${file_array[i]}")" +done + +post_data="{\"remote_evaluation\": {\"validation_token\": \"$validation_token\",\"files_attributes\": [$files_attributes]}}" + +curl -H 'Content-Type: application/json' --data "$post_data" "$target_url" +echo diff --git a/app/assets/remote_scripts/windows.ps1 b/app/assets/remote_scripts/windows.ps1 new file mode 100644 index 00000000..9647d2e5 --- /dev/null +++ b/app/assets/remote_scripts/windows.ps1 @@ -0,0 +1,93 @@ +# run like this: +# cd path\to\project_root +# powershell.exe -noprofile -executionpolicy bypass -file .scripts\windows.ps1 . + +# CodeOcean Remote Client v0.6 + +#file_info format: = (src/frog.java=34) +#file_path format: + +param ( + [string]$project_root +) +if ( !($project_root | select-string -pattern '[/\\]$') ){ + $project_root += '/' +} + + +function post_web_request ($content_type, $data, $url){ + $buffer = [System.Text.Encoding]::UTF8.GetBytes($data) + [System.Net.HttpWebRequest] $web_request = [System.Net.WebRequest]::Create($url) + $web_request.Method = 'POST' + $web_request.ContentType = $content_type + $web_request.ContentLength = $buffer.Length; + + $request_stream = $web_request.GetRequestStream() + $request_stream.Write($buffer, 0, $buffer.Length) + $request_stream.Flush() + $request_stream.Close() + + [System.Net.HttpWebResponse] $web_response = $web_request.GetResponse() + $stream_reader = new-object System.IO.StreamReader($web_response.GetResponseStream()) + $result = $stream_reader.ReadToEnd() + return $result +} + +function find_file ($file_name){ + $search_result = get-childitem -recurse -path $project_root -filter $file_name + if( !$search_result.exists ){ + write-host "Error: $file_name could not be found under $project_root." + exit 1 + }elseif( $search_result.gettype().name -eq 'Object[]' ){ + $search_result = $search_result[0] + } + return $search_result +} + +function get_file ($file_path){ + $path_to_file = $project_root + $path_to_file += ($file_path | select-string -pattern '^.+/').matches.value + $file_name = ($file_path | select-string -pattern '[^/]+$').matches.value + $file = get-childitem -path $path_to_file -filter $file_name + if( !$file.exists ){ + write-host "Warning: $file_name should be in $path_to_file, but it is not. Searching whole project..." + $file = find_file $file_name + write-host 'Using '$file.fullname'.' + } + return $file +} + +function get_escaped_file_content ($file){ + $content = [IO.File]::ReadAllText($file.fullname) + $content = $content.replace('\', '\\') + $content = $content -replace "`r`n", '\n' + $content = $content -replace "`n", '\n' + $content = $content.replace('"', '\"') + return $content +} + +function get_file_attributes ($file_info){ + $file = get_file ($file_info | select-string -pattern '^.*(?==)').matches.value + $escaped_file_content = get_escaped_file_content $file + $file_id = ($file_info | select-string -pattern '[^=]+$').matches.value + return "{`"file_id`": $file_id,`"content`": `"$escaped_file_content`"}" +} + +$co_file = get_file '.co' + +$file_array = get-content $co_file.fullname + +$validation_token = $file_array[0] + +$target_url = $file_array[1] + +$files_attributes = get_file_attributes $file_array[2] + +for ($i = 3; $i -lt $file_array.length; $i++){ + $files_attributes += ', ' + $files_attributes += get_file_attributes $file_array[$i] +} + +$post_data = "{`"remote_evaluation`": {`"validation_token`": `"$validation_token`",`"files_attributes`": [$files_attributes]}}" + +post_web_request 'application/json' $post_data $target_url diff --git a/app/controllers/concerns/remote_evaluation_parameters.rb b/app/controllers/concerns/remote_evaluation_parameters.rb new file mode 100644 index 00000000..4f0acab2 --- /dev/null +++ b/app/controllers/concerns/remote_evaluation_parameters.rb @@ -0,0 +1,8 @@ +module RemoteEvaluationParameters + include FileParameters + + def remote_evaluation_params + remote_evaluation_params = params[:remote_evaluation].permit(:validation_token, files_attributes: file_attributes) + end + private :remote_evaluation_params +end \ No newline at end of file diff --git a/app/controllers/remote_evaluation_controller.rb b/app/controllers/remote_evaluation_controller.rb new file mode 100644 index 00000000..36d61f73 --- /dev/null +++ b/app/controllers/remote_evaluation_controller.rb @@ -0,0 +1,35 @@ +class RemoteEvaluationController < ApplicationController + include RemoteEvaluationParameters + include SubmissionScoring + + skip_after_action :verify_authorized + skip_before_action :verify_authenticity_token + + # POST /evaluate + # @param validation_token + # @param files_attributes + def evaluate + + validation_token = remote_evaluation_params[:validation_token] + files_attributes = remote_evaluation_params[:files_attributes] || [] + + # todo extra: validiere, ob files wirklich zur Übung gehören (wenn allowNewFiles-flag nicht gesetzt ist) + if (remote_evaluation_mapping = RemoteEvaluationMapping.find_by(:validation_token => validation_token)) + puts remote_evaluation_mapping.exercise_id + puts remote_evaluation_mapping.user_id + + _params = remote_evaluation_params.except(:validation_token) + _params[:exercise_id] = remote_evaluation_mapping.exercise_id + _params[:user_id] = remote_evaluation_mapping.user_id + _params[:cause] = "remoteAssess" + _params[:user_type] = "ExternalUser" + + @submission = Submission.create(_params) + render json: score_submission(@submission) + else + # todo: better output + # todo: check token expired? + render json: "No exercise found for this validation_token! Please keep out!" + end + end +end \ No newline at end of file diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index b9a1846e..9b37fde0 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -61,12 +61,29 @@ class SubmissionsController < ApplicationController # files = @submission.files.map{ } # zipline( files, 'submission.zip') # send_data(@file.content, filename: @file.name_with_extension) + + id_file = create_remote_evaluation_mapping + require 'zip' stringio = Zip::OutputStream.write_buffer do |zio| @files.each do |file| - zio.put_next_entry(file.name_with_extension) + zio.put_next_entry(file.path.to_s == '' ? file.name_with_extension : File.join(file.path, file.name_with_extension)) zio.write(file.content) end + + # zip .co file + zio.put_next_entry(".co") + zio.write(File.read id_file) + File.delete(id_file) if File.exist?(id_file) + + # zip client scripts + scripts_path = 'app/assets/remote_scripts' + Dir.foreach(scripts_path) do |file| + next if file == '.' or file == '..' + zio.put_next_entry(File.join('.scripts', File.basename(file))) + zio.write(File.read File.join(scripts_path, file)) + end + end send_data(stringio.string, filename: @submission.exercise.title.tr(" ", "_") + ".zip") end @@ -337,4 +354,26 @@ class SubmissionsController < ApplicationController server_sent_event.close end private :with_server_sent_events + + def create_remote_evaluation_mapping + user_id = @submission.user_id + exercise_id = @submission.exercise_id + + remote_evaluation_mapping = RemoteEvaluationMapping.create(:user_id => user_id, :exercise_id => exercise_id) + + # create .co file + path = "tmp/" + user_id.to_s + ".co" + # parse validation token + content = "#{remote_evaluation_mapping.validation_token}\n" + # parse remote request url + content += "#{request.base_url}/evaluate\n" + @submission.files.each do |file| + file_path = file.path.to_s == '' ? file.name_with_extension : File.join(file.path, file.name_with_extension) + content += "#{file_path}=#{file.id.to_s}\n" + end + File.open(path, "w+") do |f| + f.write(content) + end + path + end end diff --git a/app/models/remote_evaluation_mapping.rb b/app/models/remote_evaluation_mapping.rb new file mode 100644 index 00000000..be0034b1 --- /dev/null +++ b/app/models/remote_evaluation_mapping.rb @@ -0,0 +1,10 @@ +# todo: reference to lti_param_model +class RemoteEvaluationMapping < ActiveRecord::Base + before_create :generate_token, unless: :validation_token? + belongs_to :exercise + belongs_to :user + + def generate_token + self.validation_token = SecureRandom.urlsafe_base64 + end +end \ No newline at end of file diff --git a/app/models/submission.rb b/app/models/submission.rb index 1abbbb84..6815eb40 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -2,7 +2,7 @@ class Submission < ActiveRecord::Base include Context include Creation - CAUSES = %w(assess download file render run save submit test autosave requestComments) + CAUSES = %w(assess download file render run save submit test autosave requestComments remoteAssess) FILENAME_URL_PLACEHOLDER = '{filename}' belongs_to :exercise diff --git a/config/routes.rb b/config/routes.rb index 281af37c..fc0f9406 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -140,4 +140,6 @@ Rails.application.routes.draw do end end + post "/evaluate", to: 'remote_evaluation#evaluate', via: [:post] + end diff --git a/db/migrate/20170202170437_create_remote_evaluation_mappings.rb b/db/migrate/20170202170437_create_remote_evaluation_mappings.rb new file mode 100644 index 00000000..d102370d --- /dev/null +++ b/db/migrate/20170202170437_create_remote_evaluation_mappings.rb @@ -0,0 +1,11 @@ +class CreateRemoteEvaluationMappings < ActiveRecord::Migration + def change + create_table :remote_evaluation_mappings do |t| + t.integer "user_id", null: false + t.integer "exercise_id", null: false + t.string "validation_token", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 38d3f4a0..f0ecde99 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -238,6 +238,14 @@ ActiveRecord::Schema.define(version: 20170403162848) do t.datetime "updated_at" end + create_table "remote_evaluation_mappings", force: :cascade do |t| + t.integer "user_id", null: false + t.integer "exercise_id", null: false + t.string "validation_token", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "request_for_comments", force: :cascade do |t| t.integer "user_id", null: false t.integer "exercise_id", null: false diff --git a/db/structure.sql b/db/structure.sql index 4b290bdc..09afcb66 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -39,10 +39,9 @@ SET default_with_oids = false; CREATE TABLE code_harbor_links ( id integer NOT NULL, - oauth2token character varying(255), + oauth2token character varying, created_at timestamp without time zone, - updated_at timestamp without time zone, - user_id integer + updated_at timestamp without time zone ); @@ -73,7 +72,7 @@ CREATE TABLE comments ( id integer NOT NULL, user_id integer, file_id integer, - user_type character varying(255), + user_type character varying, "row" integer, "column" integer, text text, @@ -107,11 +106,11 @@ ALTER SEQUENCE comments_id_seq OWNED BY comments.id; CREATE TABLE consumers ( id integer NOT NULL, - name character varying(255), + name character varying, created_at timestamp without time zone, updated_at timestamp without time zone, - oauth_key character varying(255), - oauth_secret character varying(255) + oauth_key character varying, + oauth_secret character varying ); @@ -173,18 +172,18 @@ ALTER SEQUENCE errors_id_seq OWNED BY errors.id; CREATE TABLE execution_environments ( id integer NOT NULL, - docker_image character varying(255), - name character varying(255), + docker_image character varying, + name character varying, created_at timestamp without time zone, updated_at timestamp without time zone, - run_command character varying(255), - test_command character varying(255), - testing_framework character varying(255), + run_command character varying, + test_command character varying, + testing_framework character varying, help text, - exposed_ports character varying(255), + exposed_ports character varying, permitted_execution_time integer, user_id integer, - user_type character varying(255), + user_type character varying, pool_size integer, file_type_id integer, memory_limit integer, @@ -219,14 +218,14 @@ CREATE TABLE exercises ( id integer NOT NULL, description text, execution_environment_id integer, - title character varying(255), + title character varying, created_at timestamp without time zone, updated_at timestamp without time zone, user_id integer, instructions text, public boolean, - user_type character varying(255), - token character varying(255), + user_type character varying, + token character varying, hide_file_tree boolean, allow_file_creation boolean, allow_auto_completion boolean DEFAULT false @@ -259,9 +258,9 @@ ALTER SEQUENCE exercises_id_seq OWNED BY exercises.id; CREATE TABLE external_users ( id integer NOT NULL, consumer_id integer, - email character varying(255), - external_id character varying(255), - name character varying(255), + email character varying, + external_id character varying, + name character varying, created_at timestamp without time zone, updated_at timestamp without time zone ); @@ -292,7 +291,7 @@ ALTER SEQUENCE external_users_id_seq OWNED BY external_users.id; CREATE TABLE file_templates ( id integer NOT NULL, - name character varying(255), + name character varying, content text, file_type_id integer, created_at timestamp without time zone, @@ -325,16 +324,16 @@ ALTER SEQUENCE file_templates_id_seq OWNED BY file_templates.id; CREATE TABLE file_types ( id integer NOT NULL, - editor_mode character varying(255), - file_extension character varying(255), + editor_mode character varying, + file_extension character varying, indent_size integer, - name character varying(255), + name character varying, user_id integer, created_at timestamp without time zone, updated_at timestamp without time zone, executable boolean, renderable boolean, - user_type character varying(255), + user_type character varying, "binary" boolean ); @@ -366,20 +365,20 @@ CREATE TABLE files ( id integer NOT NULL, content text, context_id integer, - context_type character varying(255), + context_type character varying, file_id integer, file_type_id integer, hidden boolean, - name character varying(255), + name character varying, read_only boolean, created_at timestamp without time zone, updated_at timestamp without time zone, - native_file character varying(255), - role character varying(255), - hashed_content character varying(255), - feedback_message character varying(255), + native_file character varying, + role character varying, + hashed_content character varying, + feedback_message character varying, weight double precision, - path character varying(255), + path character varying, file_template_id integer ); @@ -410,10 +409,10 @@ ALTER SEQUENCE files_id_seq OWNED BY files.id; CREATE TABLE hints ( id integer NOT NULL, execution_environment_id integer, - locale character varying(255), + locale character varying, message text, - name character varying(255), - regular_expression character varying(255), + name character varying, + regular_expression character varying, created_at timestamp without time zone, updated_at timestamp without time zone ); @@ -445,23 +444,23 @@ ALTER SEQUENCE hints_id_seq OWNED BY hints.id; CREATE TABLE internal_users ( id integer NOT NULL, consumer_id integer, - email character varying(255), - name character varying(255), - role character varying(255), + email character varying, + name character varying, + role character varying, created_at timestamp without time zone, updated_at timestamp without time zone, - crypted_password character varying(255), - salt character varying(255), + crypted_password character varying, + salt character varying, failed_logins_count integer DEFAULT 0, lock_expires_at timestamp without time zone, - unlock_token character varying(255), - remember_me_token character varying(255), + unlock_token character varying, + remember_me_token character varying, remember_me_token_expires_at timestamp without time zone, - reset_password_token character varying(255), + reset_password_token character varying, reset_password_token_expires_at timestamp without time zone, reset_password_email_sent_at timestamp without time zone, - activation_state character varying(255), - activation_token character varying(255), + activation_state character varying, + activation_token character varying, activation_token_expires_at timestamp without time zone ); @@ -519,6 +518,39 @@ CREATE SEQUENCE lti_parameters_id_seq ALTER SEQUENCE lti_parameters_id_seq OWNED BY lti_parameters.id; +-- +-- Name: remote_evaluation_mappings; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE remote_evaluation_mappings ( + id integer NOT NULL, + user_id integer NOT NULL, + exercise_id integer NOT NULL, + validation_token character varying NOT NULL, + created_at timestamp without time zone, + updated_at timestamp without time zone +); + + +-- +-- Name: remote_evaluation_mappings_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE remote_evaluation_mappings_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: remote_evaluation_mappings_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE remote_evaluation_mappings_id_seq OWNED BY remote_evaluation_mappings.id; + + -- -- Name: request_for_comments; Type: TABLE; Schema: public; Owner: - -- @@ -530,7 +562,7 @@ CREATE TABLE request_for_comments ( file_id integer NOT NULL, created_at timestamp without time zone, updated_at timestamp without time zone, - user_type character varying(255), + user_type character varying, question text, solved boolean, submission_id integer @@ -561,7 +593,7 @@ ALTER SEQUENCE request_for_comments_id_seq OWNED BY request_for_comments.id; -- CREATE TABLE schema_migrations ( - version character varying(255) NOT NULL + version character varying NOT NULL ); @@ -576,8 +608,8 @@ CREATE TABLE submissions ( user_id integer, created_at timestamp without time zone, updated_at timestamp without time zone, - cause character varying(255), - user_type character varying(255) + cause character varying, + user_type character varying ); @@ -725,6 +757,13 @@ ALTER TABLE ONLY internal_users ALTER COLUMN id SET DEFAULT nextval('internal_us ALTER TABLE ONLY lti_parameters ALTER COLUMN id SET DEFAULT nextval('lti_parameters_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY remote_evaluation_mappings ALTER COLUMN id SET DEFAULT nextval('remote_evaluation_mappings_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -850,6 +889,14 @@ ALTER TABLE ONLY lti_parameters ADD CONSTRAINT lti_parameters_pkey PRIMARY KEY (id); +-- +-- Name: remote_evaluation_mappings_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY remote_evaluation_mappings + ADD CONSTRAINT remote_evaluation_mappings_pkey PRIMARY KEY (id); + + -- -- Name: request_for_comments_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -874,13 +921,6 @@ ALTER TABLE ONLY testruns ADD CONSTRAINT testruns_pkey PRIMARY KEY (id); --- --- Name: index_code_harbor_links_on_user_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX index_code_harbor_links_on_user_id ON code_harbor_links USING btree (user_id); - - -- -- Name: index_comments_on_file_id; Type: INDEX; Schema: public; Owner: - -- @@ -1120,3 +1160,5 @@ INSERT INTO schema_migrations (version) VALUES ('20160907123009'); INSERT INTO schema_migrations (version) VALUES ('20170112151637'); +INSERT INTO schema_migrations (version) VALUES ('20170202170437'); +