From e8fdc2679895acc37536d1d2ca942d7803407b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Czkewei19=E2=80=9D?= Date: Thu, 2 May 2024 23:03:11 -0400 Subject: [PATCH] document the code --- app/assets/javascripts/dag.js | 45 ++++---- app/assets/javascripts/peml_code.js | 56 ++++----- app/assets/javascripts/simple_code.js | 10 +- app/assets/javascripts/simple_pseudocode.js | 1 + app/controllers/exercises_controller.rb | 109 +++++++++++++----- app/jobs/code_worker.rb | 28 +++-- .../Jsparson/exercise/simple/s10.html.erb | 33 ++++-- app/views/exercises/_exercise.html.haml | 2 + config/routes.rb | 1 + 9 files changed, 184 insertions(+), 101 deletions(-) diff --git a/app/assets/javascripts/dag.js b/app/assets/javascripts/dag.js index 097d1467..69798a50 100644 --- a/app/assets/javascripts/dag.js +++ b/app/assets/javascripts/dag.js @@ -1,14 +1,15 @@ +// Main function to build a graph based on a Directed Acyclic Graph (DAG) representation and a mapping from node values to labels. function buildGraph(dag, nodeValueToLabel) { - const graph = {}; - const inDegree = {}; + const graph = {}; // Object to hold the adjacency list representation of the graph. + const inDegree = {}; // Object to hold the in-degree count for each node. - // initialize graph and in-degree + // Initialize graph and in-degree for each node based on the nodeValueToLabel mapping. for (const nodeLabel in nodeValueToLabel) { graph[nodeLabel] = []; inDegree[nodeLabel] = 0; } - // parse the DAG and build the graph + // Parse the DAG string, build the graph and calculate in-degrees. const lines = dag.split('\n'); for (const line of lines) { const parts = line.split(':').map(part => part.trim()); @@ -17,8 +18,8 @@ function buildGraph(dag, nodeValueToLabel) { const dependencies = parts[1].split(' ').filter(label => label !== ''); for (const dependency of dependencies) { if (dependency !== '-1' && nodeValueToLabel[nodeLabel] !== undefined && nodeValueToLabel[dependency] !== undefined) { - graph[nodeLabel].push(dependency); // add dependency to the graph - inDegree[dependency]++; // increment in-degree of the dependency + graph[nodeLabel].push(dependency); // Add dependency to the node's adjacency list. + inDegree[dependency]++; // Increment in-degree count for the dependency. } } } @@ -29,20 +30,21 @@ function buildGraph(dag, nodeValueToLabel) { return { graph, inDegree }; } - +// Processes the solution by validating the sequence of nodes against the graph's dependencies. function processSolution(solution, graph, inDegree, nodeValueToLabel) { console.log("processSolution:", solution); console.log("processnodeValueToLabel:", nodeValueToLabel); - const visited = new Set(); + const visited = new Set(); // Set to track visited nodes. + // Process each node in the solution, ensuring all dependencies are met. for (const nodeText of solution) { const nodeLabel = Object.keys(nodeValueToLabel).find( - (label) => nodeValueToLabel[label] === nodeText + label => nodeValueToLabel[label] === nodeText ); if (nodeLabel === undefined) { console.log("Skipping node not found in nodeValueToLabel:", nodeText); - continue; // jump to the next node + continue; // Skip if node is not found in mapping. } console.log('Current label:', nodeLabel); @@ -51,28 +53,26 @@ function processSolution(solution, graph, inDegree, nodeValueToLabel) { visited.add(nodeLabel); - // check if the node has dependencies + // Check if all dependencies of the current node have been visited. for (const dependencyLabel of graph[nodeLabel]) { if (!visited.has(dependencyLabel)) { console.error("Dependency not satisfied:", nodeText, "depends on", nodeValueToLabel[dependencyLabel]); - return false; + return false; // Dependency check failed. } } } - // check if all nodes were visited + // Ensure all nodes were visited. if (visited.size !== Object.keys(nodeValueToLabel).length) { console.error("Not all nodes in nodeValueToLabel were visited."); return false; } console.log('Visited nodes:', Array.from(visited)); - return true; + return true; // All checks passed. } - - - +// High-level function to process the DAG and the solution together. function processDAG(dag, solution, nodeValueToLabel) { console.log("DAG:", dag); console.log("Node value to label mapping:", nodeValueToLabel); @@ -81,17 +81,18 @@ function processDAG(dag, solution, nodeValueToLabel) { return result; } +// Extracts and maps the solution to the corresponding node labels. function extractCode(solution, nodeValueToLabel) { const code = []; const newNodeValueToLabel = {}; for (const nodeText of solution) { const nodeLabel = Object.keys(nodeValueToLabel).find( - (key) => nodeValueToLabel[key] === nodeText + key => nodeValueToLabel[key] === nodeText ); if (nodeLabel !== undefined) { - code.push(nodeText); - newNodeValueToLabel[nodeLabel] = nodeText; + code.push(nodeText); // Collect the node text for the final code array. + newNodeValueToLabel[nodeLabel] = nodeText; // Map labels to node texts. } } - return { code, newNodeValueToLabel }; - } \ No newline at end of file + return { code, newNodeValueToLabel }; // Return the processed code and updated mapping. + } diff --git a/app/assets/javascripts/peml_code.js b/app/assets/javascripts/peml_code.js index 93dd297c..27b63d13 100644 --- a/app/assets/javascripts/peml_code.js +++ b/app/assets/javascripts/peml_code.js @@ -1,60 +1,64 @@ const url = "https://skynet.cs.vt.edu/peml-live/api/parse"; -// 获取 PEML 数据 +// Fetch the PEML data from a local file. fetch('/data/s7.peml') .then(response => { + // Convert the response to text format. return response.text(); }) .then(pemlText => { + // Construct the payload to send in the POST request. const payload = { - "peml": pemlText, - "output_format": "json", - "render_to_html": "true" + "peml": pemlText, // The PEML data as text. + "output_format": "json", // Specify the output format as JSON. + "render_to_html": "true" // Request HTML rendering. }; - // 发送 POST 请求进行解析 + // Send a POST request to the server to parse the PEML text. $.post(url, payload, function(data, status) { - console.log('Post status:', status); - console.log('Post data:', data); + console.log('Post status:', status); // Log the status of the POST request. + console.log('Post data:', data); // Log the data received from the POST request. - // 检查服务器响应数据是否包含所需字段 + // Check if the server's response contains the necessary fields. 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')); + // Extract the required fields from the response. + var title = data.title.split(" -- ")[0]; // Get the title and clean it. + var instructions = data.instructions; // Get the instructions directly. + var initialArray = data.assets.code.starter.files[0].content.map(item => item.code.split('\\n')); // Process the initial array of code. - // 在这里使用你获取的字段 - document.getElementById("title").innerHTML = title; - document.getElementById("instructions").innerHTML = instructions; + // Use the extracted fields in your application. + document.getElementById("title").innerHTML = title; // Display the title. + document.getElementById("instructions").innerHTML = instructions; // Display the instructions. - var parson = new ParsonsWidget(); - parson.init(initialArray); - parson.shuffleLines(); + var parson = new ParsonsWidget(); // Create a new ParsonsWidget instance. + parson.init(initialArray); // Initialize the widget with the initial array. + parson.shuffleLines(); // Shuffle the lines initially. + // Add an event listener for creating a new instance of the shuffled lines. $("#newInstanceLink").click(function(event) { event.preventDefault(); parson.shuffleLines(); }); + // Add an event listener for providing feedback. $("#feedbackLink").click(function(event) { event.preventDefault(); var fb = parson.getFeedback(); - $("#feedback").html(fb.feedback); + $("#feedback").html(fb.feedback); // Display the feedback. if (fb.success) { - score = 50; + score = 50; // Set score on success. } else { - score = 0; + score = 0; // Reset score on failure. } - updateScore(score); + updateScore(score); // Update the score display. }); function updateScore(score) { - // 更新分数的代码 + // Implement the logic to update the score in the UI or backend. } } else { - console.error('服务器响应数据不完整或格式不正确'); - // 在这里处理服务器响应数据不完整或格式不正确的情况 + console.error('Incomplete or incorrect server response data.'); + // Handle cases where server response data is incomplete or incorrect. } }); - }); \ No newline at end of file + }); diff --git a/app/assets/javascripts/simple_code.js b/app/assets/javascripts/simple_code.js index 555aeb54..5c7dd88b 100644 --- a/app/assets/javascripts/simple_code.js +++ b/app/assets/javascripts/simple_code.js @@ -6,6 +6,7 @@ $(document).ready(function(){ Sk.canvas = "studentCanvas"; $.getJSON('/data/simple_code.json', function(response) { data = response; + // get the specific exercise data var externalIdElement = document.getElementById("exercise-data"); var externalId = externalIdElement.getAttribute("data-external-id"); var initial = data[index]['initial']; @@ -15,14 +16,9 @@ $(document).ready(function(){ document.getElementById("instructions").innerHTML = data[index].instructions; config.sortableId = 'sortable'; config.trashId = 'sortableTrash'; - console.log(data[index]['parsonsConfig'] - ['turtleModelCode']); - console.log(externalId) - // 如果config中有turtleModelCode,就把grader改成TurtleGrader + // if there is a turtle model code, use the turtle grader if (data[index]['parsonsConfig']['turtleModelCode']) { config.grader = ParsonsWidget._graders.TurtleGrader; - console.log("有乌龟") - } else { config.grader = ParsonsWidget._graders.LanguageTranslationGrader; } @@ -81,4 +77,4 @@ $(document).ready(function(){ } }); } -}); \ No newline at end of file +}); diff --git a/app/assets/javascripts/simple_pseudocode.js b/app/assets/javascripts/simple_pseudocode.js index 440ab10b..ca159977 100644 --- a/app/assets/javascripts/simple_pseudocode.js +++ b/app/assets/javascripts/simple_pseudocode.js @@ -1,3 +1,4 @@ +// This file is used to create a Parsons problem for the simple pseudocode exercise var initial = 'IF $$toggle::a::b$$ $$toggle::<::>::<>$$ b THEN\n min := a\nELSE\n min := b\nENDIF'; var parson; $(document).ready(function(){ diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index b597b481..915061af 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -36,9 +36,12 @@ def show_exercise # def get_external_id(step) + # set the full name of the exercise full_name = "parsons_#{step}" + # find the exercise instance exercise = Exercise.where('name LIKE ?', "%#{full_name}%").first.reload puts "exercise = #{exercise.inspect}" + # return the external id of the exercise exercise ? exercise.external_id : nil end @@ -282,91 +285,139 @@ def edit session[:return_to] = @return_to end # ------------------------------------------------------------- - def edit_parsons - step = params[:step].to_i - full_name = "parsons_s#{step}" - puts "full_name = #{full_name}" - @exercise = Exercise.where('name LIKE ?', "%#{full_name}%").first - - if step >= 8 - html_file_path = Rails.root.join('app', 'views', 'exercises', 'Jsparson', 'exercise', 'simple', "s#{step}.html.erb") - if File.exist?(html_file_path) - file_content = File.read(html_file_path) - peml_content_match = file_content.match(/pemlContent\s*=\s*`(.*?)`/m) - if peml_content_match - @peml_content = peml_content_match[1].strip - else - @peml_content = '' - end +# Defines a method to edit Parsons exercises based on a step number. +def edit_parsons + # Converts the step parameter from the request into an integer. + step = params[:step].to_i + + # Constructs a string that represents the exercise name based on the step number. + full_name = "parsons_s#{step}" + puts "full_name = #{full_name}" + + # Fetches the first exercise from the database that matches the constructed name. + @exercise = Exercise.where('name LIKE ?', "%#{full_name}%").first + + # Checks if the step number is 8 or greater, indicating handling for HTML-based exercises. + if step >= 8 + # Builds the file path to the corresponding HTML exercise file. + html_file_path = Rails.root.join('app', 'views', 'exercises', 'Jsparson', 'exercise', 'simple', "s#{step}.html.erb") + + # Checks if the HTML file exists. + if File.exist?(html_file_path) + # Reads the content of the HTML file. + file_content = File.read(html_file_path) + + # Uses a regular expression to extract PEMl content enclosed within backticks. + peml_content_match = file_content.match(/pemlContent\s*=\s*`(.*?)`/m) + + # Assigns the extracted PEMl content to an instance variable, or an empty string if no content was found. + if peml_content_match + @peml_content = peml_content_match[1].strip else @peml_content = '' end else - json_file_path = Rails.root.join('public', 'data', 'simple_code.json') - if File.exist?(json_file_path) - file_content = File.read(json_file_path) - json_content = JSON.parse(file_content) - @step_data = json_content["s#{step}"] - else - @step_data = {} - end + # Sets the PEMl content to an empty string if the HTML file does not exist. + @peml_content = '' + end + else + # For steps below 8, handles JSON-based exercise data. + json_file_path = Rails.root.join('public', 'data', 'simple_code.json') + + # Checks if the JSON file exists. + if File.exist?(json_file_path) + # Reads and parses the JSON file content. + file_content = File.read(json_file_path) + json_content = JSON.parse(file_content) + + # Retrieves data for the current step and assigns it to an instance variable. + @step_data = json_content["s#{step}"] + else + # Initializes @step_data as an empty hash if the JSON file does not exist. + @step_data = {} end end +end + # ------------------------------------------------------------- + # Updates the Parsons exercises based on the step number and content provided via params. def update_parsons + # Converts the step parameter to an integer. step = params[:step].to_i - + + # Checks if the step number is greater than or equal to 8, indicating a different handling logic. if step >= 8 + # Retrieves the PEMl content from parameters. peml_content = params[:peml_content] if peml_content.present? + # Constructs the file path for the HTML exercise file based on the step number. html_file_path = Rails.root.join('app', 'views', 'exercises', 'Jsparson', 'exercise', 'simple', "s#{step}.html.erb") + # Checks if the specified HTML file exists. unless File.exist?(html_file_path) + # Redirects to the editing path with an alert if the file does not exist. redirect_to edit_parsons_exercise_path(step: step), alert: 'HTML file not found.' return end - + begin + # Reads the content of the HTML file. file_content = File.read(html_file_path) + # Updates the content by replacing the old PEMl content with the new one. updated_content = file_content.gsub(/pemlContent\s*=\s*`.*?`/m, "pemlContent = `#{peml_content}`") + # Writes the updated content back to the file. File.write(html_file_path, updated_content) + # Redirects back to the exercise page with a success notice. redirect_to edit_parsons_exercise_path(step: step), notice: 'Parsons updated successfully.' rescue => e - Rails.logger.debug e.inspect # Log the error for inspection + # Logs the exception and redirects with an error message if an error occurs during the file update. + Rails.logger.debug e.inspect redirect_to edit_parsons_exercise_path(step: step), alert: "Failed to update Parsons: #{e.message}" end else + # Redirects with an alert if the PEMl content is missing. redirect_to edit_parsons_exercise_path(step: step), alert: 'PEML content is missing.' end else + # Handles steps less than 8, expecting updated JSON content. updated_content = params[:step_content] if updated_content.present? + # Defines the path to the JSON file holding exercise data. json_file_path = Rails.root.join('public', 'data', 'simple_code.json') unless File.exist?(json_file_path) redirect_to edit_parsons_exercise_path(step: step), alert: 'JSON file not found.' return end - + begin + # Parses the existing content from the JSON file. content = JSON.parse(File.read(json_file_path)) + # Parses the updated JSON content provided via parameters. updated_data = JSON.parse(updated_content) if content["s#{step}"].present? + # Updates the specific step data with the new content. content["s#{step}"] = updated_data + # Writes the updated JSON back to the file. File.write(json_file_path, JSON.pretty_generate(content)) + # Redirects with a success notice. redirect_to edit_parsons_exercise_path(step: step), notice: 'Parsons updated successfully.' else + # Redirects with an alert if the specific step is not found in the JSON file. redirect_to edit_parsons_exercise_path(step: step), alert: "Step not found in JSON." end rescue JSON::ParserError => e - Rails.logger.debug e.inspect # Log the error for inspection + # Logs and handles JSON parsing errors. + Rails.logger.debug e.inspect redirect_to edit_parsons_exercise_path(step: step), alert: "Failed to parse JSON: #{e.message}" end else + # Redirects with an alert if the content for the step is missing. redirect_to edit_parsons_exercise_path(step: step), alert: 'Step content is missing.' end end end + # ------------------------------------------------------------- # POST /exercises def create diff --git a/app/jobs/code_worker.rb b/app/jobs/code_worker.rb index 85f870a9..c3c68d22 100644 --- a/app/jobs/code_worker.rb +++ b/app/jobs/code_worker.rb @@ -189,46 +189,55 @@ def perform(attempt_id) end end + # Defines a method to execute a PEML (Programming Exercise Markup Language) script based on a given attempt and expected output. def perform_peml(attempt_id, expected_output) + # Manages database connections to ensure thread safety and connection pooling. ActiveRecord::Base.connection_pool.with_connection do puts "Database connection established" + + # Retrieves the attempt record from the database using the provided attempt_id. attempt = Attempt.find(attempt_id) puts "Attempt found: #{attempt.inspect}" + + # Determines the programming language of the exercise associated with the attempt. language = attempt.exercise_version.exercise.language + # Specific handling for Python language exercises. if language == 'Python' - # 获取用户提交的代码 + # Cleans up the expected output to handle escaped characters properly. expected_output = expected_output.gsub("\\\\", "\\") + # Retrieves the user's solution from the first prompt's answer. user_solution = attempt.prompt_answers.first.specific.answer puts "User solution:" puts user_solution puts "Expected output:" puts expected_output - # 创建临时目录 + # Creates a temporary directory specific to the attempt to isolate execution. attempt_dir = "usr/attempts/active/#{attempt.id}" FileUtils.mkdir_p(attempt_dir) - # 将用户代码写入文件 - File.write(attempt_dir + '/solution.py', user_solution) + # Writes the user's solution into a Python script file within the temporary directory. + File.write("#{attempt_dir}/solution.py", user_solution) - # 执行用户代码 + # Executes the user's Python script and captures the result. result = execute_pythontest( 'solution', attempt_dir, 0, user_solution.count("\n") ) puts "Execution result:" puts result.inspect + # Evaluates the execution result to determine success or failure. if result.nil? - # 没有错误,比较输出结果 - actual_output = File.read(attempt_dir + '/output.txt').strip + # If no error, compare the actual output with the expected output. + actual_output = File.read("#{attempt_dir}/output.txt").strip success = (actual_output == expected_output) else - # 有错误,设置 success 为 false + # If an error occurred during execution, mark the attempt as unsuccessful. success = false end - # 保存 attempt + # Update the attempt record with the outcome of the execution. attempt.error = result attempt.correct = success attempt.save! @@ -237,6 +246,7 @@ def perform_peml(attempt_id, expected_output) end + #~ Private instance methods ................................................. private diff --git a/app/views/exercises/Jsparson/exercise/simple/s10.html.erb b/app/views/exercises/Jsparson/exercise/simple/s10.html.erb index 5c4eb68c..c3d34aad 100644 --- a/app/views/exercises/Jsparson/exercise/simple/s10.html.erb +++ b/app/views/exercises/Jsparson/exercise/simple/s10.html.erb @@ -12,6 +12,26 @@ width: 800px; height: 800px; } + .fixed { + pointer-events: none; /* 禁用鼠标事件 */ + background-color: #f5f5f5; /* 与屏幕截图中的灰色相匹配 */ + color: #333; /* 文字颜色 */ + border: 1px solid #ddd; /* 边框颜色和样式 */ + margin: 0; /* 移除外边距 */ + padding: 10px; /* 内边距 */ + box-sizing: border-box; /* 确保宽度包含内边距和边框 */ + } + + /* 可拖动行的样式 */ + .sortable-code li { + list-style-type: none; /* 移除列表项前的标记 */ + margin: 5px 0; /* 添加一点外边距 */ + padding: 10px; /* 内边距 */ + border: 1px solid #ddd; /* 边框颜色和样式 */ + background-color: #fff; /* 背景颜色 */ + cursor: move; /* 鼠标样式表示可以移动 */ + } + @@ -22,6 +42,7 @@
+

Get feedback @@ -68,8 +89,7 @@ [.content] tag: fixed display:---------- - public static void Swap(int[] arr, int i, int j) - { + public static void Swap(int[] arr, int i, int j){ ---------- tag: one @@ -119,7 +139,7 @@ } console.log("fixedlines:" + fixedLines); shuffle(shuffleLines); - const codeline = [fixedLines[0], ...shuffleLines, fixedLines[1]]; + const codeline = fixedLines.concat(shuffleLines); console.log("Codeline:", codeline); const nodeValueToLabel = extractNodeValueToLabel(pemlContent); console.log(nodeValueToLabel); @@ -144,15 +164,12 @@ var externalIdElement = document.getElementById("exercise-data"); var externalId = externalIdElement.getAttribute("data-external-id"); console.log(externalId) + + parson.init(codeline); parson.shuffleLines(); - $("#sortable li").each(function() { - if (fixedLines.includes($(this).text().trim())) { - $(this).addClass("fixed-item"); - } - }); $("#feedbackLink").click(function(event) { event.preventDefault(); diff --git a/app/views/exercises/_exercise.html.haml b/app/views/exercises/_exercise.html.haml index f18b9e3b..04d0d6ab 100644 --- a/app/views/exercises/_exercise.html.haml +++ b/app/views/exercises/_exercise.html.haml @@ -95,9 +95,11 @@ = link_to exercise.display_name, show_exercise_path(step: "s#{step}") - else = link_to exercise.display_name, exercise_practice_path(exercise) + -# Detects if a name has parsons - is_parsons = exercise.name.downcase.include?('parsons') - if [ 'show', 'index', 'search' ].include?(action_name) && (current_user.andand.can? :edit, exercise) .right + -# - if is_parsons - step = exercise.name.downcase.match(/parsons_s(\d+)/)[1] = button_link 'Edit', edit_parsons_exercise_path(step: step), class: 'btn btn-primary btn-sm' diff --git a/config/routes.rb b/config/routes.rb index 85e37943..2cd70d85 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,6 +67,7 @@ as: :exercises_query_data get 'exercises/download_attempt_data' => 'exercises#download_attempt_data', as: :download_exercise_attempt_data + # add the address of the parsons exercise to the route get '/gym/exercises/Jsparson/exercise/simple/:step', to: 'exercises#show_exercise', as: 'show_exercise' get '/exercises/Jsparson/exercise/simple/:step/parsons_edit', to: 'exercises#edit_parsons', as: 'edit_parsons_exercise' post '/exercises/Jsparson/exercise/simple/:step/update_parsons', to: 'exercises#update_parsons', as: 'update_parsons_exercise'