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

Commit f300922

Browse files
authored
Merge pull request #1394 from amy21206/master
New feature and bug fixes for hparsons directive
2 parents 72b461e + 42c6091 commit f300922

File tree

4 files changed

+251
-67
lines changed

4 files changed

+251
-67
lines changed

runestone/hparsons/hparsons.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,16 @@ class HParsonsDirective(RunestoneIdDirective):
102102
Here is the problem description. It must ends with the tildes.
103103
Make sure you use the correct delimitier for each section below.
104104
~~~~
105+
--hiddenprefix--
106+
// code that is for scaffolding the execution (e.g. initializing database)
105107
--blocks--
106108
block 1
107109
block 2
108110
block 3
111+
--hiddensuffix--
112+
// code that is for scaffolding unittest/execution (e.g. adding query for database)
113+
// most of the time the hiddensuffix is just "select * from table" to
114+
// get all entries from the table to test the update or other operations.
109115
--unittest--
110116
assert 1,1 == world
111117
assert 0,1 == hello

runestone/hparsons/js/SQLFeedback.js

Lines changed: 144 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,14 @@ export default class SQLFeedback extends HParsonsFeedback {
4545
// fnprefix sets the path to load the sql-wasm.wasm file
4646
var bookprefix;
4747
var fnprefix;
48-
if (eBookConfig.useRunestoneServices) {
48+
if (
49+
eBookConfig.useRunestoneServices ||
50+
window.location.search.includes("mode=browsing")
51+
) {
4952
bookprefix = `${eBookConfig.app}/books/published/${eBookConfig.basecourse}`;
5053
fnprefix = bookprefix + "/_static";
5154
} else {
55+
// The else clause handles the case where you are building for a static web browser
5256
bookprefix = "";
5357
fnprefix = "/_static";
5458
}
@@ -130,17 +134,115 @@ export default class SQLFeedback extends HParsonsFeedback {
130134
respDiv.parentElement.removeChild(respDiv);
131135
}
132136
$(this.output).text("");
137+
// creating new results div
138+
respDiv = document.createElement("div");
139+
respDiv.id = divid;
140+
this.outDiv.appendChild(respDiv);
141+
// show the output div
142+
$(this.outDiv).show();
143+
133144
// Run this query
134145
let query = await this.buildProg();
135146
if (!this.hparsons.db) {
136147
$(this.output).text(
137-
`Error: Database not initialized! DBURL: ${this.dburl}`
148+
`Error: Database not initialized! DBURL: ${this.hparsons.dburl}`
138149
);
139150
return;
140151
}
141152

142-
let it = this.hparsons.db.iterateStatements(query);
143-
this.results = [];
153+
// When having prefix/suffix, the visualization is consistent with "showlastsql" option of sql activecode:
154+
// only visualize last entry
155+
156+
let executionSuccessFlag = true;
157+
// executing hidden prefix if exist
158+
if (query.prefix) {
159+
this.prefixresults = this.executeIteratedStatements(this.hparsons.db.iterateStatements(query.prefix));
160+
if (this.prefixresults.at(-1).status == 'failure') {
161+
// if error occured in hidden prefix, log and stop executing the rest
162+
this.visualizeResults(respDiv, this.prefixresults, "Error executing hidden code in prefix");
163+
executionSuccessFlag = false;
164+
}
165+
}
166+
167+
// executing student input in micro Parsons
168+
if (executionSuccessFlag) {
169+
this.results = this.executeIteratedStatements(this.hparsons.db.iterateStatements(query.input));
170+
if (this.results.at(-1).status == 'failure') {
171+
// if error occured in student input, stop executing suffix/unitttest
172+
// and visualize the error
173+
this.visualizeResults(respDiv, this.results);
174+
executionSuccessFlag = false;
175+
} else if (!query.suffix) {
176+
this.visualizeResults(respDiv, this.results);
177+
}
178+
}
179+
180+
// executing hidden suffix if exist
181+
// In most cases the suffix is just "select * from x" to
182+
// get all data from the table to see if the operations the table is correct
183+
if (executionSuccessFlag && query.suffix) {
184+
this.suffixresults = this.executeIteratedStatements(this.hparsons.db.iterateStatements(query.suffix));
185+
if (this.suffixresults.at(-1).status == 'failure') {
186+
// if error occured in hidden suffix, visualize the results
187+
this.visualizeResults(respDiv, this.suffixresults, "Error executing hidden code in suffix");
188+
executionSuccessFlag = false;
189+
} else {
190+
this.visualizeResults(respDiv, this.suffixresults);
191+
}
192+
}
193+
194+
// Now handle autograding
195+
// autograding takes the results of the hidden suffix if exist
196+
// otherwise take the result of student input
197+
if (this.hparsons.unittest) {
198+
if (executionSuccessFlag) {
199+
if (this.suffixresults) {
200+
this.testResult = this.autograde(
201+
this.suffixresults[this.suffixresults.length - 1]
202+
);
203+
} else {
204+
this.testResult = this.autograde(
205+
this.results[this.results.length - 1]
206+
);
207+
}
208+
} else {
209+
// unit test results when execution failed
210+
this.passed = 0;
211+
this.failed = 0;
212+
this.percent = NaN;
213+
this.unit_results = `percent:${this.percent}:passed:${this.passed}:failed:${this.failed}`;
214+
// Do not show unittest results if execution failed
215+
$(this.output).css("visibility", "hidden");
216+
}
217+
} else {
218+
$(this.output).css("visibility", "hidden");
219+
}
220+
221+
return Promise.resolve("done");
222+
}
223+
224+
// Refactored from activecode-sql.
225+
// Takes iterated statements from db.iterateStatemnts(queryString)
226+
// Returns Array<result>:
227+
/* each result: {
228+
status: "success" or "faliure",
229+
// for SELECT statements (?):
230+
columns: number of columns,
231+
values: data,
232+
rowcount: number of rows in data,
233+
// for INSERT, UPDATE, DELETE:
234+
operation: "INSERT", "UPDATE", or "DELETE",
235+
rowcount: number of rows modified,
236+
// when error occurred (aside from status):
237+
message: error message,
238+
sql: remaining SQL (?)
239+
// when no queries were executed:
240+
message: "no queries submitted"
241+
}*/
242+
// If an error occurs it will stop executing the rest of queries in it.
243+
// Thus the error result will always be the last item.
244+
executeIteratedStatements(it) {
245+
let results = [];
144246
try {
145247
for (let statement of it) {
146248
let columns = statement.getColumnNames();
@@ -150,7 +252,7 @@ export default class SQLFeedback extends HParsonsFeedback {
150252
while (statement.step()) {
151253
data.push(statement.get());
152254
}
153-
this.results.push({
255+
results.push({
154256
status: "success",
155257
columns: columns,
156258
values: data,
@@ -169,44 +271,53 @@ export default class SQLFeedback extends HParsonsFeedback {
169271
prefix === "update" ||
170272
prefix === "delete"
171273
) {
172-
this.results.push({
274+
results.push({
173275
status: "success",
174276
operation: prefix,
175-
rowcount: this.db.getRowsModified(),
277+
rowcount: this.hparsons.db.getRowsModified(),
176278
});
177279
} else {
178-
this.results.push({ status: "success" });
280+
results.push({ status: "success" });
179281
}
180282
}
181283
}
182284
} catch (e) {
183-
this.results.push({
285+
results.push({
184286
status: "failure",
185287
message: e.toString(),
186288
sql: it.getRemainingSQL(),
187289
});
188290
}
189-
190-
if (this.results.length === 0) {
191-
this.results.push({
291+
if (results.length === 0) {
292+
results.push({
192293
status: "failure",
193294
message: "No queries submitted.",
194295
});
195296
}
297+
return results;
298+
}
196299

197-
respDiv = document.createElement("div");
198-
respDiv.id = divid;
199-
this.outDiv.appendChild(respDiv);
200-
$(this.outDiv).show();
201-
// Sometimes we don't want to show a bunch of intermediate results
202-
// like when we are including a bunch of previous statements from
203-
// other activecodes In that case the showlastsql flag can be set
204-
// so we only show the last result
205-
let resultArray = this.results;
300+
// output the results in the resultArray(Array<results>).
301+
// container: the container that contains the results
302+
// resultArray (Array<result>): see executeIteratedStatements
303+
// Each result will be in a separate row.
304+
// devNote will be displayed in the top row if exist;
305+
// it usually won't happen unless something is wrong with prefix and suffix.
306+
// ("error execution prefix/suffix")
307+
visualizeResults(container, resultArray, devNote) {
308+
if (devNote) {
309+
let section = document.createElement("div");
310+
section.setAttribute("class", "hp_sql_result");
311+
container.appendChild(section);
312+
let messageBox = document.createElement("pre");
313+
messageBox.textContent = devNote;
314+
messageBox.setAttribute("class", "hp_sql_result_failure");
315+
section.appendChild(messageBox);
316+
}
206317
for (let r of resultArray) {
207318
let section = document.createElement("div");
208319
section.setAttribute("class", "hp_sql_result");
209-
respDiv.appendChild(section);
320+
container.appendChild(section);
210321
if (r.status === "success") {
211322
if (r.columns) {
212323
let tableDiv = document.createElement("div");
@@ -245,26 +356,19 @@ export default class SQLFeedback extends HParsonsFeedback {
245356
section.appendChild(messageBox);
246357
}
247358
}
248-
249-
// Now handle autograding
250-
if (this.hparsons.suffix) {
251-
this.testResult = this.autograde(
252-
this.results[this.results.length - 1]
253-
);
254-
} else {
255-
$(this.output).css("visibility", "hidden");
256-
}
257-
258-
return Promise.resolve("done");
259359
}
260-
360+
261361
// adapted from activecode
262362
async buildProg() {
263363
// assemble code from prefix, suffix, and editor for running.
264-
// TODO: automatically joins the text array with space.
265-
// Should be joining without space when implementing regex.
266-
var prog;
267-
prog = this.hparsons.hparsonsInput.getParsonsTextArray().join(' ') + "\n";
364+
let prog = {};
365+
if (this.hparsons.hiddenPrefix) {
366+
prog.prefix = this.hparsons.hiddenPrefix;
367+
}
368+
prog.input = this.hparsons.hparsonsInput.getParsonsTextArray().join(' ') + "\n";
369+
if (this.hparsons.hiddenSuffix) {
370+
prog.suffix = this.hparsons.hiddenSuffix;
371+
}
268372
return Promise.resolve(prog);
269373
}
270374

@@ -273,7 +377,7 @@ export default class SQLFeedback extends HParsonsFeedback {
273377
if (this.unit_results) {
274378
let act = {
275379
scheme: "execution",
276-
correct: (this.failed === 0 && this.percent != null) ? "T" : "F",
380+
correct: (this.failed == 0 && this.passed != 0) ? "T" : "F",
277381
answer: this.hparsons.hparsonsInput.getParsonsTextArray(),
278382
percent: this.percent // percent is null if there is execution error
279383
}
@@ -291,7 +395,7 @@ export default class SQLFeedback extends HParsonsFeedback {
291395

292396
// might move to base class if used by multiple execution based feedback
293397
autograde(result_table) {
294-
var tests = this.hparsons.suffix.split(/\n/);
398+
var tests = this.hparsons.unittest.split(/\n/);
295399
this.passed = 0;
296400
this.failed = 0;
297401
// Tests should be of the form

runestone/hparsons/js/hparsons.js

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ if (hpList === undefined) hpList = {};
2020
export default class HParsons extends RunestoneBase {
2121
constructor(opts) {
2222
super(opts);
23-
// copied from activecode
24-
var suffStart;
2523
// getting settings
2624
var orig = $(opts.orig).find("textarea")[0];
2725
this.reuse = $(orig).data("reuse") ? true : false;
@@ -48,16 +46,7 @@ export default class HParsons extends RunestoneBase {
4846
this.loadButton = null;
4947
this.outerDiv = null;
5048
this.controlDiv = null;
51-
let prefixEnd = this.code.indexOf("^^^^");
52-
if (prefixEnd > -1) {
53-
this.prefix = this.code.substring(0, prefixEnd);
54-
this.code = this.code.substring(prefixEnd + 5);
55-
}
56-
suffStart = this.code.indexOf("--unittest--");
57-
if (suffStart > -1) {
58-
this.suffix = this.code.substring(suffStart + 5);
59-
this.code = this.code.substring(0, suffStart);
60-
}
49+
this.processContent(this.code)
6150

6251
// Change to factory when more execution based feedback is included
6352
if (this.isBlockGrading) {
@@ -85,33 +74,48 @@ export default class HParsons extends RunestoneBase {
8574
this.checkServer('hparsonsAnswer', true);
8675
}
8776

77+
processContent(code) {
78+
// todo: add errors when blocks are nonexistent (maybe in python)?
79+
this.hiddenPrefix = this.processSingleContent(code, '--hiddenprefix--');
80+
this.originalBlocks = this.processSingleContent(code, '--blocks--').split('\n').slice(1,-1);
81+
this.hiddenSuffix = this.processSingleContent(code, '--hiddensuffix--');
82+
this.unittest = this.processSingleContent(code, '--unittest--');
83+
}
84+
85+
processSingleContent(code, delimitier) {
86+
let index = code.indexOf(delimitier);
87+
if (index > -1) {
88+
let content = code.substring(index + delimitier.length);
89+
let endIndex = content.indexOf("\n--");
90+
content =
91+
endIndex > -1
92+
? content.substring(0, endIndex + 1)
93+
: content;
94+
return content;
95+
}
96+
return undefined;
97+
}
98+
8899
// copied from activecode, already modified to add parsons
89100
createEditor() {
90101
this.outerDiv = document.createElement("div");
91102
$(this.origElem).replaceWith(this.outerDiv);
92103
this.outerDiv.id = `${this.divid}-container`;
93104
this.outerDiv.addEventListener("micro-parsons", (ev) => {
94-
this.logHorizontalParsonsEvent(ev.detail);
95-
this.feedbackController.clearFeedback();
105+
const eventListRunestone = ['input', 'reset'];
106+
if (eventListRunestone.includes(ev.detail.type)) {
107+
// only log the events in the event list
108+
this.logHorizontalParsonsEvent(ev.detail);
109+
// when event is input or reset: clear previous feedback
110+
this.feedbackController.clearFeedback();
111+
}
96112
});
97-
let blocks = [];
98-
let blockIndex = this.code.indexOf("--blocks--");
99-
if (blockIndex > -1) {
100-
let blocksString = this.code.substring(blockIndex + 10);
101-
let endIndex = blocksString.indexOf("\n--");
102-
blocksString =
103-
endIndex > -1
104-
? blocksString.substring(0, endIndex)
105-
: blocksString;
106-
blocks = blocksString.split("\n");
107-
}
108-
this.originalBlocks = blocks.slice(1, -1);
109113
const props = {
110114
selector: `#${this.divid}-container`,
111115
id: `${this.divid}-hparsons`,
112116
reuse: this.reuse,
113117
randomize: this.randomize,
114-
parsonsBlocks: blocks.slice(1, -1),
118+
parsonsBlocks: [...this.originalBlocks],
115119
language: this.language
116120
}
117121
InitMicroParsons(props);

0 commit comments

Comments
 (0)