diff --git a/app/assets/javascripts/dag.js b/app/assets/javascripts/dag.js new file mode 100644 index 00000000..3aaae79e --- /dev/null +++ b/app/assets/javascripts/dag.js @@ -0,0 +1,106 @@ +// 根据 DAG 描述构建图 +function buildGraph(dag, nodeValueToLabel) { + const graph = {}; + const lines = dag.trim().split('\n'); + + for (const line of lines) { + const [node, ...parents] = line.split(':').map(item => item.trim()); + const label = nodeValueToLabel[node]; + if (parents.length === 0 || parents[0] === '') { + graph[label] = []; + } else { + const validParents = parents.map(parent => nodeValueToLabel[parent]).filter(parent => parent !== ''); + graph[label] = validParents; + } + } + + return graph; + } + + // 构建入度数组并初始化队列 + function initializeCounts(graph) { + const inDegree = {}; + const queue = []; + + for (const node in graph) { + inDegree[node] = graph[node].length; + if (inDegree[node] === 0) { + queue.push(node); + } + } + + return { inDegree, queue }; + } + + + function processSolution(graph, inDegree, queue, solution, nodeValueToLabel) { + const visited = new Set(); + if (Array.isArray(solution)) { + solution = solution.join('\n'); + } else if (typeof solution !== 'string') { + throw new TypeError('The solution must be a string or an array.'); + } + + const solutionNodes = solution.split('\n').map(line => line.trim()); + const graphNodes = Object.keys(graph).filter(node => node !== '__root__'); // 排除虚拟根节点 + + console.log("Solution nodes:", solutionNodes); + console.log("Graph nodes:", graphNodes); + + // 检查学生的解答中的项目数量是否与图中的节点数量匹配 + if (solutionNodes.length !== graphNodes.length) { + throw new Error('Number of items in student solution does not match the number of nodes in the graph.'); + } + + for (const node of solutionNodes) { // 修改这里 + console.log("Current node:", node); + console.log("Current queue:", queue); + + // 查找节点对应的标签 + const label = node; // 修改这里 + if (!label) { + console.log("Node label not found, returning false"); + return false; + } + + // 如果当前节点的标签不在队列中,返回false + if (!queue.includes(label)) { + console.log("Node label not in queue, returning false"); + return false; + } + + // 将当前节点的标签从队列中移除 + queue.splice(queue.indexOf(label), 1); + visited.add(label); + + // 更新相邻节点的入度,并将入度变为0的节点加入队列 + for (const neighbor in graph) { + if (graph[neighbor].includes(label)) { + inDegree[neighbor]--; + if (inDegree[neighbor] === 0) { + queue.push(neighbor); + } + } + } + console.log("Updated in-degree:", inDegree); + console.log("Updated queue:", queue); + } + + // 如果所有节点都被访问过,返回true,否则返回false + const allVisited = visited.size === Object.keys(graph).length; + console.log("All nodes visited:", allVisited); + return allVisited; + } + + function processDAG(dag, solution) { + const nodeValueToLabel = { + "one": "print('Hello')", + "two": "print('Parsons')", + "three": "print('Problems!')" + }; + + const graph = buildGraph(dag, nodeValueToLabel); + const { inDegree, queue } = initializeCounts(graph); + const result = processSolution(graph, inDegree, queue, solution, nodeValueToLabel); + return result; + } \ No newline at end of file diff --git a/app/assets/javascripts/peml_code.js b/app/assets/javascripts/peml_code.js new file mode 100644 index 00000000..93dd297c --- /dev/null +++ b/app/assets/javascripts/peml_code.js @@ -0,0 +1,60 @@ +const url = "https://skynet.cs.vt.edu/peml-live/api/parse"; + +// 获取 PEML 数据 +fetch('/data/s7.peml') + .then(response => { + return response.text(); + }) + .then(pemlText => { + const payload = { + "peml": pemlText, + "output_format": "json", + "render_to_html": "true" + }; + + // 发送 POST 请求进行解析 + $.post(url, payload, function(data, status) { + console.log('Post status:', status); + console.log('Post data:', data); + + // 检查服务器响应数据是否包含所需字段 + if (data && data.title && data.instructions && data.assets && data.assets.code && data.assets.code.starter && data.assets.code.starter.files && data.assets.code.starter.files[0] && data.assets.code.starter.files[0].content) { + // 获取你需要的字段 + var title = data.title.split(" -- ")[0]; + var instructions = data.instructions; + var initialArray = data.assets.code.starter.files[0].content.map(item => item.code.split('\\n')); + + // 在这里使用你获取的字段 + document.getElementById("title").innerHTML = title; + document.getElementById("instructions").innerHTML = instructions; + + var parson = new ParsonsWidget(); + parson.init(initialArray); + parson.shuffleLines(); + + $("#newInstanceLink").click(function(event) { + event.preventDefault(); + parson.shuffleLines(); + }); + + $("#feedbackLink").click(function(event) { + event.preventDefault(); + var fb = parson.getFeedback(); + $("#feedback").html(fb.feedback); + if (fb.success) { + score = 50; + } else { + score = 0; + } + updateScore(score); + }); + + function updateScore(score) { + // 更新分数的代码 + } + } else { + console.error('服务器响应数据不完整或格式不正确'); + // 在这里处理服务器响应数据不完整或格式不正确的情况 + } + }); + }); \ No newline at end of file diff --git a/app/assets/javascripts/simple_code.js b/app/assets/javascripts/simple_code.js index e3cca305..555aeb54 100644 --- a/app/assets/javascripts/simple_code.js +++ b/app/assets/javascripts/simple_code.js @@ -39,7 +39,6 @@ $(document).ready(function(){ var fb = parson.getFeedback(); if (data[index]['parsonsConfig']['turtleModelCode']){ if (fb.success) { - //把$("#feedback").html(fb.feedback)改成“Great job, you solved the exercise!” $("#feedback").html("Great job, you solved the exercise!"); } else { $("#feedback").html("Sorry, your solution does not match the model image"); diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index f92cc231..2902e7d8 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -4,8 +4,9 @@ class ExercisesController < ApplicationController require 'zip' require 'tempfile' require 'json' + require 'parsons_prompt_representer' - + skip_before_action :verify_authenticity_token, only: [:execute_peml] load_and_authorize_resource skip_authorize_resource only: [:practice, :call_open_pop] @@ -89,6 +90,60 @@ def update_score end # ------------------------------------------------------------- + def execute_peml + payload = JSON.parse(request.body.read) + user_solution = payload['user_solution'].join("\n") + expected_output = payload['expected_output'] + language = payload['language'] + external_id = payload['external_id'] + + # find the specific exercise instance + exercise = Exercise.find_by_external_id(external_id) + + # make sure the exercise exists + unless exercise + render json: { status: 'error', message: 'Can not find the specific exercise' }, status: :not_found + return + end + + # find the specific exercise version + exercise_id = Exercise.get_exercise_id(external_id) + exercise_version = ExerciseVersion.get_exercise_version(exercise_id) + + # create a new attempt + @attempt = Attempt.new( + user: current_user, + exercise_version: exercise_version, + submit_time: Time.current, + submit_num: 1 + ) + + if @attempt.save + # create a temporary directory for the attempt + attempt_dir = "usr/attempts/active/#{@attempt.id}" + FileUtils.mkdir_p(attempt_dir) + + # write the user's code to a file + File.write(attempt_dir + '/solution.py', user_solution) + + + # execute the code using CodeWorker + CodeWorker.new.perform_peml(@attempt.id, expected_output) + + # reload the attempt to get the updated status + @attempt.reload + + # render the response based on the error status + if @attempt.errors.empty? + render json: { message: "Your answer is correct!", success: true } + else + render json: { message: "Your answer is incorrect.", errors: @attempt.errors.full_messages, success: false } + end + else + render json: { error: "Unable to create attempt: #{@attempt.errors.full_messages.join(', ')}", success: false } + end + end + # ------------------------------------------------------------- # GET /exercises/download.csv def download @exercises = Exercise.accessible_by(current_ability) @@ -303,12 +358,25 @@ def create multiplechoiceprompt = {"multiple_choice_prompt" => msg[:multiple_choice_prompt].clone()} form_hash["current_version"]["prompts"] << multiplechoiceprompt form_hash.delete("multiple_choice_prompt") - elsif msg[:question_type].to_i == 4 - msg[:parsons_prompt].merge!(msg[:prompt]) - form_hash["current_version"]["prompts"] = Array.new - parsonsprompt = {"parsons_prompt" => msg[:parsons_prompt].clone()} - form_hash["current_version"]["prompts"] << parsonsprompt - form_hash.delete("parsons_prompt") + elsif msg[:instructions].present? && msg[:assets].present? + parsons_prompt = {} + parsons_prompt["exercise_id"] = msg[:exercise_id] + parsons_prompt["title"] = msg[:title] + parsons_prompt["author"] = msg[:author] + parsons_prompt["license"] = msg[:license] + parsons_prompt["tags"] = msg[:tags] + parsons_prompt["instructions"] = msg[:instructions] + parsons_prompt["assets"] = msg[:assets] + + form_hash["current_version"]["prompts"] = [{ "parsons_prompt" => parsons_prompt }] + + form_hash.delete("exercise_id") + form_hash.delete("title") + form_hash.delete("author") + form_hash.delete("license") + form_hash.delete("tags") + form_hash.delete("instructions") + form_hash.delete("assets") end form_hash.delete("prompt") form_hash.delete("exercise_version") @@ -446,12 +514,52 @@ def upload_create exercise_version_params = exercise_params[:exercise_version] use_rights = exercise_params[:exercise_collection_id].to_i text_representation = exercise_version_params['text_representation'] - hash = YAML.load(text_representation) else text_representation = File.read(params[:form][:file].path) - hash = YAML.load(text_representation) use_rights = 0 # Personal exercise end + + # 检查 text_representation 中的 tags.style 字段是否包含 "parsons" + if text_representation.include?("tags.style") && text_representation.include?("parsons") + # 使用自定义解析器解析 text_representation + parsed_data = parse_text_representation(text_representation) + + # 使用 ParsonsPromptRepresenter 创建 ParsonsPrompt 对象 + parsons_prompt = ParsonsPromptRepresenter.new(ParsonsPrompt.new).from_hash(parsed_data) + exercises = [parsons_prompt.to_hash] + else + # 使用 ExerciseRepresenter 解析 text_representation + exercises = ExerciseRepresenter.for_collection.new([]).from_hash(YAML.load(text_representation)) + end + + # 后续的处理逻辑保持不变 + exercises.each do |e| + if e[:instructions].present? && e[:assets].present? + # 处理 Parsons 问题 + parsons_prompt = {} + + # 从 e 中获取相应字段的值,并赋值给 parsons_prompt + parsons_prompt["exercise_id"] = e[:exercise_id] + parsons_prompt["title"] = e[:title] + parsons_prompt["author"] = e[:author] + parsons_prompt["license"] = e[:license] + parsons_prompt["tags"] = e[:tags] + parsons_prompt["instructions"] = e[:instructions] + parsons_prompt["assets"] = e[:assets] + + # 更新 prompt + e[:prompt] = [{ "parsons_prompt" => parsons_prompt }] + + # 删除 e 中已经复制到 parsons_prompt 的字段 + e.delete(:exercise_id) + e.delete(:title) + e.delete(:author) + e.delete(:license) + e.delete(:tags) + e.delete(:instructions) + e.delete(:assets) + end + end if !hash.kind_of?(Array) hash = [hash] end @@ -1170,5 +1278,34 @@ def count_submission session[:submit_num] += 1 end end + # ------------------------------------------------------------- + def parse_text_representation(text_representation) + parsed_data = {} + + lines = text_representation.split("\r\n") + lines.each do |line| + if line.include?(":") && !line.start_with?("#") + key, value = line.split(":", 2).map(&:strip) + + if key.include?(".") + nested_keys = key.split(".") + current_level = parsed_data + + nested_keys.each_with_index do |nested_key, index| + if index == nested_keys.length - 1 + current_level[nested_key] = value + else + current_level[nested_key] ||= {} + current_level = current_level[nested_key] + end + end + else + parsed_data[key] = value + end + end + end + + parsed_data + end end diff --git a/app/jobs/code_worker.rb b/app/jobs/code_worker.rb index 0d940e30..85f870a9 100644 --- a/app/jobs/code_worker.rb +++ b/app/jobs/code_worker.rb @@ -189,6 +189,53 @@ def perform(attempt_id) end end + def perform_peml(attempt_id, expected_output) + ActiveRecord::Base.connection_pool.with_connection do + puts "Database connection established" + attempt = Attempt.find(attempt_id) + puts "Attempt found: #{attempt.inspect}" + language = attempt.exercise_version.exercise.language + + if language == 'Python' + # 获取用户提交的代码 + expected_output = expected_output.gsub("\\\\", "\\") + user_solution = attempt.prompt_answers.first.specific.answer + puts "User solution:" + puts user_solution + puts "Expected output:" + puts expected_output + + # 创建临时目录 + attempt_dir = "usr/attempts/active/#{attempt.id}" + FileUtils.mkdir_p(attempt_dir) + + # 将用户代码写入文件 + File.write(attempt_dir + '/solution.py', user_solution) + + # 执行用户代码 + result = execute_pythontest( + 'solution', attempt_dir, 0, user_solution.count("\n") + ) + puts "Execution result:" + puts result.inspect + + if result.nil? + # 没有错误,比较输出结果 + actual_output = File.read(attempt_dir + '/output.txt').strip + success = (actual_output == expected_output) + else + # 有错误,设置 success 为 false + success = false + end + + # 保存 attempt + attempt.error = result + attempt.correct = success + attempt.save! + end + end + end + #~ Private instance methods ................................................. private diff --git a/app/models/attempt.rb b/app/models/attempt.rb index a770040c..6d6548d4 100644 --- a/app/models/attempt.rb +++ b/app/models/attempt.rb @@ -20,12 +20,10 @@ # # Indexes # -# idx_attempts_on_user_exercise_version (user_id,exercise_version_id) -# idx_attempts_on_workout_score_exercise_version (workout_score_id,exercise_version_id) -# index_attempts_on_active_score_id (active_score_id) -# index_attempts_on_exercise_version_id (exercise_version_id) -# index_attempts_on_user_id (user_id) -# index_attempts_on_workout_score_id (workout_score_id) +# index_attempts_on_active_score_id (active_score_id) +# index_attempts_on_exercise_version_id (exercise_version_id) +# index_attempts_on_user_id (user_id) +# index_attempts_on_workout_score_id (workout_score_id) # # Foreign Keys # diff --git a/app/models/parsons_prompt.rb b/app/models/parsons_prompt.rb index 369d0587..688e6ad2 100644 --- a/app/models/parsons_prompt.rb +++ b/app/models/parsons_prompt.rb @@ -1,3 +1,6 @@ class ParsonsPrompt < ActiveRecord::Base belongs_to :parsons + belongs_to :exercise_version + + store_accessor :assets, :code, :test end \ No newline at end of file diff --git a/app/representers/parsons_prompt_representer.rb b/app/representers/parsons_prompt_representer.rb new file mode 100644 index 00000000..7813d4d0 --- /dev/null +++ b/app/representers/parsons_prompt_representer.rb @@ -0,0 +1,32 @@ +# app/representers/parsons_prompt_representer.rb + +class ParsonsPromptRepresenter < Representable::Decorator + include Representable::Hash + + property :instructions + property :exercise_id + + property :assets do + property :code do + property :starter do + collection :files do + property :content, getter: lambda { |*| + code_blocks = [] + content.each do |block| + code_blocks << { "tag" => block["tag"], "display" => block["display"] } + end + code_blocks + }, setter: lambda { |val, *| + self.content = val.map { |block| { "tag" => block["tag"], "display" => block["display"] } } + } + end + end + end + + property :test do + collection :files do + property :content + end + end + end +end \ No newline at end of file diff --git a/app/views/exercises/Jsparson/exercise/simple/s7.html.erb b/app/views/exercises/Jsparson/exercise/simple/s7.html.erb new file mode 100644 index 00000000..d45127da --- /dev/null +++ b/app/views/exercises/Jsparson/exercise/simple/s7.html.erb @@ -0,0 +1,152 @@ + + +
++ Get feedback +
+ Get feedback +