diff --git a/runestone/hparsons/hparsons.py b/runestone/hparsons/hparsons.py index 1971d288d..32661962f 100755 --- a/runestone/hparsons/hparsons.py +++ b/runestone/hparsons/hparsons.py @@ -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 diff --git a/runestone/hparsons/js/SQLFeedback.js b/runestone/hparsons/js/SQLFeedback.js index 55b5ca097..7ba6e37c7 100644 --- a/runestone/hparsons/js/SQLFeedback.js +++ b/runestone/hparsons/js/SQLFeedback.js @@ -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"; } @@ -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: + /* 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(); @@ -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, @@ -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). + // container: the container that contains the results + // resultArray (Array): 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"); @@ -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); } @@ -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 } @@ -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 diff --git a/runestone/hparsons/js/hparsons.js b/runestone/hparsons/js/hparsons.js index e242ef91b..7eb748c6c 100644 --- a/runestone/hparsons/js/hparsons.js +++ b/runestone/hparsons/js/hparsons.js @@ -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; @@ -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) { @@ -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); diff --git a/runestone/hparsons/test/_sources/index.rst b/runestone/hparsons/test/_sources/index.rst index cfa01fdeb..6ffa966cd 100644 --- a/runestone/hparsons/test/_sources/index.rst +++ b/runestone/hparsons/test/_sources/index.rst @@ -101,3 +101,73 @@ Reusable Block with Execution Based Feedback assert 1,1 == world assert 0,1 == hello assert 2,1 == 42 + + +Randomized Block with Execution Based Feedback and Hidden Code +--------------------------------------------------------------- +.. hparsons:: test_hparsons_sql_exe_hidden + :language: sql + :randomize: + + In the ``grades`` table: + + .. image:: https://i.ibb.co/r6qShy5/practice-grade.png + + A student completed an extra assignment and got some additional points. + + Please write an UPDATE statement to change the entry whose ``student_id`` is 1, and set their math score for ``final`` ``test_name`` to 90. + + hidden prefix initializes the table above; + hidden suffix is "SELECT * FROM grades". + + ~~~~ + --hiddenprefix-- + DROP TABLE IF EXISTS grades; + create table "grades" ("student_id" INTEGER, "test_name" TEXT, "english" INTEGER, "math" INTEGER); + INSERT INTO grades (student_id,test_name,english,math) VALUES + ('1', 'midterm', 62, 84), + ('1', 'final', 70, 86), + ('2', 'midterm', 50, 95), + ('2', 'final', 80, 99), + ('3', 'midterm', 55, 91); + --blocks-- + UPDATE grades + SET + math = 90 + WHERE + student_id = 1 AND test_name = "final" + LET + student_id = 1 AND test_name = final + --hiddensuffix-- + ;SELECT * FROM grades + --unittest-- + assert 1,1 == final + assert 1,3 == 90 + assert 3,3 == 99 + + +Randomized Block with Execution Based Feedback and Hidden Code + error in prefix +-------------------------------------------------------------------------------- +.. hparsons:: test_hparsons_sql_exe_hidden_error + :language: sql + :randomize: + + The third line of the hidden code is incorrect. + + ~~~~ + --hiddenprefix-- + DROP TABLE IF EXISTS grades; + create table "grades" ("student_id" INTEGER, "test_name" TEXT, "english" INTEGER, "math" INTEGER); + INSERT INTO grades (student_id,test_name,english,math) + --blocks-- + UPDATE grades + SET + math = 90 + WHERE + student_id = 1 AND test_name = "final" + LET + student_id = 1 AND test_name = final + --unittest-- + assert 1,1 == final + assert 1,3 == 90 + assert 3,3 == 99