diff --git a/app/assets/javascripts/dag.js b/app/assets/javascripts/dag.js
index 3aaae79e..097d1467 100644
--- a/app/assets/javascripts/dag.js
+++ b/app/assets/javascripts/dag.js
@@ -1,106 +1,97 @@
-// 根据 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;
+ const graph = {};
+ const inDegree = {};
+
+ // initialize graph and in-degree
+ for (const nodeLabel in nodeValueToLabel) {
+ graph[nodeLabel] = [];
+ inDegree[nodeLabel] = 0;
}
-
- // 构建入度数组并初始化队列
- function initializeCounts(graph) {
- const inDegree = {};
- const queue = [];
-
- for (const node in graph) {
- inDegree[node] = graph[node].length;
- if (inDegree[node] === 0) {
- queue.push(node);
+
+ // parse the DAG and build the graph
+ const lines = dag.split('\n');
+ for (const line of lines) {
+ const parts = line.split(':').map(part => part.trim());
+ if (parts.length === 2) {
+ const nodeLabel = parts[0];
+ 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
+ }
}
}
-
- 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.');
+ console.log("Graph:", graph);
+ console.log("In-degree:", inDegree);
+ return { graph, inDegree };
+}
+
+
+function processSolution(solution, graph, inDegree, nodeValueToLabel) {
+ console.log("processSolution:", solution);
+ console.log("processnodeValueToLabel:", nodeValueToLabel);
+ const visited = new Set();
+
+ for (const nodeText of solution) {
+ const nodeLabel = Object.keys(nodeValueToLabel).find(
+ (label) => nodeValueToLabel[label] === nodeText
+ );
+
+ if (nodeLabel === undefined) {
+ console.log("Skipping node not found in nodeValueToLabel:", nodeText);
+ continue; // jump to the next node
}
-
- 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");
+
+ console.log('Current label:', nodeLabel);
+ console.log('Current node text:', nodeText);
+ console.log('Node value to label mapping:', nodeValueToLabel);
+
+ visited.add(nodeLabel);
+
+ // check if the node has dependencies
+ for (const dependencyLabel of graph[nodeLabel]) {
+ if (!visited.has(dependencyLabel)) {
+ console.error("Dependency not satisfied:", nodeText, "depends on", nodeValueToLabel[dependencyLabel]);
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;
}
+
+ // check if 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;
+}
+
+
+
- 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;
+function processDAG(dag, solution, nodeValueToLabel) {
+ console.log("DAG:", dag);
+ console.log("Node value to label mapping:", nodeValueToLabel);
+ const { graph, inDegree } = buildGraph(dag, nodeValueToLabel);
+ const result = processSolution(solution, graph, inDegree, nodeValueToLabel);
+ return result;
+}
+
+function extractCode(solution, nodeValueToLabel) {
+ const code = [];
+ const newNodeValueToLabel = {};
+ for (const nodeText of solution) {
+ const nodeLabel = Object.keys(nodeValueToLabel).find(
+ (key) => nodeValueToLabel[key] === nodeText
+ );
+ if (nodeLabel !== undefined) {
+ code.push(nodeText);
+ newNodeValueToLabel[nodeLabel] = nodeText;
+ }
+ }
+ return { code, newNodeValueToLabel };
}
\ No newline at end of file
diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb
index 2902e7d8..b32f7c3a 100644
--- a/app/controllers/exercises_controller.rb
+++ b/app/controllers/exercises_controller.rb
@@ -518,50 +518,37 @@ def upload_create
text_representation = File.read(params[:form][:file].path)
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)
+
+ begin
+ hash = YAML.load(text_representation)
+ if !hash.kind_of?(Array)
+ hash = [hash]
+ is_parsons = false
+ end
+ rescue Psych::SyntaxError
+ attributes = {}
+ text_representation.scan(/^(\w+(?:\.\w+)*):(.*)$/) do |key, value|
+ keys = key.split('.')
+ target = attributes
+ keys[0..-2].each do |k|
+ target[k] ||= {}
+ target = target[k]
+ end
+ target[keys.last] = value.strip
+ end
+
+ title = attributes['title']
+ assets_code = attributes.dig('assets', 'code', 'starter', 'files')
+ if assets_code
+ assets_code_content = assets_code['content']
+ hash = assets_code_content.scan(/^tag:\s*(\w+)\s*\n^display:\s*(.+)$/m).map do |tag, display|
+ { 'tag' => tag, 'display' => display }
+ end
+ is_parsons = true
+ else
+ hash = []
+ is_parsons = false
end
- end
- if !hash.kind_of?(Array)
- hash = [hash]
end
files = exercise_params[:files]
@@ -601,11 +588,16 @@ def upload_create
@return_to = session.delete(:return_to) || exercises_path
# parse the text_representation
- exercises = ExerciseRepresenter.for_collection.new([]).from_hash(hash)
+ if is_parsons
+ exercises = ParsonsExerciseRepresenter.for_collection.new([]).from_hash(hash)
+ else
+ exercises = ExerciseRepresenter.for_collection.new([]).from_hash(hash)
+ end
success_all = true
error_msgs = []
success_msgs = []
exercises.each do |e|
+ e.name = title if title.present?
if !e.save
success_all = false
# put together an error message
@@ -665,7 +657,7 @@ def upload_create
# Notify user of success
success_msgs <<
- "
X#{e.id}: #{e.name} saved, try it #{view_context.link_to 'here', exercise_practice_path(e)}."
+ "X#{e.id}: #{e.name || 'Parsons problem'} saved, try it #{view_context.link_to 'here', exercise_practice_path(e)}."
end
end
diff --git a/app/models/attempt.rb b/app/models/attempt.rb
index 6d6548d4..a770040c 100644
--- a/app/models/attempt.rb
+++ b/app/models/attempt.rb
@@ -20,10 +20,12 @@
#
# Indexes
#
-# 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)
+# 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)
#
# Foreign Keys
#
diff --git a/app/models/exercise_version.rb b/app/models/exercise_version.rb
index 1c781932..554f8fbd 100644
--- a/app/models/exercise_version.rb
+++ b/app/models/exercise_version.rb
@@ -54,6 +54,7 @@ class ExerciseVersion < ActiveRecord::Base
has_many :resource_files, through: :ownerships
belongs_to :creator, class_name: 'User'
belongs_to :irt_data, dependent: :destroy
+ has_one :parsons_prompt
#~ Hooks ....................................................................
diff --git a/app/models/parsons_prompt.rb b/app/models/parsons_prompt.rb
index 688e6ad2..88c4e183 100644
--- a/app/models/parsons_prompt.rb
+++ b/app/models/parsons_prompt.rb
@@ -1,6 +1,28 @@
+# == Schema Information
+#
+# Table name: parsons_prompts
+#
+# id :integer not null, primary key
+# assets :text(65535)
+# instructions :text(65535)
+# title :text(65535)
+# created_at :datetime
+# updated_at :datetime
+# exercise_id :string(255)
+# exercise_version_id :integer not null
+#
+# Indexes
+#
+# fk_rails_40d6ef5b4f (exercise_version_id)
+#
+# Foreign Keys
+#
+# fk_rails_... (exercise_version_id => exercise_versions.id)
+#
+
class ParsonsPrompt < ActiveRecord::Base
belongs_to :parsons
belongs_to :exercise_version
- store_accessor :assets, :code, :test
- end
\ No newline at end of file
+ serialize :assets, JSON
+ end
diff --git a/app/representers/ParsonsExerciseRepresenter.rb b/app/representers/ParsonsExerciseRepresenter.rb
new file mode 100644
index 00000000..69ab4dd6
--- /dev/null
+++ b/app/representers/ParsonsExerciseRepresenter.rb
@@ -0,0 +1,46 @@
+class ParsonsExerciseRepresenter < Representable::Decorator
+ include Representable::Hash
+
+ collection_representer instance: lambda { |options|
+ fragment = options[:fragment]
+ if fragment.has_key? 'external_id'
+ e = Exercise.where(external_id: fragment['external_id']).first
+ e || Exercise.new
+ else
+ Exercise.new
+ end
+ }
+
+ property :name
+ property :external_id
+ property :is_public, setter: lambda { |val, *| self.is_public = val.to_b }
+ property :experience
+ property :language_list, getter: lambda { |*| language_list.to_s }
+ property :style_list, getter: lambda { |*| style_list.to_s }
+ property :tag_list, getter: lambda { |*| tag_list.to_s }
+
+ property :current_version, class: ExerciseVersion, setter: lambda { |val, *|
+ self.current_version = val
+ self.exercise_versions << self.current_version
+ self.current_version.exercise = self
+ }, instance: lambda { |*| ExerciseVersion.new } do
+ property :version, setter: lambda { |*| }
+ property :creator, getter: lambda { |*| creator.andand.email }, setter: lambda { |val, *|
+ if val
+ self.creator = User.where(email: val).first
+ end
+ }
+
+ property :starter_code, getter: lambda { |*|
+ files = assets_code_starter_files
+ content = files.map { |file| file['content'] }.join('\n') if files
+ content
+ }
+
+ property :test_code, getter: lambda { |*|
+ files = assets_test_files
+ content = files['content'] if files
+ content
+ }
+ end
+ end
\ No newline at end of file
diff --git a/app/views/exercises/Jsparson/exercise/simple/s10.html.erb b/app/views/exercises/Jsparson/exercise/simple/s10.html.erb
new file mode 100644
index 00000000..5c4eb68c
--- /dev/null
+++ b/app/views/exercises/Jsparson/exercise/simple/s10.html.erb
@@ -0,0 +1,294 @@
+
+
+
+ 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/app/views/exercises/Jsparson/exercise/simple/s8.html.erb b/app/views/exercises/Jsparson/exercise/simple/s8.html.erb
index 76c2ae88..b0e72390 100644
--- a/app/views/exercises/Jsparson/exercise/simple/s8.html.erb
+++ b/app/views/exercises/Jsparson/exercise/simple/s8.html.erb
@@ -144,17 +144,15 @@
});
function extractNodeValueToLabel(pemlContent) {
- const nodeValueToLabel = {};
- const regex = /tag:\s*(\w+)\s*display:\s*(['"])(.*?)\2/g;
- let match;
-
- while ((match = regex.exec(pemlContent)) !== null) {
- const tag = match[1];
- const display = match[3];
- nodeValueToLabel[tag] = display;
- }
-
- return nodeValueToLabel;
+ const nodeValueToLabel = {};
+ const regex = /tag:\s*(\w+)\s*\n\s*display:\s*(.+)/g;
+ let match;
+ while ((match = regex.exec(pemlContent)) !== null) {
+ const tag = match[1];
+ const display = match[2].trim();
+ nodeValueToLabel[tag] = display;
+ }
+ return nodeValueToLabel;
}
diff --git a/app/views/exercises/Jsparson/exercise/simple/s9.html.erb b/app/views/exercises/Jsparson/exercise/simple/s9.html.erb
new file mode 100644
index 00000000..e4eefb20
--- /dev/null
+++ b/app/views/exercises/Jsparson/exercise/simple/s9.html.erb
@@ -0,0 +1,198 @@
+
+
+
+ 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/db/migrate/20240413031046_create_parsons_prompts.rb b/db/migrate/20240413031046_create_parsons_prompts.rb
index a0562c94..1ab16bfb 100644
--- a/db/migrate/20240413031046_create_parsons_prompts.rb
+++ b/db/migrate/20240413031046_create_parsons_prompts.rb
@@ -1,10 +1,10 @@
-class CreateParsonsPrompts < ActiveRecord::Migration[6.1]
+class CreateParsonsPrompts < ActiveRecord::Migration
def change
create_table :parsons_prompts do |t|
t.text :title
t.text :instructions
t.string :exercise_id
- t.json :assets
+ t.text :assets
t.references :exercise_version, null: false, foreign_key: true
t.timestamps
end
diff --git a/db/schema.rb b/db/schema.rb
index 10f8cec1..ecc16b4b 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: 20240207035240) do
+ActiveRecord::Schema.define(version: 20240413031046) do
create_table "active_admin_comments", force: :cascade do |t|
t.string "namespace", limit: 255
@@ -47,7 +47,9 @@
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|
@@ -411,6 +413,18 @@
add_index "ownerships", ["exercise_version_id"], name: "index_ownerships_on_exercise_version_id", using: :btree
add_index "ownerships", ["filename"], name: "index_ownerships_on_filename", using: :btree
+ create_table "parsons_prompts", force: :cascade do |t|
+ t.text "title", limit: 65535
+ t.text "instructions", limit: 65535
+ t.string "exercise_id", limit: 255
+ t.text "assets", limit: 65535
+ t.integer "exercise_version_id", limit: 4, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "parsons_prompts", ["exercise_version_id"], name: "fk_rails_40d6ef5b4f", using: :btree
+
create_table "prompt_answers", force: :cascade do |t|
t.integer "attempt_id", limit: 4
t.integer "prompt_id", limit: 4
@@ -745,6 +759,7 @@
add_foreign_key "lms_instances", "lms_types", name: "lms_instances_lms_type_id_fk"
add_foreign_key "lti_workouts", "lms_instances"
add_foreign_key "ownerships", "exercise_versions"
+ add_foreign_key "parsons_prompts", "exercise_versions"
add_foreign_key "prompt_answers", "attempts", name: "prompt_answers_attempt_id_fk"
add_foreign_key "prompt_answers", "prompts", name: "prompt_answers_prompt_id_fk"
add_foreign_key "prompts", "irt_data", column: "irt_data_id", name: "prompts_irt_data_id_fk"
diff --git a/spec/factories/attempts.rb b/spec/factories/attempts.rb
index 8c5b912f..b696476c 100644
--- a/spec/factories/attempts.rb
+++ b/spec/factories/attempts.rb
@@ -20,10 +20,12 @@
#
# Indexes
#
-# 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)
+# 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)
#
# Foreign Keys
#