Skip to content
This repository was archived by the owner on Jun 7, 2023. It is now read-only.

Commit b4eb207

Browse files
committed
✨ Add block-based feedback to horizontal parsons problems
1 parent 8991af6 commit b4eb207

5 files changed

Lines changed: 341 additions & 34 deletions

File tree

runestone/hparsons/hparsons.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def setup(app):
5252
%(textentry)s
5353
%(reuse)s
5454
%(randomize)s
55+
%(blockanswer)s
5556
style="visibility: hidden;">
5657
%(initialsetting)s
5758
</textarea>
@@ -100,13 +101,15 @@ class HParsonsDirective(RunestoneIdDirective):
100101
TODO: fix textentry
101102
:reuse: only for parsons -- make the blocks reusable
102103
:textentry: if you will use text entry instead of horizontal parsons
104+
:blockanswer: 0 1 2 3 # Provide answer for block-based feedback. Please note that the number of block start from 0. If not provided, will use execution based feedback.
103105
104106
Here is the problem description. It must ends with the tildes.
105107
Make sure you use the correct delimitier for each section below.
106108
~~~~
107109
--blocks--
108110
block 1
109111
block 2
112+
block 3
110113
--explanations--
111114
explanations for block 1
112115
explanations for block 2
@@ -127,6 +130,7 @@ class HParsonsDirective(RunestoneIdDirective):
127130
"textentry": directives.flag,
128131
"reuse": directives.flag,
129132
"randomize": directives.flag,
133+
"blockanswer": directives.unchanged,
130134
}
131135
)
132136

@@ -150,6 +154,11 @@ def run(self):
150154
else:
151155
self.options['randomize'] = ''
152156

157+
if "blockanswer" in self.options:
158+
self.options["blockanswer"] = "data-blockanswer='{}'".format(self.options["blockanswer"])
159+
else:
160+
self.options['blockanswer'] = ''
161+
153162
explain_text = None
154163
if self.content:
155164
if "~~~~" in self.content:
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// This file is adapted from lineGrader in parsons directive.
2+
// We could have fit our data structure to use the original LineBasedGrader directly,
3+
// but that would result in changes in parsons directive affecting this, so we created a copy
4+
// instead.
5+
6+
export default class BlockBasedGrader {
7+
constructor(problem) {
8+
this.problem = problem;
9+
}
10+
// Use a LIS (Longest Increasing Subsequence) algorithm to return the indexes
11+
// that are not part of that subsequence.
12+
inverseLISIndices(arr) {
13+
// Get all subsequences
14+
var allSubsequences = [];
15+
for (var i = 0; i < arr.length; i++) {
16+
var subsequenceForCurrent = [arr[i]],
17+
current = arr[i],
18+
lastElementAdded = -1;
19+
for (var j = i; j < arr.length; j++) {
20+
var subsequent = arr[j];
21+
if (subsequent > current && lastElementAdded < subsequent) {
22+
subsequenceForCurrent.push(subsequent);
23+
lastElementAdded = subsequent;
24+
}
25+
}
26+
allSubsequences.push(subsequenceForCurrent);
27+
}
28+
// Figure out the longest one
29+
var longestSubsequenceLength = -1;
30+
var longestSubsequence;
31+
for (let i in allSubsequences) {
32+
var subs = allSubsequences[i];
33+
if (subs.length > longestSubsequenceLength) {
34+
longestSubsequenceLength = subs.length;
35+
longestSubsequence = subs;
36+
}
37+
}
38+
// Create the inverse indexes
39+
var indexes = [];
40+
var lIndex = 0;
41+
for (let i = 0; i < arr.length; i++) {
42+
if (lIndex > longestSubsequence.length) {
43+
indexes.push(i);
44+
} else {
45+
if (arr[i] == longestSubsequence[lIndex]) {
46+
lIndex += 1;
47+
} else {
48+
indexes.push(i);
49+
}
50+
}
51+
}
52+
return indexes;
53+
}
54+
// grade that element, returning the state
55+
grade() {
56+
this.correctLines = 0;
57+
this.percentLines = 0;
58+
var solutionLines = this.solution;
59+
var answerLines = this.answer;
60+
var i;
61+
var state;
62+
this.percentLines =
63+
Math.min(answerLines.length, solutionLines.length) /
64+
Math.max(answerLines.length, solutionLines.length);
65+
if (answerLines.length < solutionLines.length) {
66+
state = "incorrectTooShort";
67+
this.correctLength = false;
68+
} else if (answerLines.length == solutionLines.length) {
69+
this.correctLength = true;
70+
} else {
71+
this.correctLength = false;
72+
}
73+
74+
// Determine whether the code **that is there** is in the correct order
75+
// If there is too much or too little code this only matters for
76+
// calculating a percentage score.
77+
let isCorrectOrder = true;
78+
this.correctLines = 0;
79+
this.solutionLength = solutionLines.length;
80+
let loopLimit = Math.min(solutionLines.length, answerLines.length);
81+
for (i = 0; i < loopLimit; i++) {
82+
if (answerLines[i] !== solutionLines[i]) {
83+
isCorrectOrder = false;
84+
} else {
85+
this.correctLines += 1;
86+
}
87+
}
88+
89+
if (
90+
isCorrectOrder &&
91+
this.correctLength
92+
) {
93+
// Perfect
94+
state = "correct";
95+
} else if (!isCorrectOrder && state != "incorrectTooShort") {
96+
state = "incorrectMoveBlocks";
97+
}
98+
this.calculatePercent();
99+
this.graderState = state;
100+
return state;
101+
}
102+
103+
calculatePercent() {
104+
let numLines = this.percentLines * 0.2;
105+
let lines = this.answer.length;
106+
let numCorrectBlocks = (this.correctLines / lines) * 0.8;
107+
108+
this.percent = numLines + numCorrectBlocks;
109+
}
110+
}

runestone/hparsons/js/horizontal-parsons.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15513,8 +15513,11 @@ class HParsonsElement extends HTMLElement {
1551315513
sheet.innerHTML += '.hparsons-input {padding: 15px;}\n';
1551415514
sheet.innerHTML += '.hparsons-tip { font-style: italic; }\n';
1551515515
sheet.innerHTML += '.parsons-block {display: inline-block; font-family: monospace; border-color:gray; margin: 0 1px; position: relative; border-radius: 10px; background-color: #efefef; border: 1px solid #d3d3d3; padding: 5px 10px; margin-top: 5px;}\n';
15516+
sheet.innerHTML += '.parsons-block.incorrectPosition {background-color: #ffbaba; border: 1px solid red;}\n';
1551615517
sheet.innerHTML += '.parsons-block:hover, .parsons-block:focus { border-color: black;}\n';
1551715518
sheet.innerHTML += '.drop-area { background-color: #ffa; padding: 0 5px; height: 42px; margin: 2px 0;}\n';
15519+
sheet.innerHTML += '.drop-area.incorrect { background-color: #f2dede; border-color: #f2b6b6}\n';
15520+
sheet.innerHTML += '.drop-area.correct { background-color: #dff0d8; border-color: #ade595}\n';
1551815521
// TODO:(UI) move the tooltip to the top of the line
1551915522
sheet.innerHTML += '.parsons-block .tooltip { visibility: hidden; width: 200px; background-color: black; color: #fff; text-align: center; padding: 5px 0; border-radius: 6px; position: absolute; z-index: 1; margin: 0 10px; bottom: 120%; margin-left: -100px;}\n';
1552015523
sheet.innerHTML += '.parsons-block .tooltip::after {content: " ";position: absolute; top: 100%;left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: black transparent transparent transparent;}\n';

0 commit comments

Comments
 (0)