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 @@ + + + + Simple js-parsons example assignment + <%= stylesheet_link_tag 'parsons' %> + <%= stylesheet_link_tag 'prettify' %> + <%= stylesheet_link_tag 'odsaAV-min' %> + <%= stylesheet_link_tag 'JSAV' %> + <%= javascript_include_tag 'prettify' %> + + + +
+

+

+
+
+
+
+
+

+ Get feedback +

Feedback

+
Your feedback will appear here when you check your answer.
+

+
+
+ <%= javascript_include_tag 'jquery' %> + <%= javascript_include_tag 'jquery-ui' %> + <%= javascript_include_tag 'jquery.ui.touch-punch.min' %> + <%= javascript_include_tag 'underscore-min' %> + <%= javascript_include_tag 'lis' %> + <%= javascript_include_tag 'parsons' %> + <%= javascript_include_tag 'skulpt' %> + <%= javascript_include_tag 'skulpt-stdlib' %> + + + + \ No newline at end of file diff --git a/app/views/exercises/Jsparson/exercise/simple/s8.html.erb b/app/views/exercises/Jsparson/exercise/simple/s8.html.erb new file mode 100644 index 00000000..76c2ae88 --- /dev/null +++ b/app/views/exercises/Jsparson/exercise/simple/s8.html.erb @@ -0,0 +1,190 @@ + + + + Simple js-parsons example assignment + <%= stylesheet_link_tag 'parsons' %> + <%= stylesheet_link_tag 'prettify' %> + <%= stylesheet_link_tag 'odsaAV-min' %> + <%= stylesheet_link_tag 'JSAV' %> + <%= javascript_include_tag 'prettify' %> + + + +
+

+

+
+
+
+
+
+

+ Get feedback +

Feedback

+
Your feedback will appear here when you check your answer.
+

+
+
+ <%= javascript_include_tag 'jquery' %> + <%= javascript_include_tag 'jquery-ui' %> + <%= javascript_include_tag 'jquery.ui.touch-punch.min' %> + <%= javascript_include_tag 'dag' %> + <%= javascript_include_tag 'underscore-min' %> + <%= javascript_include_tag 'lis' %> + <%= javascript_include_tag 'parsons' %> + <%= javascript_include_tag 'skulpt' %> + <%= javascript_include_tag 'skulpt-stdlib' %> + + + + \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 5a5b2231..85e37943 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,7 @@ post 'lti/launch', as: :lti_launch # => 'workout_offerings#practice', as: :lti_workout_offering_practice post 'lti/assessment' + post '/execute_peml', to: 'exercises#execute_peml' get 'home' => 'home#index' get 'main' => 'home#index' diff --git a/db/migrate/20240413031046_create_parsons_prompts.rb b/db/migrate/20240413031046_create_parsons_prompts.rb new file mode 100644 index 00000000..a0562c94 --- /dev/null +++ b/db/migrate/20240413031046_create_parsons_prompts.rb @@ -0,0 +1,12 @@ +class CreateParsonsPrompts < ActiveRecord::Migration[6.1] + def change + create_table :parsons_prompts do |t| + t.text :title + t.text :instructions + t.string :exercise_id + t.json :assets + t.references :exercise_version, null: false, foreign_key: true + t.timestamps + end + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index e20001c2..10f8cec1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20240207040304) do +ActiveRecord::Schema.define(version: 20240207035240) do create_table "active_admin_comments", force: :cascade do |t| t.string "namespace", limit: 255 @@ -47,9 +47,7 @@ add_index "attempts", ["active_score_id"], name: "index_attempts_on_active_score_id", using: :btree add_index "attempts", ["exercise_version_id"], name: "index_attempts_on_exercise_version_id", using: :btree - add_index "attempts", ["user_id", "exercise_version_id"], name: "idx_attempts_on_user_exercise_version", using: :btree add_index "attempts", ["user_id"], name: "index_attempts_on_user_id", using: :btree - add_index "attempts", ["workout_score_id", "exercise_version_id"], name: "idx_attempts_on_workout_score_exercise_version", using: :btree add_index "attempts", ["workout_score_id"], name: "index_attempts_on_workout_score_id", using: :btree create_table "attempts_tag_user_scores", id: false, force: :cascade do |t| @@ -80,25 +78,6 @@ add_index "choices_multiple_choice_prompt_answers", ["choice_id", "multiple_choice_prompt_answer_id"], name: "choices_multiple_choice_prompt_answers_idx", unique: true, using: :btree add_index "choices_multiple_choice_prompt_answers", ["multiple_choice_prompt_answer_id"], name: "choices_MC_prompt_answers_MC_prompt_answer_id_fk", using: :btree - create_table "parsons", force: :cascade do |t| - t.string "title" - t.text "instructions" - t.text "initial" - t.text "unittest", default: "" - t.string "type" - t.text "concepts" - t.integer "order" - t.text "parsonsConfig" - t.timestamps null: false - end - - create_table :parsons_prompt do |t| - t.references :parsons, null: false, foreign_key: true - t.text :prompt_text - t.integer :position - t.timestamps null: false - end - create_table "coding_prompt_answers", force: :cascade do |t| t.text "answer", limit: 65535 t.text "error", limit: 65535 diff --git a/public/data/s7.peml b/public/data/s7.peml new file mode 100644 index 00000000..fe99eda1 --- /dev/null +++ b/public/data/s7.peml @@ -0,0 +1,36 @@ +title: Your First Parsons Problem -- Execute +author: Cliff Shaffer (from js-Parsons, converted to PEML) + +tags.topics: Intro Parsons Problem +tags.style: parsons, execute + +instructions:---------- +**Your task**: Construct a Python program that prints strings "Hello", "Parsons", and "Problems" on their own lines. You can get feedback on your current solution with the feedback button. You should construct your program by dragging and dropping the lines to the solution area on the right. +---------- + +[systems] +language: Python + +[assets.code.wrapper.files] +content:---------- + ___ +---------- + +[assets.code.starter.files] +[.content] +tag: one +code: print('Hello') + +tag: two +code: print('Parsons') + +tag: three +code: print('Problems!') +[] + +[assets.test.files] +format: text/csv-unquoted +content:---------- +expected,description +Hello\nParsons\nProblems! +---------- \ No newline at end of file diff --git a/public/data/simple_code.json b/public/data/simple_code.json index 7c5e4ad3..71997545 100644 --- a/public/data/simple_code.json +++ b/public/data/simple_code.json @@ -5,11 +5,11 @@ "max_wrong_lines": 1, "vartests": [ { - "initcode": "min = None\na = 1\nb = 2", + "initcode": "min = None\na = 0\nb = 2", "code": "", - "message": "Testing with a = 1 ja b = 2", + "message": "Testing with a = 0 ja b = 2", "variables": { - "min": 1 + "min": 0 } }, { @@ -80,11 +80,11 @@ "order": 2 }, "s6": { - "initial": "REPEAT 3 TIMES\nforward(100)\nleft(120)\nENDREPEAT", + "initial": "REPEAT 3 TIMES\nforward(200)\nleft(120)\nENDREPEAT", "parsonsConfig": { "max_wrong_lines": 1, - "turtleModelCode": "modelTurtle.forward(100)\nmodelTurtle.left(120)\nmodelTurtle.forward(100)\nmodelTurtle.left(120)\nmodelTurtle.forward(100)\nmodelTurtle.left(120)", - "executable_code": "for i in range(0,3):\nmyTurtle.forward(100)\nmyTurtle.left(120)\npass", + "turtleModelCode": "modelTurtle.forward(200)\nmodelTurtle.left(120)\nmodelTurtle.forward(200)\nmodelTurtle.left(120)\nmodelTurtle.forward(200)\nmodelTurtle.left(120)", + "executable_code": "for i in range(0,3):\nmyTurtle.forward(200)\nmyTurtle.left(120)\npass", "programmingLang": "pseudo" }, "title": "draw triangle", diff --git a/spec/factories/attempts.rb b/spec/factories/attempts.rb index b696476c..8c5b912f 100644 --- a/spec/factories/attempts.rb +++ b/spec/factories/attempts.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 #