Skip to content
This repository was archived by the owner on Jun 7, 2023. It is now read-only.
6 changes: 6 additions & 0 deletions runestone/hparsons/hparsons.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,16 @@ class HParsonsDirective(RunestoneIdDirective):
Here is the problem description. It must ends with the tildes.
Make sure you use the correct delimitier for each section below.
~~~~
--hiddenprefix--
// code that is for scaffolding the execution (e.g. initializing database)
--blocks--
block 1
block 2
block 3
--hiddensuffix--
// code that is for scaffolding unittest/execution (e.g. adding query for database)
// most of the time the hiddensuffix is just "select * from table" to
// get all entries from the table to test the update or other operations.
--unittest--
assert 1,1 == world
assert 0,1 == hello
Expand Down
184 changes: 144 additions & 40 deletions runestone/hparsons/js/SQLFeedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,14 @@ export default class SQLFeedback extends HParsonsFeedback {
// fnprefix sets the path to load the sql-wasm.wasm file
var bookprefix;
var fnprefix;
if (eBookConfig.useRunestoneServices) {
if (
eBookConfig.useRunestoneServices ||
window.location.search.includes("mode=browsing")
) {
bookprefix = `${eBookConfig.app}/books/published/${eBookConfig.basecourse}`;
fnprefix = bookprefix + "/_static";
} else {
// The else clause handles the case where you are building for a static web browser
bookprefix = "";
fnprefix = "/_static";
}
Expand Down Expand Up @@ -130,17 +134,115 @@ export default class SQLFeedback extends HParsonsFeedback {
respDiv.parentElement.removeChild(respDiv);
}
$(this.output).text("");
// creating new results div
respDiv = document.createElement("div");
respDiv.id = divid;
this.outDiv.appendChild(respDiv);
// show the output div
$(this.outDiv).show();

// Run this query
let query = await this.buildProg();
if (!this.hparsons.db) {
$(this.output).text(
`Error: Database not initialized! DBURL: ${this.dburl}`
`Error: Database not initialized! DBURL: ${this.hparsons.dburl}`
);
return;
}

let it = this.hparsons.db.iterateStatements(query);
this.results = [];
// When having prefix/suffix, the visualization is consistent with "showlastsql" option of sql activecode:
// only visualize last entry

let executionSuccessFlag = true;
// executing hidden prefix if exist
if (query.prefix) {
this.prefixresults = this.executeIteratedStatements(this.hparsons.db.iterateStatements(query.prefix));
if (this.prefixresults.at(-1).status == 'failure') {
// if error occured in hidden prefix, log and stop executing the rest
this.visualizeResults(respDiv, this.prefixresults, "Error executing hidden code in prefix");
executionSuccessFlag = false;
}
}

// executing student input in micro Parsons
if (executionSuccessFlag) {
this.results = this.executeIteratedStatements(this.hparsons.db.iterateStatements(query.input));
if (this.results.at(-1).status == 'failure') {
// if error occured in student input, stop executing suffix/unitttest
// and visualize the error
this.visualizeResults(respDiv, this.results);
executionSuccessFlag = false;
} else if (!query.suffix) {
this.visualizeResults(respDiv, this.results);
}
}

// executing hidden suffix if exist
// In most cases the suffix is just "select * from x" to
// get all data from the table to see if the operations the table is correct
if (executionSuccessFlag && query.suffix) {
this.suffixresults = this.executeIteratedStatements(this.hparsons.db.iterateStatements(query.suffix));
if (this.suffixresults.at(-1).status == 'failure') {
// if error occured in hidden suffix, visualize the results
this.visualizeResults(respDiv, this.suffixresults, "Error executing hidden code in suffix");
executionSuccessFlag = false;
} else {
this.visualizeResults(respDiv, this.suffixresults);
}
}

// Now handle autograding
// autograding takes the results of the hidden suffix if exist
// otherwise take the result of student input
if (this.hparsons.unittest) {
if (executionSuccessFlag) {
if (this.suffixresults) {
this.testResult = this.autograde(
this.suffixresults[this.suffixresults.length - 1]
);
} else {
this.testResult = this.autograde(
this.results[this.results.length - 1]
);
}
} else {
// unit test results when execution failed
this.passed = 0;
this.failed = 0;
this.percent = NaN;
this.unit_results = `percent:${this.percent}:passed:${this.passed}:failed:${this.failed}`;
// Do not show unittest results if execution failed
$(this.output).css("visibility", "hidden");
}
} else {
$(this.output).css("visibility", "hidden");
}

return Promise.resolve("done");
}

// Refactored from activecode-sql.
// Takes iterated statements from db.iterateStatemnts(queryString)
// Returns Array<result>:
/* each result: {
status: "success" or "faliure",
// for SELECT statements (?):
columns: number of columns,
values: data,
rowcount: number of rows in data,
// for INSERT, UPDATE, DELETE:
operation: "INSERT", "UPDATE", or "DELETE",
rowcount: number of rows modified,
// when error occurred (aside from status):
message: error message,
sql: remaining SQL (?)
// when no queries were executed:
message: "no queries submitted"
}*/
// If an error occurs it will stop executing the rest of queries in it.
// Thus the error result will always be the last item.
executeIteratedStatements(it) {
let results = [];
try {
for (let statement of it) {
let columns = statement.getColumnNames();
Expand All @@ -150,7 +252,7 @@ export default class SQLFeedback extends HParsonsFeedback {
while (statement.step()) {
data.push(statement.get());
}
this.results.push({
results.push({
status: "success",
columns: columns,
values: data,
Expand All @@ -169,44 +271,53 @@ export default class SQLFeedback extends HParsonsFeedback {
prefix === "update" ||
prefix === "delete"
) {
this.results.push({
results.push({
status: "success",
operation: prefix,
rowcount: this.db.getRowsModified(),
rowcount: this.hparsons.db.getRowsModified(),
});
} else {
this.results.push({ status: "success" });
results.push({ status: "success" });
}
}
}
} catch (e) {
this.results.push({
results.push({
status: "failure",
message: e.toString(),
sql: it.getRemainingSQL(),
});
}

if (this.results.length === 0) {
this.results.push({
if (results.length === 0) {
results.push({
status: "failure",
message: "No queries submitted.",
});
}
return results;
}

respDiv = document.createElement("div");
respDiv.id = divid;
this.outDiv.appendChild(respDiv);
$(this.outDiv).show();
// Sometimes we don't want to show a bunch of intermediate results
// like when we are including a bunch of previous statements from
// other activecodes In that case the showlastsql flag can be set
// so we only show the last result
let resultArray = this.results;
// output the results in the resultArray(Array<results>).
// container: the container that contains the results
// resultArray (Array<result>): see executeIteratedStatements
// Each result will be in a separate row.
// devNote will be displayed in the top row if exist;
// it usually won't happen unless something is wrong with prefix and suffix.
// ("error execution prefix/suffix")
visualizeResults(container, resultArray, devNote) {
if (devNote) {
let section = document.createElement("div");
section.setAttribute("class", "hp_sql_result");
container.appendChild(section);
let messageBox = document.createElement("pre");
messageBox.textContent = devNote;
messageBox.setAttribute("class", "hp_sql_result_failure");
section.appendChild(messageBox);
}
for (let r of resultArray) {
let section = document.createElement("div");
section.setAttribute("class", "hp_sql_result");
respDiv.appendChild(section);
container.appendChild(section);
if (r.status === "success") {
if (r.columns) {
let tableDiv = document.createElement("div");
Expand Down Expand Up @@ -245,26 +356,19 @@ export default class SQLFeedback extends HParsonsFeedback {
section.appendChild(messageBox);
}
}

// Now handle autograding
if (this.hparsons.suffix) {
this.testResult = this.autograde(
this.results[this.results.length - 1]
);
} else {
$(this.output).css("visibility", "hidden");
}

return Promise.resolve("done");
}

// adapted from activecode
async buildProg() {
// assemble code from prefix, suffix, and editor for running.
// TODO: automatically joins the text array with space.
// Should be joining without space when implementing regex.
var prog;
prog = this.hparsons.hparsonsInput.getParsonsTextArray().join(' ') + "\n";
let prog = {};
if (this.hparsons.hiddenPrefix) {
prog.prefix = this.hparsons.hiddenPrefix;
}
prog.input = this.hparsons.hparsonsInput.getParsonsTextArray().join(' ') + "\n";
if (this.hparsons.hiddenSuffix) {
prog.suffix = this.hparsons.hiddenSuffix;
}
return Promise.resolve(prog);
}

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

// might move to base class if used by multiple execution based feedback
autograde(result_table) {
var tests = this.hparsons.suffix.split(/\n/);
var tests = this.hparsons.unittest.split(/\n/);
this.passed = 0;
this.failed = 0;
// Tests should be of the form
Expand Down
58 changes: 31 additions & 27 deletions runestone/hparsons/js/hparsons.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ if (hpList === undefined) hpList = {};
export default class HParsons extends RunestoneBase {
constructor(opts) {
super(opts);
// copied from activecode
var suffStart;
// getting settings
var orig = $(opts.orig).find("textarea")[0];
this.reuse = $(orig).data("reuse") ? true : false;
Expand All @@ -48,16 +46,7 @@ export default class HParsons extends RunestoneBase {
this.loadButton = null;
this.outerDiv = null;
this.controlDiv = null;
let prefixEnd = this.code.indexOf("^^^^");
if (prefixEnd > -1) {
this.prefix = this.code.substring(0, prefixEnd);
this.code = this.code.substring(prefixEnd + 5);
}
suffStart = this.code.indexOf("--unittest--");
if (suffStart > -1) {
this.suffix = this.code.substring(suffStart + 5);
this.code = this.code.substring(0, suffStart);
}
this.processContent(this.code)

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

processContent(code) {
// todo: add errors when blocks are nonexistent (maybe in python)?
this.hiddenPrefix = this.processSingleContent(code, '--hiddenprefix--');
this.originalBlocks = this.processSingleContent(code, '--blocks--').split('\n').slice(1,-1);
this.hiddenSuffix = this.processSingleContent(code, '--hiddensuffix--');
this.unittest = this.processSingleContent(code, '--unittest--');
}

processSingleContent(code, delimitier) {
let index = code.indexOf(delimitier);
if (index > -1) {
let content = code.substring(index + delimitier.length);
let endIndex = content.indexOf("\n--");
content =
endIndex > -1
? content.substring(0, endIndex + 1)
: content;
return content;
}
return undefined;
}

// copied from activecode, already modified to add parsons
createEditor() {
this.outerDiv = document.createElement("div");
$(this.origElem).replaceWith(this.outerDiv);
this.outerDiv.id = `${this.divid}-container`;
this.outerDiv.addEventListener("micro-parsons", (ev) => {
this.logHorizontalParsonsEvent(ev.detail);
this.feedbackController.clearFeedback();
const eventListRunestone = ['input', 'reset'];
if (eventListRunestone.includes(ev.detail.type)) {
// only log the events in the event list
this.logHorizontalParsonsEvent(ev.detail);
// when event is input or reset: clear previous feedback
this.feedbackController.clearFeedback();
}
});
let blocks = [];
let blockIndex = this.code.indexOf("--blocks--");
if (blockIndex > -1) {
let blocksString = this.code.substring(blockIndex + 10);
let endIndex = blocksString.indexOf("\n--");
blocksString =
endIndex > -1
? blocksString.substring(0, endIndex)
: blocksString;
blocks = blocksString.split("\n");
}
this.originalBlocks = blocks.slice(1, -1);
const props = {
selector: `#${this.divid}-container`,
id: `${this.divid}-hparsons`,
reuse: this.reuse,
randomize: this.randomize,
parsonsBlocks: blocks.slice(1, -1),
parsonsBlocks: [...this.originalBlocks],
language: this.language
}
InitMicroParsons(props);
Expand Down
Loading