From e3e71158268595805d93c20ad9705df0b2f00c98 Mon Sep 17 00:00:00 2001 From: Jochen Rick Date: Mon, 6 Jun 2016 14:30:11 -0400 Subject: [PATCH 1/8] better Parsons view --- runestone/parsons/css/parsons.css | 147 +- runestone/parsons/js/parsons.js | 2428 ++++++++++--------------- runestone/parsons/js/parsons_setup.js | 117 +- runestone/parsons/parsons.py | 25 +- 4 files changed, 1055 insertions(+), 1662 deletions(-) diff --git a/runestone/parsons/css/parsons.css b/runestone/parsons/css/parsons.css index 837dbd67f..afe61412b 100644 --- a/runestone/parsons/css/parsons.css +++ b/runestone/parsons/css/parsons.css @@ -1,117 +1,122 @@ -/** Stylesheet for the puzzles */ - .sortable-code { position: static; padding-left: 0px; - margin-left: 2%; - display:inline-block; + margin: 0px 15px; + display: inline-block; text-align: left; vertical-align: top; } -#sortableTrash { width: 38%; } -#sortable { width: 56%; } -.sortable-code ul { - font-size: 120%; +.source, .answer, .answer1, .answer2, .answer3, .answer4 { + position: relative; + font-size: 100%; font-family: monospace; list-style: none; background-color: #efefff; padding-bottom: 10px; padding-left: 0; margin-left: 0; - border: 1px solid #efefff;; -} -.sortable-code ul:empty { - padding-bottom: 30px; -} -.sortable-code li, .sortable-code li:before, .sortable-code li:after { - box-sizing: content-box; -} -ul.output { - background-color: #FFA; -} -.sortable-code li { + border: 1px solid #efefff; +} +.source { + background-color: #efefff; +} +.answer { + background-color: #ffa; +} +.answer1 { + background: linear-gradient(#ff7, #ff7) no-repeat border-box; + background-size: 30px 100%; + background-position: 0 0; + background-origin: padding-box; + background-color: #ffa; +} +.answer2 { + background: linear-gradient(#ff7, #ff7) no-repeat border-box; + background-size: 30px 100%; + background-position: 30px 0; + background-origin: padding-box; + background-color: #ffa; +} +.answer3 { + background: linear-gradient(#ff7, #ff7) no-repeat border-box, linear-gradient(#ff7, #ff7) no-repeat border-box; + background-size: 30px 100%, 30px 100%; + background-position: 0 0, 60px 0; + background-origin: padding-box, padding-box; + background-color: #ffa; +} +.answer4 { + background: linear-gradient(#ff7, #ff7) no-repeat border-box, linear-gradient(#ff7, #ff7) no-repeat border-box; + background-size: 30px 100%, 30px 100%; + background-position: 30px 0, 90px 0; + background-origin: padding-box, padding-box; + background-color: #ffa; +} +.block { + position: absolute; -moz-border-radius:10px; -webkit-border-radius:10px; border-radius: 10px; - background-color:#EFEFEF; - border:1px solid lightgray; + background-color: #EFEFEF; + border: 1px solid lightgray; padding:10px; margin-top: 5px; white-space: nowrap; overflow: hidden; cursor: move; } -.sortable-code li:hover { - overflow: visible; +.block code { + display: block; + clear: both; + float: left; + background-color: transparent; } - -ul.incorrect { +.block, .block:before, .block:after { + box-sizing: content-box; +} +code.indent1 { + margin-left: 30px; +} +code.indent2 { + margin-left: 60px; +} +code.indent3 { + margin-left: 90px; +} +code.indent4 { + margin-left: 120px; +} +.incorrect { border: 1px solid red; + background: none; background-color: #ffefef; } -ul.correct { +.correct { + background: none; background-color: #efffef; background-color: #DFF2BF; } -li.incorrectIndent { +.incorrectIndent { border: 1px solid red; border-left: 10px solid red; + padding-left: 1px; } -li.correctIndent { +.correctIndent { border: 1px solid green; border-left: 10px solid green; } -li.incorrectPosition, .testcase.fail, .testcase.error { +.incorrectPosition { background-color: #FFBABA; border:1px solid red; } -li.correctPosition, .testcase.pass { +.correctPosition { background-color: #DFF2BF; border:1px solid green; } - -.testcase { padding: 10px; margin-bottom: 10px;} -.testcase .msg { font-weight: bold; } -.testcase .error { color: red;} -.testcase .feedback { font-weight: bolder;} -.testcase .fail .expected, .testcase .fail .actual { - color: red; - font-weight: bolder; -} -.testcase .output { - display: block; - white-space: pre; - background-color: #555555; - color: white; - font-size: 12px; - line-height: 15px; - margin: 5px; - padding: 5px; -} -/** For styling the toggleable elements */ -.jsparson-toggle { - padding: 0 15px; - display: inline-block; - border: 1px dashed black; - z-index: 500; - cursor: pointer; - min-width: 10px; - min-height: 15px; -} -.jsparson-toggle:empty { - border-color: red; -} -.jsparson-toggle:empty:before { - content: "??"; - display: block; - color: red; -} - .parsons{ /*background-color: #fbfcfd; border-color: #e6e6e6; @@ -135,7 +140,3 @@ li.correctPosition, .testcase.pass { margin-right: auto; text-align:center; } - -.parsons-disabled { - pointer-events: none; -} diff --git a/runestone/parsons/js/parsons.js b/runestone/parsons/js/parsons.js index b0acb035b..e455f8a22 100644 --- a/runestone/parsons/js/parsons.js +++ b/runestone/parsons/js/parsons.js @@ -1,939 +1,322 @@ -(function($, _) { // wrap in anonymous function to not show some helper variables - - // regexp used for trimming - var trimRegexp = /^\s*(.*?)\s*$/; - var translations = { - fi: { - trash_label: 'Raahaa rivit ohjelmaasi tästä', - solution_label: 'Muodosta ratkaisusi tähän', - order: function() { - return "Ohjelma sisältää vääriä palasia tai palasten järjestys on väärä. Tämä on mahdollista korjata siirtämällä, poistamalla tai vaihtamalla korostettuja palasia.";}, - lines_missing: function() { - return "Ohjelmassasi on liian vähän palasia, jotta se toimisi oikein.";}, - lines_too_many: function() { - return "Ohjelmassasi on liian monta palasta, jotta se toimisi oikein.";}, - no_matching: function(lineNro) { - return "Korostettu palanen (" + lineNro + ") on sisennetty kieliopin vastaisesti."; }, - no_matching_open: function(lineNro, block) { - return "Rivillä " + lineNro + " päätettävää " + block + - " lohkoa ei ole aloitettu."; }, - no_matching_close: function(lineNro, block) { - return block + " lohkoa riviltä " + lineNro + " ei ole päätetty."; }, - block_close_mismatch: function(closeLine, closeBlock, openLine, inBlock) { - return "Ei voi päättää lohkoa " + closeBlock + " rivillä " + closeLine + - " oltaessa vielä lohkossa " + inBlock + " riviltä " + openLine; }, - block_structure: function(lineNro) { - return "Korostettu palanen (" + lineNro + ") on sisennetty väärään koodilohkoon."; }, - unittest_error: function(errormsg) { - return "Virhe ohjelman jäsentämisessä/suorituksessa
" + errormsg + ""; - }, - unittest_output_assertion: function(expected, actual) { - return "Odotettu tulostus: " + expected + "" + - "Ohjelmasi tulostus: " + actual + ""; - }, - unittest_assertion: function(expected, actual) { - return "Odotettu arvo: " + expected + "
" + - "Ohjelmasi antama arvo: " + actual + ""; - }, - variabletest_assertion: function(varname, expected, actual) { - return "Muuttujan " + varname + " odotettu arvo: " + expected + " " + - "Ohjelmasi antama arvo: " + actual + ""; - } - }, - en: { - trash_label: 'Drag from here', - solution_label: 'Construct your solution here', - order: function() { - return "Code fragments in your program are wrong, or in wrong order. This can be fixed by moving, removing, or replacing highlighted fragments.";}, - lines_missing: function() { - return "Your program has too few code fragments.";}, - lines_too_many: function() { - return "Your program has too many code fragments.";}, - no_matching: function(lineNro) { - return "Based on language syntax, the highlighted fragment (" + lineNro + ") is not correctly indented."; }, - no_matching_open: function(lineNro, block) { - return "The " + block + " ended on line " + lineNro + " never started."; }, - no_matching_close: function(lineNro, block) { - return "Block " + block + " defined on line " + lineNro + " not ended properly"; - }, - block_close_mismatch: function(closeLine, closeBlock, openLine, inBlock) { - return "Cannot end block " + closeBlock + " on line " + closeLine + " when still inside block " + inBlock + " started on line " + openLine; - }, - block_structure: function(lineNro) { return "The highlighted fragment " + lineNro + " belongs to a wrong block (i.e. indentation)."; }, - unittest_error: function(errormsg) { - return "Error in parsing/executing your program
" + errormsg + ""; - }, - unittest_output_assertion: function(expected, actual) { - return "Expected output: " + expected + "" + - "Output of your program: " + actual + ""; - }, - unittest_assertion: function(expected, actual) { - return "Expected value: " + expected + "
" + - "Actual value: " + actual + ""; - }, - variabletest_assertion: function(varname, expected, actual) { - return "Expected value of variable " + varname + ": " + expected + "
" + - "Actual value: " + actual + ""; - } - } - }; - - // Different graders - - var graders = {}; - // Grader that will execute the code and check variable values after that - // Expected and supported options: - // - vartests (required): array of variable test objects - // Each variable test object can/must have the following properties: - // - initcode: code that will be prepended before the learner solution code - // - code: code that will be appended after the learner solution code - // - message (required): a textual description of the test, shown to learner - // Properties specifying what is tested: - // - variables: an object with properties for each variable name to - // be tested; the value of the property is the expected - // value - // or - // - variable: a variable name to be tested - // - expected: expected value of the variable after code execution - var VariableCheckGrader = function(parson) { - this.parson = parson; - }; - graders.VariableCheckGrader = VariableCheckGrader; - // Executes the given Python code and returns an object with two properties: - // mainmod: the result of Skulpt importMainWithBody call with the given code - // output: the output of the program - // Note, that the Skulpt execution can throw an exception, which will not be handled - // by this function, so the caller should take care of that. - VariableCheckGrader.prototype._python_exec = function(code) { - var output = ""; - // function for reading python imports with skulpt - function builtinRead(x) { - if (Sk.builtinFiles === undefined || Sk.builtinFiles["files"][x] === undefined) - throw "File not found: '" + x + "'"; - return Sk.builtinFiles["files"][x]; - } - // configure Skulpt - Sk.execLimit = this.parson.options.exec_limit || 2500; // time limit for the code to run - Sk.configure({ - output: function(str) { output += str; }, - python3: this.parson.options.python3 || false, - read: builtinRead - }); - return {mainmod: Sk.importMainWithBody("", false, code), output: output}; - }; - // Executes the given code using Skulpt and returns an object with variable - // values of the variables given in the variables array. - // Possible errors will be in the _error property of the returned object. - // Output of the code will be in _output property of the result. - // Example: this._variablesAfterExecution("x=0\ny=2\nprint x", ["x", "y"]) - // will return object {"x": 0, "y": 2, "_output": "0"} - VariableCheckGrader.prototype._variablesAfterExecution = function(code, variables) { - var output = "", - execResult, mainmod, - result = {'variables': {}}, - varname; - try { - execResult = this._python_exec(code); - } catch (e) { - return {"_output": output, "_error": "" + e}; - } - mainmod = execResult.mainmod; - for (var i = 0; i < variables.length; i++) { - varname = variables[i]; - result.variables[varname] = mainmod.tp$getattr(varname); - } - result._output = execResult.output; - return result; - }; - // Formats a JavaScript variable to the corresponding Python value *and* - // formats a Skulpt variable to the corresponding Python value - VariableCheckGrader.prototype.formatVariableValue = function(varValue) { - var varType = typeof varValue; - if (varType === "undefined" || varValue === null) { - return "None"; - } else if (varType === "string") { // show strings in quotes - return '"' + varValue + '"'; - } else if (varType === "boolean") { // Python booleans with capital first letter - return varValue?"True":"False"; - } else if ($.isArray(varValue)) { // JavaScript arrays - return '[' + varValue.join(', ') + ']'; - } else if (varType === "object" && varValue.tp$name === "number") { // Python numbers - return varValue.v; - } else if (varType === "object" && varValue.tp$name === "NoneType") { // None - return "None"; - } else if (varType === "object" && varValue.tp$name === "bool") { // Python strings - return varValue.v?"True":"False"; - } else if (varType === "object" && varValue.tp$name === "str") { // Python strings - return '"' + varValue.v + '"'; - } else if (varType === "object" && varValue.tp$name === "list") { // Python lists - return '[' + varValue.v.join(', ') + ']'; - } else { - return varValue; - } - }; - // Fix or strip line numbers in the (error) message - // Basically removes the number of lines in prependCode from the line number shown. - VariableCheckGrader.prototype.stripLinenumberIfNeeded = function(msg, prependCode, studentCode) { - var lineNbrRegexp = /.*on line ([0-9]+).*/; - // function that fixes the line numbers in student feedback - var match = msg.match(lineNbrRegexp); - if (match) { - var lineNo = parseInt(match[1], 10), - lowerLimit = prependCode? - prependCode.split('\n').length - :0, - upperLimit = lowerLimit + studentCode.split('\n').length - 1; - // if error in prepended code or tests, remove the line number - if (lineNo <= lowerLimit || lineNo > upperLimit) { - return msg.replace(' on line ' + lineNo, ''); - } else if (lowerLimit > 0) { - // if error in student code, make sure the line number matches student lines - return msg.replace(' on line ' + lineNo, ' on line ' + (lineNo - lowerLimit)); - } - } - return msg; - }; - //Return executable code in one string - VariableCheckGrader.prototype._codelinesAsString = function() { - var student_code = this.parson.getModifiedCode("#ul-" + this.parson.options.sortableId); - var executableCode = ""; - $.each(student_code, function(index, item) { - // split codeblocks on br elements - var lines = $("#" + item.id).html().split(//); - // go through all the lines - for (var i = 0; i < lines.length; i++) { - // add indents and get the text for the line (to remove the syntax highlight html elements) - executableCode += python_indents[item.indent] + $("" + lines[i] + "").text() + "\n"; - } - }); - return executableCode; - }; - VariableCheckGrader.prototype.grade = function(studentcode) { - var parson = this.parson, - that = this, - feedback = "", - log_errors = [], - all_passed = true; - $.each(parson.options.vartests, function(index, testdata) { - var student_code = studentcode || that._codelinesAsString(); - var executableCode = (testdata.initcode || "") + "\n" + student_code + "\n" + (testdata.code || ""); - var variables, expectedVals; - - if ('variables' in testdata) { - variables = _.keys(testdata.variables); - expectedVals = testdata.variables; - } else { - variables = [testdata.variable]; - expectedVals = {}; - expectedVals[testdata.variable] = testdata.expected; - } - var res = that._variablesAfterExecution(executableCode, variables); - var testcaseFeedback = "", - success = true, - log_entry = {'code': testdata.code, 'msg': testdata.message}, - expected_value, - actual_value; - if ("_error" in res) { - testcaseFeedback += parson.translations.unittest_error(that.stripLinenumberIfNeeded(res._error, - testdata.initcode, - student_code)); - success = false; - log_entry.type = "error"; - log_entry.errormsg = res._error; - } else { - log_entry.type = "assertion"; - log_entry.variables = {}; - for (var j = 0; j < variables.length; j++) { - var variable = variables[j], - variableSuccess; - if (variable === "__output") { // checking output of the program - expected_value = expectedVals[variable]; - actual_value = res._output; - variableSuccess = (actual_value == expected_value); // should we do a strict test?? - testcaseFeedback += "
"; - testcaseFeedback += parson.translations.unittest_output_assertion(expected_value, actual_value) + - "
"; - } else { - expected_value = that.formatVariableValue(expectedVals[variable]); - actual_value = that.formatVariableValue(res.variables[variable]); - variableSuccess = (actual_value == expected_value); // should we do a strict test?? - testcaseFeedback += "
"; - testcaseFeedback += parson.translations.variabletest_assertion(variable, expected_value, actual_value) + - "
"; - } - log_entry.variables[variable] = {expected: expected_value, actual: actual_value}; - if (!variableSuccess) { - success = false; - } - } - } - all_passed = all_passed && success; - log_entry.success = success; - log_errors.push(log_entry); - feedback += "
" + testdata.message + "
" + - testcaseFeedback + "
"; - }); - return { html: feedback, tests: log_errors, success: all_passed }; - }; - - // A grader to be used for exercises which draw turtle graphics. - // Required options: - // - turtleModelCode: The code constructing the model drawing. The turtle is initialized - // to modelTurtle variable, so your code should use that variable. - // - // Options that can be specified (that is, optional): - // - turtlePenDown: a boolean specifying whether or not the pen should be put down - // initially for the student constructed code - // - turtleModelCanvas: ID of the canvas DOM element where the model solution will be drawn. - // Defaults to modelCanvas. - // - turtleStudentCanvas: ID of the canvas DOM element where student turtle will draw. - // Defaults to studentCanvas. - // - // Grading is based on comparing the commands executed by the model and student turtle. - // If the executable_code option is also specified, the code on each line of that option will - // be executed instead of the code in the student constructed lines. Note, that the student - // code should use the variable myTurtle for commands to control the turtle in order for the - // grading to work. - var TurtleGrader = function(p) { - this.parson = p; - // execute the model solution turtlet path to have the target "picture" visible in the - // beginning - var modelCommands = this._executeTurtleModel(); - - // specify variable tests for the commands executed by the student turtlet and the model - var penDown = typeof p.options.turtlePenDown === "boolean"?p.options.turtlePenDown:true; - var vartests = [ - {initcode: "import parsonturtle\nmyTurtle = parsonturtle.ParsonTurtle()\n" + - "myTurtle.speed(0.3)\nmyTurtle.pensize(3, False)\n" + - (penDown ? "" : "myTurtle.up()\n"), // set the state of the pen - code: (p.options.turtleTestCode?p.options.turtleTestCode:"") + "\ncommands = myTurtle.commands()", - message: "", variables: {commands: modelCommands}} - ]; - // set the vartests in the parson options - p.options.vartests = vartests; - }; - // expose the grader to ParsonsWidget._graders - graders.TurtleGrader = TurtleGrader; - // copy the python execution functions from VariableCheckGrader - TurtleGrader.prototype._python_exec = VariableCheckGrader.prototype._python_exec; - TurtleGrader.prototype._variablesAfterExecution = VariableCheckGrader.prototype._variablesAfterExecution; - // Execute the model turtlet code - TurtleGrader.prototype._executeTurtleModel = function() { - var code = "import parsonturtle\nmodelTurtle = parsonturtle.ParsonTurtle()\n" + - "modelTurtle.color(160, 160, 160, False)\n" + - this.parson.options.turtleModelCode + - "\ncommands = modelTurtle.commands()\n"; - Sk.canvas = this.parson.options.turtleModelCanvas || "modelCanvas"; - var result = this._variablesAfterExecution(code, ["commands"]); - if (!result.variables || !result.variables.commands || !result.variables.commands.v) { - return "None"; - } - return result.variables.commands.v; - }; - // grade the student solution - TurtleGrader.prototype.grade = function() { - // set the correct canvas where the turtle should draw - Sk.canvas = this.parson.options.turtleStudentCanvas || "studentCanvas"; - // Pass the grading on to either the LangTranslationGrader or VariableChecker - if (this.parson.options.executable_code) { - return new LanguageTranslationGrader(this.parson).grade(); - } else { - return new VariableCheckGrader(this.parson).grade(); - } - }; - - // Grader that will execute student code and Skulpt unittests - var UnitTestGrader = function(parson) { - this.parson = parson; - }; - graders.UnitTestGrader = UnitTestGrader; - // copy the line number fixer and code-construction from VariableCheckGrader - UnitTestGrader.prototype.stripLinenumberIfNeeded = VariableCheckGrader.prototype.stripLinenumberIfNeeded; - UnitTestGrader.prototype._codelinesAsString = VariableCheckGrader.prototype._codelinesAsString; - // copy the python executor from VariableCheckGrager - UnitTestGrader.prototype._python_exec = VariableCheckGrader.prototype._python_exec; - // do the grading - UnitTestGrader.prototype.grade = function(studentcode) { - var success = true, - parson = this.parson, - unittests = parson.options.unittests, - studentCode = studentcode || this._codelinesAsString(), - feedbackHtml = "", // HTML to be returned as feedback - result, mainmod; - - var executableCode = studentCode + "\n" + unittests; - - // if there is code to add before student code, add it - if (parson.options.unittest_code_prepend) { - executableCode = parson.options.unittest_code_prepend + "\n" + executableCode; - } - - try { - mainmod = this._python_exec(executableCode).mainmod; - result = JSON.parse(mainmod.tp$getattr("_test_result").v); - } catch (e) { - result = [{status: "error", _error: e.toString() }]; - } - - // go through the results and generate HTML feedback - for (var i = 0, l = result.length; i < l; i++) { - var res = result[i]; - feedbackHtml += '
'; - if (res.status === "error") { // errors in execution - feedbackHtml += parson.translations.unittest_error(this.stripLinenumberIfNeeded(res._error, - parson.options.unittest_code_prepend, - studentCode)); - success = false; - } else { // passed or failed tests - feedbackHtml += '' + this.stripLinenumberIfNeeded(res.feedback) + '
'; - feedbackHtml += 'Expected ' + res.expected + - '' + res.test + '' + res.actual + - ''; - if (res.status === "fail") { - success = false; - } - } - feedbackHtml += '
'; - } - - return { html: feedbackHtml, tests: result, success: success }; - }; - - // Code "Translating" grader - var LanguageTranslationGrader = function(parson) { - this.parson = parson; - }; - // Add the grader to the list of graders - graders.LanguageTranslationGrader = LanguageTranslationGrader; - // add open/close block definitions for pseudocode - var langBlocks = {}; - LanguageTranslationGrader._languageBlocks = langBlocks; - // specify the blocks for the pseudo language as a simple example case - langBlocks.pseudo = { - open: { - "^\s*IF.*THEN\s*$": "IF", "^\s*ELSE\s*$":"IF", // IF - "^\s*WHILE.*DO\s*$": "WHILE", // WHILE - "^\s*REPEAT.*TIMES\s*$": "REPEAT..TIMES", - "^\s*REPEAT\s*$": "REPEAT", // REPEAT ... UNTIL - "^\s*FOR.*DO\s*$": "FOR", - "^\s*FOR.*TO.*\s*$": "FOR", - "^\s*MODULE.*\\)\s*$": "MODULE", "^\s*MODULE.*RETURNS.*$": "MODULE", - "^\s*DO\s*$": "DO..WHILE" - }, - close: { - "^\s*ELSE\s*$": "IF", "^\s*ENDIF\s*$": "IF", // ENDIF - "^\s*ENDWHILE\s*$": "WHILE", - "^\s*ENDREPEAT\s*$": "REPEAT..TIMES", - "^\s*UNTIL.*\s*$": "REPEAT", - "^\s*ENDFOR\s*$": "FOR", - "^\s*ENDMODULE\s*$": "MODULE", - "^\s*WHILE(?!.*DO)": "DO..WHILE" - } - }; - langBlocks.java = { - open: { - "^.*\{\s*$": "block" - }, - close: { - "^.*\}\s*$": "block" - } - }; - LanguageTranslationGrader.prototype.grade = function() { - var student_code = this.parson.normalizeIndents( - this.parson.getModifiedCode("#ul-" + this.parson.options.sortableId)); - - // Check opening and closing blocks. - // The block_open and block_close are expected to be maps with regexps as properties and - // names of blocks as the property values. For example, a pseudocode IF..THEN..ELSE..ENDIF - // blocks can be defined like this: - // open = {"^\s*IF.*THEN\s*$": "IF", "^\s*ELSE\s*$":"IF"}; - // close = {"^s*ELSE\s*$": "IF", "^\s*ENDIF\s*$": "IF"}; - var open = this.parson.options.block_open, - close = this.parson.options.block_close, - blockErrors = [], - i; - var progLang = this.parson.options.programmingLang; - if (progLang && LanguageTranslationGrader._languageBlocks[progLang]) { - open = $.extend({}, open, LanguageTranslationGrader._languageBlocks[progLang].open); - close = $.extend({}, close, LanguageTranslationGrader._languageBlocks[progLang].close); - } - - if (open && close) { // check blocks only if block definitions are given - var blocks = [], - prevIndent = 0, // keep track of previous indent inside blocks - minIndent = 0; // minimum indent needed inside newly opened blocks - // go through all student code lines - for (i = 0; i < student_code.length; i++) { - var isClose = false, // was a new blocks opened on this line - isOpen = false, // was a block closed on this line - item = student_code[i], - line = $("#" + item.id).text(), // code of the line - topBlock, bO; - - // Check if a proper indentation or the line was found in normalizeIndents - // -1 will mean no matching indent was found - if (item.indent < 0) { - blockErrors.push(this.parson.translations.no_matching(i + 1)); - $("#" + item.id).addClass("incorrectIndent"); - break; // break on error - } - - // Go through all block closing regexps and test if they match - // Some lines can both close and open a block (such as else), so the - // closing blocks need to be handled first - for (var blockClose in close) { - if (new RegExp(blockClose).test(line)) { - isClose = true; - topBlock = blocks.pop(); - if (!topBlock) { - blockErrors.push(this.parson.translations.no_matching_open(i + 1, close[blockClose])); - $("#" + item.id).addClass("incorrectPosition"); - } else if (close[blockClose] !== topBlock.name) { // incorrect closing block - blockErrors.push(this.parson.translations.block_close_mismatch(i + 1, close[blockClose], topBlock.line, topBlock.name)); - $("#" + item.id).addClass("incorrectPosition"); - } else if (student_code[i].indent !== topBlock.indent) { // incorrect indent - blockErrors.push(this.parson.translations.no_matching(i + 1)); - $("#" + item.id).addClass("incorrectIndent"); - } - prevIndent = topBlock?topBlock.indent:0; - minIndent = 0; - break; // only one block can be closed on a single line - } - } - // Go through all block opening regexps and test if they match - for (var blockOpen in open) { - if (new RegExp(blockOpen).test(line)) { - isOpen = true; - bO = {name: open[blockOpen], indent: student_code[i].indent, line: i + 1, item: item}; - blocks.push(bO); - prevIndent = 0; - minIndent = bO.indent; - break; // only one block can be opened on a single line - } - } - // if not opening or closing a block, check block indentation - if (!isClose && !isOpen && blocks.length > 0) { - // indentation should match previous indent if inside block - // and be greater than the indent of the block opening the block (minIndent) - if ((prevIndent && student_code[i].indent !== prevIndent) || - student_code[i].indent <= minIndent) { - blockErrors.push(this.parson.translations.no_matching(i + 1)); - $("#" + item.id).addClass("incorrectIndent"); - } - prevIndent = student_code[i].indent; - } - // if we have errors, clear the blocks and exit from the loop - if (blockErrors.length > 0) { - blocks = []; - break; - } - } - // create errors for all blocks opened but not closed - for (i = 0; i < blocks.length; i++) { - blockErrors.push(this.parson.translations.no_matching_close(blocks[i].line, blocks[i].name)); - $("#" + blocks[i].item.id).addClass("incorrectPosition"); - } - } - // if there were errors in the blocks, give feedback and don't execute the code - if (blockErrors.length > 0) { - var feedback = "
", - fbmsg = ""; - for (i = 0; i < blockErrors.length; i++) { - fbmsg += blockErrors[i] + "
"; - } - feedback += this.parson.translations.unittest_error(fbmsg); - feedback += "
"; - return { html: feedback, success: false }; - } - - // Replace codelines show with codelines to be executed - var code = this._replaceCodelines(); - // run unit tests or variable check grader - if (this.parson.options.unittests) { - return new UnitTestGrader(this.parson).grade(code); - } else { - return new VariableCheckGrader(this.parson).grade(code); - } - }; - // Replaces codelines in the student's solution with the codelines - // specified in the executable_code option of the parsons widget. - // The executable_code option can be an array of lines or a string (in - // which case it will be split on newline. - // For each line in the model solution, there should be a matching line - // in the executable_code. - LanguageTranslationGrader.prototype._replaceCodelines = function() { - var student_code = this.parson.normalizeIndents(this.parson.getModifiedCode("#ul-" + - this.parson.options.sortableId)), - executableCodeString = "", - parson = this.parson, - executableCode = parson.options.executable_code; - if (typeof executableCode === "string") { - executableCode = executableCode.split("\n"); - } - // replace each line with in solution with the corresponding line in executable code - var toggleRegexp = new RegExp("\\$\\$toggle(" + parson.options.toggleSeparator + ".*?)?\\$\\$", "g"); - $.each(student_code, function(index, item) { - var ind = parseInt(item.id.replace(parson.id_prefix, ''), 10); - - // Handle toggle elements. Expects the toggle areas in executable code to be marked - // with $$toggle$$ and there to be as many toggles in executable code than in the - // code shown to learner. - var execline = executableCode[ind]; - var toggles = execline.match(toggleRegexp); - if (toggles) { - for (var i = 0; i < toggles.length; i++) { - var opts = toggles[i].substring(10, toggles[i].length - 2).split(parson.options.toggleSeparator); - if (opts.length >= 1 && opts[0] !== "$$") { - // replace the toggle content with Python executable version as well - execline = execline.replace(toggles[i], opts[item.selectedToggleIndex(i)]); - } else { // use the same content for the toggle in Python - execline = execline.replace(toggles[i], item.toggleValue(i)); - } - } - } - var execlines = execline.split(//); - for (i = 0; i < execlines.length; i++) { - // add the modified codeline to the executable code - executableCodeString += python_indents[item.indent] + execlines[i] + "\n"; - } - }); - return executableCodeString; - }; - - // The "original" grader for giving line based feedback. - var LineBasedGrader = function(parson) { - this.parson = parson; - }; - graders.LineBasedGrader = LineBasedGrader; - LineBasedGrader.prototype.grade = function(elementId) { - var parson = this.parson; - var elemId = elementId || parson.options.sortableId; - var student_code = parson.normalizeIndents(parson.getModifiedCode("#ul-" + elemId)); - var lines_to_check = Math.min(student_code.length, parson.model_solution.length); - var errors = [], log_errors = []; - var incorrectLines = [], studentCodeLineObjects = []; - var i; - var wrong_order = false; - - // Find the line objects for the student's code - for (i = 0; i < student_code.length; i++) { - studentCodeLineObjects.push($.extend(true, - {}, - parson.getLineById(student_code[i].id))); - } - - // This maps codeline strings to the index, at which starting from 0, we have last - // found this codeline. This is used to find the best indices for each - // codeline in the student's code for the LIS computation and, for example, - // assigns appropriate indices for duplicate lines. - var lastFoundCodeIndex = {}; - $.each(studentCodeLineObjects, function(index, lineObject) { - // find the first matching line in the model solution - // starting from where we have searched previously - for (var i = (typeof(lastFoundCodeIndex[lineObject.code]) !== 'undefined') ? lastFoundCodeIndex[lineObject.code]+1 : 0; - i < parson.model_solution.length; - i++) { - if (parson.model_solution[i].code === lineObject.code) { - // found a line in the model solution that matches the student's line - lastFoundCodeIndex[lineObject.code] = i; - lineObject.lisIgnore = false; - // This will be used in LIS computation - lineObject.position = i; - break; - } - } - if (i === parson.model_solution.length) { - if (typeof(lastFoundCodeIndex[lineObject.code]) === 'undefined') { - // Could not find the line in the model solution at all, - // it must be a distractor - // => add to feedback, log, and ignore in LIS computation - wrong_order = true; - lineObject.markIncorrectPosition(); - incorrectLines.push(lineObject.orig); - lineObject.lisIgnore = true; - } else { - // The line is part of the solution but there are now - // too many instances of the same line in the student's code - // => Let's just have their correct position to be the same - // as the last one actually found in the solution. - // LIS computation will handle such duplicates properly and - // choose only one of the equivalent positions to the LIS and - // extra duplicates are left in the inverse and highlighted as - // errors. - // TODO This method will not always give the most intuitive - // highlights for lines to supposed to be moved when there are - // several extra duplicates in the student's code. - lineObject.lisIgnore = false; - lineObject.position = lastFoundCodeIndex[lineObject.code]; - } - - } - }); - - var lisStudentCodeLineObjects = - studentCodeLineObjects.filter(function (lineObject) { return !lineObject.lisIgnore; }); - var inv = - LIS.best_lise_inverse_indices(lisStudentCodeLineObjects - .map(function (lineObject) { return lineObject.position; })); - $.each(inv, function(_index, lineObjectIndex) { - // Highlight the lines that could be moved to fix code as defined by the LIS computation - lisStudentCodeLineObjects[lineObjectIndex].markIncorrectPosition(); - incorrectLines.push(lisStudentCodeLineObjects[lineObjectIndex].orig); - }); - if (inv.length > 0 || incorrectLines.length > 0) { - wrong_order = true; - log_errors.push({type: "incorrectPosition", lines: incorrectLines}); - } - - if (wrong_order) { - errors.push(parson.translations.order()); - } - - // Check the number of lines in student's code - if (parson.model_solution.length < student_code.length) { - $("#ul-" + elemId).addClass("incorrect"); - errors.push(parson.translations.lines_too_many()); - log_errors.push({type: "tooManyLines", lines: student_code.length}); - } else if (parson.model_solution.length > student_code.length){ - $("#ul-" + elemId).addClass("incorrect"); - errors.push(parson.translations.lines_missing()); - log_errors.push({type: "tooFewLines", lines: student_code.length}); - } - - // Finally, check indent if no other errors - if (errors.length === 0) { - for (i = 0; i < lines_to_check; i++) { - var code_line = student_code[i]; - var model_line = parson.model_solution[i]; - if (code_line.indent !== model_line.indent && - ((!parson.options.first_error_only) || errors.length === 0)) { - code_line.markIncorrectIndent(); - errors.push(parson.translations.block_structure(i+1)); - log_errors.push({type: "incorrectIndent", line: (i+1)}); - } - if (code_line.code == model_line.code && - code_line.indent == model_line.indent && - errors.length === 0) { - code_line.markCorrect(); - } - } - } - - return {errors: errors, log_errors: log_errors, success: (errors.length === 0)}; - }; - - - var python_indents = [], - spaces = ""; - for (var counter = 0; counter < 20; counter++) { - python_indents[counter] = spaces; - spaces += " "; - } - - var defaultToggleTypeHandlers = { - boolean: ["True", "False"], - compop: ["<", ">", "<=", ">=", "==", "!="], - mathop: ["+", "-", "*", "/"], - boolop: ["and", "or"], - range: function($item) { - var min = parseFloat($item.data("min") || "0"), - max = parseFloat($item.data("max") || "10"), - step = parseFloat($item.data("step") || "1"), - opts = [], - curr = min; - while (curr <= max) { - opts.push("" + curr); - curr += step; - } - return opts; - } - }; - var addToggleableElements = function(widget) { - for (var i = 0; i < widget.modified_lines.length; i++) { - widget.modified_lines[i]._addToggles(); - } - // toggleable elements are only enabled for unit tests - if (!widget.options.unittests && !widget.options.vartests) { return; } - var handlers = $.extend(defaultToggleTypeHandlers, widget.options.toggleTypeHandlers), - context = $("#" + widget.options.sortableId + ", #" + widget.options.trashId); - $(".jsparson-toggle", context).each(function(index, item) { - var type = $(item).data("type"); - if (!type) { return; } - var handler = handlers[type], - jspOptions; - if ($.isFunction(handler)) { - jspOptions = handler($(item)); - } else { - jspOptions = handler; - } - if (jspOptions && $.isArray(jspOptions)) { - $(item).attr("data-jsp-options", JSON.stringify(jspOptions)); - } - }); - // register a click handler for all the toggleable elements (and unregister existing) - context.off("click", ".jsparson-toggle").on("click", ".jsparson-toggle", function() { - var $this = $(this), - curVal = $this.text(), - choices = $this.data("jsp-options"), - newVal = choices[(choices.indexOf(curVal) + 1)%choices.length], - $parent = $this.parent("li"); - // clear existing feedback - widget.clearFeedback(); - // change the shown toggle element - $this.text(newVal); - // log the event - widget.addLogEntry({type: "toggle", oldvalue: curVal, newvalue: newVal, - target: $parent[0].id, - toggleindex: $parent.find(".jsparson-toggle").index($this)}); - }); - }; - - // Create a line object skeleton with the following from - // a code string of an assignment definition string (see parseCode) - // code: stripped of #paired or #distractor and with real line endings - // indent: how indented is the code based on spaces - // distractor: boolean as to whether it is not part of the solution - // paired: boolean whether this distractor should be paired with last valid line - var ParsonsCodeline = function(codestring, widget) { - this.widget = widget; - this.code = ""; - this.indent = 0; - this._toggles = []; - if (codestring) { - var code = codestring; - if (code.search(/#paired\s*[\\n]?/) >= 0) { - // This line is a paired distractor - this.distractor = true; - this.paired = true; - code = code.replace(/#paired\s*/, ""); - } else if (code.search(/#distractor\s*[\\n]?/) >= 0) { - // This line is a regular distractor - this.distractor = true; - this.paired = false; - code = code.replace(/#distractor\s*/, ""); - } else { - // This line is part of the solution - this.distractor = false; - this.paired = false; - } - // Consecutive lines to be dragged as a single block of code have strings "\\n" to - // represent newlines => replace them with actual new line characters "\n" - //codestring = codestring.replace(/\\n\s+/g,"\\n"); // remove leading spaced if more than one line in a code block - added in below to not change the codestring - this.indent = codestring.length - codestring.replace(/^\s+/, "").length; - var linelist = []; - var line = ""; - - for (i=0; i 0) { - code = ""; - for (i=0;i"); - - } - this.elem().html(html); - this.elem().find(".jsparson-toggle").each(function(index, item) { - that._toggles.push(item); - }); - } - }; - // Returns the number of toggleable elements in this code block - ParsonsCodeline.prototype.toggleCount = function() { - return this._toggles.length; - }; - // Returns the index of the currently selected toggle option for the - // toggle element at given index - ParsonsCodeline.prototype.selectedToggleIndex = function(index) { - if (index < 0 || index >= this._toggles.length) { return -1; } - var elem = this._toggles[index]; - var opts = $(elem).data("jsp-options"); - return opts.indexOf(elem.textContent); - }; - // Returns the value of the toggleable element at the given index (0-based) - ParsonsCodeline.prototype.toggleValue = function(index) { - if (index < 0 || index >= this._toggles.length) { return undefined; } - return this._toggles[index].textContent; - }; - // expose the type for testing, extending etc - window.ParsonsCodeline = ParsonsCodeline; - - // Creates a parsons widget. Init must be called after creating an object. - var ParsonsWidget = function(options) { - // Contains line objects of the user-draggable code. - // The order is not meaningful (unchanged from the initial state) but - // indent property for each line object is updated as the user moves - // codelines around. (see parseCode for line object description) - this.modified_lines = []; - // contains line objects of distractors (see parseCode for line object description) - this.extra_lines = []; - // contains line objects (see parseCode for line object description) - this.model_solution = []; - +/* parsons.js +=== This file contains the JS for the Runestone Parsons component +=== Contributors: +===== Isaiah Mayerchak +===== Barbara Ericson +===== Jeff Rick +=== Adapted from the original JS Parsons by +===== Ville Karavirta +===== Petri Ihantola +===== Juha Helminen +===== Mike Hewner +*/ + +// wrap in anonymous function to not show some helper variables +(function($, _) { + var graders = {}; + var LineBasedGrader = function(widget) { + this.widget = widget; + }; + graders.LineBasedGrader = LineBasedGrader; + + // grade that element + LineBasedGrader.prototype.grade = function() { + var widget = this.widget; + var logAct = widget.answerHash(); + var answerArea = $("#" + widget.options.answerId); + var feedbackArea = $("#" + widget.options.feedbackId); + var solutionLines = widget.solutionLines(); + var answerLines = widget.answerLines(); + var i; + + if (answerLines.length < solutionLines.length) { + // too little code + answerArea.addClass("incorrect"); + feedbackArea.fadeIn(500); + feedbackArea.attr("class", "alert alert-danger"); + feedbackArea.html("Your program is too short."); + } else { + // Determine whether the code is in the correct order + var isCorrectOrder = false; + if (answerLines.length == solutionLines.length) { + isCorrectOrder = true; + for (i = 0; i < solutionLines.length; i++) { + if (answerLines[i].text !== solutionLines[i].text) { + isCorrectOrder = false; + } + } + } + if (isCorrectOrder) { + // Determine whether it is the correct indention + var incorrectIndention = []; + for (i = 0; i < solutionLines.length; i++) { + if (answerLines[i].viewIndent() !== solutionLines[i].modelIndent()) { + console.log(answerLines[i]); + console.log(answerLines[i].viewIndent()); + console.log(solutionLines[i].modelIndent()); + incorrectIndention.push(answerLines[i]); + } + } + if (incorrectIndention.length == 0) { + // Perfect + answerArea.addClass("correct"); + feedbackArea.fadeIn(100); + feedbackArea.attr("class", "alert alert-success"); + feedbackArea.html("Perfect!"); + logAct = "yes"; + } else { + // Incorrect Indention + var incorrectBlocks = []; + for (i = 0; i < incorrectIndention.length; i++) { + block = incorrectIndention[i].block; + if (incorrectBlocks.indexOf(block) == -1) { + incorrectBlocks.push(block); + block.markIncorrectIndent(); + } + } + answerArea.addClass("incorrect"); + feedbackArea.fadeIn(500); + feedbackArea.attr("class", "alert alert-danger"); + if (incorrectBlocks.length == 1) { + feedbackArea.html("This block is not indented correctly. Either indent it more by dragging it right or reduce the indention by dragging it left."); + } else { + feedbackArea.html("These blocks are not indented correctly. To indent a block more, drag it to the right. To reduce the indention, drag it to the left."); + } + } + } else { + // Incorrect: indicate which blocks to move + var answerBlocks = widget.answerBlocks(); + var inSolution = []; + var inSolutionIndexes = []; + var notInSolution = []; + for (i = 0; i < answerBlocks.length; i++) { + var block = answerBlocks[i]; + var index = solutionLines.indexOf(block.lines[0]); + if (index == -1) { + notInSolution.push(block); + } else { + inSolution.push(block); + inSolutionIndexes.push(index); + } + } + var lisIndexes = LIS.best_lise_inverse_indices(inSolutionIndexes); + for (i = 0; i < lisIndexes.length; i++) { + notInSolution.push(inSolution[lisIndexes[i]]); + } + answerArea.addClass("incorrect"); + feedbackArea.fadeIn(500); + feedbackArea.attr("class", "alert alert-danger"); + for (i = 0; i < notInSolution.length; i++) { + notInSolution[i].markIncorrectPosition(); + } + feedbackArea.html("Highlighted blocks in your program are wrong or are in the wrong order. This can be fixed by moving, removing, or replacing highlighted blocks."); + } + } + // log it + widget.problem.logBookEvent({ + "event" : "parsons", + "act" : "yes", + "correct" : logAct, + "answer" : widget.answerHash(), + "trash" : widget.sourceHash(), + "div_id" : this.divid + }); + }; + + // Create a line object with the following + // block = the block that this line is in + // text = the text of the code line + // indent = the indent level + var ParsonsCodeline = function(codestring, block) { + this.block = block; + var trimmed = codestring.replace(/\s*$/, ""); + this.text = trimmed.replace(/^\s*/, ""); + this.indent = trimmed.length - this.text.length; + }; + + // Answer the indent of this codeline as determined by the view (answer) + ParsonsCodeline.prototype.viewIndent = function() { + var indent = this.indent; + if (this.block.widget.options.noindent) { + indent += this.block.indent; + } else { + indent += this.block.viewIndent; + } + return indent; + } + + // Answer the indent of this codeline as determined by the model (solution) + ParsonsCodeline.prototype.modelIndent = function() { + return this.indent + this.block.indent; + } + + // Answer an HTML representation of this codeline + ParsonsCodeline.prototype.asHTML = function() { + var html = '' + this.text + '<\/code>'; + return html; + }; + + // Answer a text representation (i.e. code) of this codeline + ParsonsCodeline.prototype.asText = function() { + var text = ''; + var indent = this.indent + this.block.indent; + for (var i = 0; i < indent; i++) { + // four spaces for each indent + text += ' '; + } + text += this.text; + return text; + }; + + // Create a code block object based on the codestring + // widget: the ParsonsWidget + // index: index of the block (could be an array) + // lines: an array of ParsonsCodeline + // indent: how indented is the code based on spaces + // distractor: boolean as to whether it is not part of the solution + // paired: boolean whether this distractor should be paired with last valid line + var ParsonsCodeblock = function(codestring, widget) { + this.widget = widget; + this.lines = []; + this.indent = 0; + if (codestring) { + var code = codestring; + var options = {}; + // Figure out options based on the #option and #option=value syntax + // Remove the options from the code + code = code.replace(/#(\w+)=(\w+)/, function(mystring, arg1, arg2) { + options[arg1] = arg2; + return "" + }); + code = code.replace(/#(\w+)/, function(mystring, arg1) { + options[arg1] = true; + return "" + }); + + // Based on the options, determine the distractors + if (options["paired"]) { + // paired distractor + delete options["paired"]; + this.distractor = true; + this.paired = true; + } else if (options["distractor"]) { + // distractor + delete options["distractor"]; + this.distractor = true; + this.paired = false; + } else { + // This line is part of the solution + this.distractor = false; + this.paired = false; + } + + //Report unused options + for (var option in options) { + console.log(option + " is not a valid #option for a code block"); + } + + code = code.split(/\\n/); + for (var i = 0; i < code.length; i++) { + code[i] = new ParsonsCodeline(code[i], this); + } + this.lines = code; + } + }; + + // Used to normalize indents + // Part of initialization + ParsonsCodeblock.prototype.addIndentsTo = function(array) { + for (var i = 0; i < this.lines.length; i++) { + var value = this.indent + this.lines[i].indent; + if ($.inArray(value, array) == -1) { + array.push(value); + } + } + }; + + // Normalize indents based on array of indents + // Part of initialization + ParsonsCodeblock.prototype.normalizeIndents = function(array) { + var minIndent = 1000; + for (var i = 0; i < this.lines.length; i++) { + var value = this.indent + this.lines[i].indent; + value = array.indexOf(value); + this.lines[i].indent = value; + minIndent = Math.min(minIndent, value); + } + this.indent = minIndent; + for (i = 0; i < this.lines.length; i++) { + this.lines[i].indent = this.lines[i].indent - minIndent; + } + }; + + // Answer a string that represents this codeblock for saving + ParsonsCodeblock.prototype.hash = function() { + var hash = ""; + if (this.index.constructor === Array) { + for (var i = 0; i < this.index.length; i++) { + hash += this.index[i] + "_"; + } + } else { + hash += this.index + "_"; + } + hash += this.viewIndent; + return hash; + }; + + // Answer an HTML representation of this codeblock + ParsonsCodeblock.prototype.asHTML = function() { + var html = '
'; + for (var i = 0; i < this.lines.length; i++) { + html += this.lines[i].asHTML(); + } + html += '<\/div>'; + return html; + }; + + // Answer a text representation (i.e. code) of this codeblock + ParsonsCodeblock.prototype.asText = function() { + var text = this.lines[0].asText; + for (var i = 1; i < this.lines.length; i++) { + text += '\n' + this.lines[i].asText(); + } + return text; + }; + + // Return the DOM element for the codeblock + ParsonsCodeblock.prototype.elem = function() { + return $("#" + this.id); + }; + + // Mark the view for this codeblock as correct position + ParsonsCodeblock.prototype.markCorrect = function() { + this.elem().addClass(this.widget.FEEDBACK_STYLES.correctPosition); + }; + + // Mark the view for this codeblock as incorrect position + ParsonsCodeblock.prototype.markIncorrectPosition = function() { + this.elem().addClass(this.widget.FEEDBACK_STYLES.incorrectPosition); + }; + + // Mark the view for this codeblock as the incorrect indent + ParsonsCodeblock.prototype.markIncorrectIndent = function() { + this.elem().addClass(this.widget.FEEDBACK_STYLES.incorrectIndent); + }; + + // expose the type for testing, extending etc + window.ParsonsCodeblock = ParsonsCodeblock; + + // Creates a parsons widget. Init must be called after creating an object. + var ParsonsWidget = function(problem, options) { + this.problem = problem; //To collect statistics, feedback should not be based on this this.user_actions = []; @@ -942,192 +325,128 @@ this.states = {}; var defaults = { - 'incorrectSound': false, - 'x_indent': 50, - 'can_indent': true, - 'feedback_cb': false, + 'x_indent': 30, 'first_error_only': true, - 'max_wrong_lines': 10, - 'lang': 'en', - 'toggleSeparator': '::' + 'lang': 'en' }; this.options = jQuery.extend({}, defaults, options); this.feedback_exists = false; - this.id_prefix = options['sortableId'] + 'codeline'; - if (translations.hasOwnProperty(this.options.lang)) { - this.translations = translations[this.options.lang]; - } else { - this.translations = translations['en']; - } + this.id_prefix = options['codelineId']; - // translate trash_label and solution_label - if (!this.options.hasOwnProperty("trash_label")) { - this.options.trash_label = this.translations.trash_label; - } - if (!this.options.hasOwnProperty("solution_label")) { - this.options.solution_label = this.translations.solution_label; - } this.FEEDBACK_STYLES = { 'correctPosition' : 'correctPosition', 'incorrectPosition' : 'incorrectPosition', 'correctIndent' : 'correctIndent', 'incorrectIndent' : 'incorrectIndent'}; - // use grader passed as an option if defined and is a function - if (this.options.grader && _.isFunction(this.options.grader)) { - this.grader = new this.options.grader(this); - } else { - // initialize the grader - if (typeof(this.options.unittests) !== "undefined") { /// unittests are specified - this.grader = new UnitTestGrader(this); - } else if (typeof(this.options.vartests) !== "undefined") { /// tests for variable values - this.grader = new VariableCheckGrader(this); - } else { // "traditional" parson feedback - this.grader = new LineBasedGrader(this); - } - } + this.grader = new LineBasedGrader(this); }; ParsonsWidget._graders = graders; - - ////Public methods - // Parses an assignment definition given as a string and returns and - // transforms this into an object defining the assignment with line objects. - // - // lines: A string that defines the solution to the assignment and also - // any possible distractors - // max_distractrors: The number of distractors allowed to be included with - // the lines required in the solution - ParsonsWidget.prototype.parseCode = function(lines, max_distractors) { - var that = this; - // Create line objects out of each codeline and separate - // lines belonging to the solution and distractor lines - // Fields in line objects: - // code: a string of the code, may include newline characters and - // thus in fact represents a block of consecutive lines - // indent: indentation level, -1 for distractors - // distractor: boolean whether this is a distractor - // paired: boolean whether this is a paired distractor - // orig: the original index of the line in the assignment definition string, - // for distractors this is not meaningful but for lines belonging to the - // solution, this is their expected position - var lineObject, lineObjects = [], distractorIDs = []; - $.each(lines, function(index, item) { - lineObject = new ParsonsCodeline(item, that); - lineObject.orig = index; - if (lineObject.code.length > 0) { - // If it is not whitespace, add it to lineObjects - if (lineObject.distractor) { - distractorIDs.push(index); - } - lineObjects.push(lineObject); - } - }); - - // Normalize the indents, making distractors use -1 - var indents = [], indent, i; - for (i = 0; i < lineObjects.length; i++) { - lineObject = lineObjects[i]; - if (!lineObject.distractor) { - indent = lineObject.indent; - if ($.inArray(indent, indents) == -1) { - indents.push(indent); - } - } - } - indents = indents.sort(function(a, b){return a-b}); - for (i = 0; i < lineObjects.length; i++) { - lineObject = lineObjects[i]; - if (lineObject.distractor) { - lineObject.indent = -1; - } else { - lineObject.indent = indents.indexOf(lineObject.indent); - } - } - - // Trim distractors if necessary - var selectedDistractorIDs; - if (max_distractors < distractorIDs.length) { - // Randomly select which distractors are used - selectedDistractorIDs = []; - var permutation = this.getRandomPermutation(distractorIDs.length); - for (var i = 0; i < max_distractors; i++) { - selectedDistractorIDs.push(distractorIDs[permutation[i]]); - } - } else { - selectedDistractorIDs = distractorIDs; - } - - // Create solution, distractors, initial and errors - // and return them - var solution = [], distractors = [], initial = [], errors = []; - $.each(lineObjects, function(index, item) { - // Make a copy (need it but it doesn't seem right) - lineObject = jQuery.extend({}, item); - if (item.distractor) { - if ($.inArray(item.orig, selectedDistractorIDs) > -1) { - distractors.push(lineObject); - initial.push(lineObject); - } - } else { - solution.push(lineObject); - initial.push(lineObject); - } - }); - return { - // an array of line objects specifying the solution - solution: $.extend(true, [], solution), - // an array of line objects specifying the requested number - // of distractors (not all possible alternatives) - distractors: $.extend(true, [], distractors), - // an array of line objects specifying the initial code arrangement - // given to the user to use in constructing the solution - widgetInitial: $.extend(true, [], initial), - errors: errors - }; - }; - - ParsonsWidget.prototype.init = function(text) { - // TODO: Error handling, parseCode may return errors in an array in property named errors. - var initial_structures = this.parseCode(text.split("\n"), this.options.max_wrong_lines); - this.model_solution = initial_structures.solution; - this.extra_lines = initial_structures.distractors; - this.modified_lines = initial_structures.widgetInitial; - var id_prefix = this.id_prefix; - - // Add ids to the line objects in the user-draggable lines - $.each(this.modified_lines, function(index, item) { - item.id = id_prefix + index; - item.indent = 0; - }); - }; - - ParsonsWidget.prototype.getHash = function(searchString) { - var hash = [], - ids = $(searchString).sortable('toArray'), - line; - for (var i = 0; i < ids.length; i++) { - line = this.getLineById(ids[i]); - hash.push(line.orig + "_" + line.indent); - } - //prefix with something to handle empty output situations - if (hash.length === 0) { - return "-"; - } else { - return hash.join("-"); - } - }; + // Initialize the ParsonsWidget object with the following properties + // blocks: an array of codeblocks as they are specified in the HTML text + // solution: the array of codeblocks that is the solution + ParsonsWidget.prototype.init = function(text) { + var that = this; + var id_prefix = this.id_prefix; + + // Create the initial blocks + var aBlock, blocks = []; + $.each(text.split("\n"), function(index, item) { + aBlock = new ParsonsCodeblock(item, that); + aBlock.index = index; + aBlock.id = id_prefix + index; + aBlock.viewIndent = 0; + blocks.push(aBlock); + }); + // Normalize the indents + var indents = []; + for (i = 0; i < blocks.length; i++) { + blocks[i].addIndentsTo(indents); + } + indents = indents.sort(function(a, b){return a-b}); + for (i = 0; i < blocks.length; i++) { + blocks[i].normalizeIndents(indents); + } + + // For convenience sake, create the solution. + // Note that this can always be reconstructed from the blocks + var solution = []; + $.each(blocks, function(index, item) { + if (!item.distractor) { + solution.push(item); + } + }); + + this.blocks = blocks; + this.solution = solution; + this.resetView(); + }; + + // Create a hash that identifies the block order and indention + ParsonsWidget.prototype.getHash = function(searchString) { + var hash = [], + divs = $(searchString)[0].getElementsByTagName('div'), + block; + for (var i = 0; i < divs.length; i++) { + block = this.getBlockById(divs[i].id); + hash.push(block.hash()); + } + //prefix with something to handle empty output situations + if (hash.length === 0) { + return "-"; + } else { + return hash.join("-"); + } + }; - ParsonsWidget.prototype.solutionHash = function() { - return this.getHash("#ul-" + this.options.sortableId); + // Answer the hash of the answer area + ParsonsWidget.prototype.answerHash = function() { + return this.getHash("#" + this.options.answerId); }; - ParsonsWidget.prototype.trashHash = function() { - return this.getHash("#ul-" + this.options.trashId); + // Answer the hash of the source area + ParsonsWidget.prototype.sourceHash = function() { + return this.getHash("#" + this.options.sourceId); }; + // Return a codeblock that corresponds to the hash + ParsonsWidget.prototype.blockFromHash = function(hash) { + var split = hash.split("_"); + var block = this.blocks[Number(split[0])]; + if (this.options.noindent) { + block.viewIndent = 0; + } else { + block.viewIndent = Number(split[1]); + } + return block; + }; + + // Return an array of codeblocks that corresponds to the hash + ParsonsWidget.prototype.blocksFromHash = function(hash) { + var split; + if (hash === "-" || hash === "" || hash === null) { + split = []; + } else { + split = hash.split("-"); + } + var blocks = []; + for (var i = 0; i < split.length; i++) { + blocks.push(this.blockFromHash(split[i])); + } + return blocks; + }; + + // Update the HTML based on hashes + // Called from local storage + ParsonsWidget.prototype.createHTMLFromHashes = function(sourceHash, answerHash) { + var sourceBlocks = this.blocksFromHash(sourceHash); + var answerBlocks = this.blocksFromHash(answerHash); + this.createView(sourceBlocks, answerBlocks); + }; + ParsonsWidget.prototype.whatWeDidPreviously = function() { - var hash = this.solutionHash(); + var hash = this.answerHash(); var previously = this.states[hash]; if (!previously) { return undefined; } var visits = _.filter(this.state_path, function(state) { @@ -1145,410 +464,507 @@ return $.extend(false, {'visits': visits, stepsToLast: stepsToLast}, previously); }; - /** - * Returns states of the toggles for logging purposes - */ - ParsonsWidget.prototype._getToggleStates = function() { - var context = $("#" + this.options.sortableId + ", #" + this.options.trashId), - toggles = $(".jsparson-toggle", context), - toggleStates = {}; - $("#" + this.options.sortableId + " .jsparson-toggle").each(function() { - if (!toggleStates.output) { - toggleStates.output = []; - } - toggleStates.output.push($(this).text()); - }); - if (this.options.trashId) { - toggleStates.input = []; - $("#" + this.options.trashId + " .jsparson-toggle").each(function() { - toggleStates.input.push($(this).text()); - }); - } - if ((toggleStates.output && toggleStates.output.length > 0) || - (toggleStates.input && toggleStates.input.length > 0)) { - return toggleStates; - } else { - return undefined; - } - }; - - ParsonsWidget.prototype.addLogEntry = function(entry) { - var state, previousState; - var logData = { - time: new Date(), - output: this.solutionHash(), - type: "action" - }; - - if (this.options.trashId) { - logData.input = this.trashHash(); - } - - if (entry.target) { - entry.target = entry.target.replace(this.id_prefix, ""); - } - - // add toggle states to log data if there are toggles - var toggles = this._getToggleStates(); - if (toggles) { - logData.toggleStates = toggles; - } - - state = logData.output; - - jQuery.extend(logData, entry); - this.user_actions.push(logData); - - //Updating the state history - if(this.state_path.length > 0) { - previousState = this.state_path[this.state_path.length - 1]; - this.states[previousState] = logData; - } - - //Add new item to the state path only if new and previous states are not equal - if (this.state_path[this.state_path.length - 1] !== state) { - this.state_path.push(state); - } - // callback for reacting to actions - if ($.isFunction(this.options.action_cb)) { - this.options.action_cb.call(this, logData); - } - }; - - /** - * Update indentation of a line based on new coordinates - * leftDiff horizontal difference from (before and after drag) in px - ***/ - ParsonsWidget.prototype.updateIndent = function(leftDiff, id) { - - var code_line = this.getLineById(id); - var new_indent = this.options.can_indent ? code_line.indent + Math.floor(leftDiff / this.options.x_indent) : 0; - new_indent = Math.max(0, new_indent); - code_line.indent = new_indent; - - return new_indent; - }; - - // Get a line object by the full id including id prefix - // (see parseCode for description of line objects) - ParsonsWidget.prototype.getLineById = function(id) { - var index = -1; - for (var i = 0; i < this.modified_lines.length; i++) { - if (this.modified_lines[i].id == id) { - index = i; - break; - } - } - return this.modified_lines[index]; - }; - - // Check and normalize code indentation. - // Does not use the current object (this) ro make changes to - // the parameter. - // Returns a new array of line objects whose indent fields' values - // may be different from the argument. If indentation does not match, - // i.e. code is malformed, value of indent may be -1. - // For example, the first line may not be indented. - ParsonsWidget.prototype.normalizeIndents = function(lines) { - - var normalized = []; - var new_line; - var match_indent = function(index) { - //return line index from the previous lines with matching indentation - for (var i = index-1; i >= 0; i--) { - if (lines[i].indent == lines[index].indent) { - return normalized[i].indent; - } - } - return -1; - }; - for ( var i = 0; i < lines.length; i++ ) { - //create shallow copy from the line object - new_line = jQuery.extend({}, lines[i]); - if (i === 0) { - new_line.indent = 0; - if (lines[i].indent !== 0) { - new_line.indent = -1; - } - } else if (lines[i].indent == lines[i-1].indent) { - new_line.indent = normalized[i-1].indent; - } else if (lines[i].indent > lines[i-1].indent) { - new_line.indent = normalized[i-1].indent + 1; - } else { - // indentation can be -1 if no matching indentation exists, i.e. IndentationError in Python - new_line.indent = match_indent(i); - } - normalized[i] = new_line; - } - return normalized; - }; - - /** - * Retrieve the code lines based on what is in the DOM - * - * TODO(petri) refactor to UI - * */ - ParsonsWidget.prototype.getModifiedCode = function(search_string) { - //ids of the the modified code - var lines_to_return = [], - solution_ids = $(search_string).sortable('toArray'), - i, item; - for (i = 0; i < solution_ids.length; i++) { - item = this.getLineById(solution_ids[i]); - lines_to_return.push($.extend(new ParsonsCodeline(), item)); - } - return lines_to_return; - }; - - ParsonsWidget.prototype.hashToIDList = function(hash) { - var lines = []; - var lineValues; - var lineObject; - var h; - - if (hash === "-" || hash === "" || hash === null) { - h = []; - } else { - h = hash.split("-"); - } - - var ids = []; - for (var i = 0; i < h.length; i++) { - lineValues = h[i].split("_"); - ids.push(this.modified_lines[lineValues[0]].id); - } - return ids; - }; - - ParsonsWidget.prototype.updateIndentsFromHash = function(hash) { - var lineValues; - var h; - - if (hash === "-" || hash === "" || hash === null) { - h = []; - } else { - h = hash.split("-"); - } - - var ids = []; - for (var i = 0; i < h.length; i++) { - lineValues = h[i].split("_"); - this.modified_lines[lineValues[0]].indent = Number(lineValues[1]); - this.updateHTMLIndent(this.modified_lines[lineValues[0]].id); - } - return ids; - }; - - - /** - * TODO(petri) refoctor to UI - */ - ParsonsWidget.prototype.displayError = function(message) { - if (this.options.incorrectSound && $.sound) { - $.sound.play(this.options.incorrectSound); - } - alert(message); - }; - - ParsonsWidget.prototype.colorFeedback = function(elemId) { - return new LineBasedGrader(this).grade(elemId); - }; - - - - - /** - * @return - * TODO(petri): Separate UI from here - */ - ParsonsWidget.prototype.getFeedback = function() { - this.feedback_exists = true; - var fb = this.grader.grade(); - if (this.options.feedback_cb) { - this.options.feedback_cb(fb); //TODO(petri): what is needed? - } - // if answer is correct, mark it in the UI - if (fb.success) { - $("#ul-" + this.options.sortableId).addClass("correct"); - } - // log the feedback and return; based on the type of grader - if ('html' in fb) { // unittest/vartests type feedback - this.addLogEntry({type: "feedback", tests: fb.tests, success: fb.success}); - return { feedback: fb.html, success: fb.success }; - } else { - this.addLogEntry({type: "feedback", errors: fb.log_errors, success: fb.success}); - return fb.errors; - } - }; - - ParsonsWidget.prototype.clearFeedback = function() { - if (this.feedback_exists) { - $("#ul-" + this.options.sortableId).removeClass("incorrect correct"); - var li_elements = $("#ul-" + this.options.sortableId + " li"); - $.each(this.FEEDBACK_STYLES, function(index, value) { - li_elements.removeClass(value); - }); - } - this.feedback_exists = false; - }; - - - ParsonsWidget.prototype.getRandomPermutation = function(n) { - var permutation = []; - var i; - for (i = 0; i < n; i++) { - permutation.push(i); - } - var swap1, swap2, tmp; - for (i = 0; i < n; i++) { - swap1 = Math.floor(Math.random() * n); - swap2 = Math.floor(Math.random() * n); - tmp = permutation[swap1]; - permutation[swap1] = permutation[swap2]; - permutation[swap2] = tmp; - } - return permutation; - }; - - - ParsonsWidget.prototype.shuffleLines = function() { - var permutationFunction = this.options.permutation?this.options.permutation:this.getRandomPermutation; - var chunks = [], chunk = []; - $.each(this.modified_lines, function(index, item) { - if (item.paired) { - chunk.push(item); - } else { - chunk = []; - chunk.push(item); - chunks.push(chunk); - } - }); - var permutation = permutationFunction(chunks.length); - var sortedChunks = []; - for (var i in permutation) { - sortedChunks.push(chunks[permutation[i]]); - } - var idlist = []; - for (var c = 0; c < chunks.length; c++) { - chunk = sortedChunks[c]; - if (chunk.length > 1) { - permutation = permutationFunction(chunk.length); - for (var i in permutation) { - idlist.push(chunk[permutation[i]].id); - } - } else { - idlist.push(chunk[0].id); - } - } - if (this.options.trashId) { - this.createHTMLFromLists([],idlist); - } else { - this.createHTMLFromLists(idlist,[]); - } - addToggleableElements(this); - }; - - ParsonsWidget.prototype.createHTMLFromHashes = function(solutionHash, trashHash) { - var solution = this.hashToIDList(solutionHash); - var trash = this.hashToIDList(trashHash); - this.createHTMLFromLists(solution, trash); - this.updateIndentsFromHash(solutionHash); - }; - - ParsonsWidget.prototype.updateHTMLIndent = function(codelineID) { - var line = this.getLineById(codelineID); - $('#' + codelineID).css("margin-left", this.options.x_indent * line.indent + "px"); - }; - - - ParsonsWidget.prototype.codeLineToHTML = function(codeline) { - return '
  • ' + codeline.code + '<\/li>'; - }; - - ParsonsWidget.prototype.codeLinesToHTML = function(codelineIDs, destinationID) { - var lineHTML = []; - for(var id in codelineIDs) { - var line = this.getLineById(codelineIDs[id]); - lineHTML.push(this.codeLineToHTML(line)); - } - return '
      '+lineHTML.join('')+'
    '; - }; - - /** modifies the DOM by inserting exercise elements into it */ - ParsonsWidget.prototype.createHTMLFromLists = function(solutionIDs, trashIDs) { - var html; - if (this.options.trashId) { - html = (this.options.trash_label?'

    '+this.options.trash_label+'

    ':'') + - this.codeLinesToHTML(trashIDs, this.options.trashId); - $("#" + this.options.trashId).html(html); - html = (this.options.solution_label?'

    '+this.options.solution_label+'

    ':'') + - this.codeLinesToHTML(solutionIDs, this.options.sortableId); - $("#" + this.options.sortableId).html(html); - } else { - html = this.codeLinesToHTML(solutionIDs, this.options.sortableId); - $("#" + this.options.sortableId).html(html); - } - - if (window.prettyPrint && (typeof(this.options.prettyPrint) === "undefined" || this.options.prettyPrint)) { - prettyPrint(); - } - - var that = this; - var sortable = $("#ul-" + this.options.sortableId).sortable( - { - start : function() { that.clearFeedback(); }, - stop : function(event, ui) { - if ($(event.target)[0] != ui.item.parent()[0]) { - return; - } - that.updateIndent(ui.position.left - ui.item.parent().position().left, - ui.item[0].id); - that.updateHTMLIndent(ui.item[0].id); - that.addLogEntry({type: "moveOutput", target: ui.item[0].id}, true); - }, - receive : function(event, ui) { - var ind = that.updateIndent(ui.position.left - ui.item.parent().position().left, - ui.item[0].id); - that.updateHTMLIndent(ui.item[0].id); - that.addLogEntry({type: "addOutput", target: ui.item[0].id}, true); - }, - grid : that.options.can_indent ? [that.options.x_indent, 1 ] : false - }); - sortable.addClass("output"); - if (this.options.trashId) { - var trash = $("#ul-" + this.options.trashId).sortable( - { - connectWith: sortable, - start: function() { that.clearFeedback(); }, - receive: function(event, ui) { - that.getLineById(ui.item[0].id).indent = 0; - that.updateHTMLIndent(ui.item[0].id); - that.addLogEntry({type: "removeOutput", target: ui.item[0].id}, true); - }, - stop: function(event, ui) { - if ($(event.target)[0] != ui.item.parent()[0]) { - // line moved to output and logged there - return; - } - that.addLogEntry({type: "moveInput", target: ui.item[0].id}, true); - } - }); - sortable.sortable('option', 'connectWith', trash); - } - // Log the original codelines in the exercise in order to be able to - // match the input/output hashes to the code later on. We need only a - // few properties of the codeline objects - var bindings = []; - for (var i = 0; i < this.modified_lines.length; i++) { - var line = this.modified_lines[i]; - bindings.push({code: line.code, distractor: line.distractor}) - } - this.addLogEntry({type: 'init', time: new Date(), bindings: bindings}); - }; - - - window['ParsonsWidget'] = ParsonsWidget; - } + // Return a block object by the full id including id prefix + ParsonsWidget.prototype.getBlockById = function(id) { + for (var i = 0; i < this.blocks.length; i++) { + var block = this.blocks[i]; + if (block.id == id) { + return block; + } + } + return undefined; + }; + + // Retrieve the codelines based on what is in the DOM + ParsonsWidget.prototype.getModifiedCode = function(search_string) { + var codeLines = []; + var that = this; + $(search_string + " div").each(function(idx, i) { + var domItem = $(i); + var lineItem = that.getBlockById(domItem[0].id); + codeLines.push(lineItem); + }); + return codeLines; + }; + + // Return array of codeblocks based on what is in the answer field + ParsonsWidget.prototype.answerBlocks = function() { + var that = this; + var answerBlocks = []; + $("#" + this.options.answerId + " div").each(function(idx, i) { + answerBlocks.push(that.getBlockById($(i)[0].id)); + }); + return answerBlocks; + }; + + // Return array of codelines based on what is in the answer field + ParsonsWidget.prototype.answerLines = function() { + var that = this; + var answerLines = []; + var blocks = this.answerBlocks(); + for (var i = 0; i < blocks.length; i++) { + var block = blocks[i]; + for (var j = 0; j < block.lines.length; j++) { + answerLines.push(block.lines[j]); + } + } + return answerLines; + }; + + // Return array of codelines based on what is in the solution + ParsonsWidget.prototype.solutionLines = function() { + var solutionLines = []; + for (var i = 0; i < this.solution.length; i++) { + var lines = this.solution[i].lines; + for (var j = 0; j < lines.length; j++) { + solutionLines.push(lines[j]); + } + } + return solutionLines; + }; + + // Grade the answer compared to the solution + ParsonsWidget.prototype.getFeedback = function() { + this.grader.grade(); + this.feedback_exists = true; + }; + + // Clear any feedback from the answer area + ParsonsWidget.prototype.clearFeedback = function() { + if (this.feedback_exists) { + $("#" + this.options.answerId).removeClass("incorrect correct"); + var blocks = $("#" + this.options.answerId + " div"); + $.each(this.FEEDBACK_STYLES, function(index, value) { + blocks.removeClass(value); + }); + $("#" + this.options.feedbackId).hide(); + } + this.feedback_exists = false; + }; + + // A function for returning a shuffled version of an array + ParsonsWidget.prototype.shuffled = function(array) { + var currentIndex = array.length; + var returnArray = array.slice(); + var temporaryValue, randomIndex; + // While there remain elements to shuffle... + while (0 !== currentIndex) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + // And swap it with the current element. + temporaryValue = returnArray[currentIndex]; + returnArray[currentIndex] = returnArray[randomIndex]; + returnArray[randomIndex] = temporaryValue; + } + return returnArray; + }; + + // Based on the movingId, etc., establish the moving state + // rest = not moving + // source = moving inside source area + // answer = moving inside answer area + // moving = moving outside areas + ParsonsWidget.prototype.movingState = function() { + if (this.movingId == undefined) { + return "rest"; + } + var moving = $("#" + this.movingId); + var x = this.movingX; + var y = this.movingY; + // Check if in answer area + var left = this.answerArea.offset().left; + var right = left + this.answerArea.outerWidth(); + var top = this.answerArea.offset().top; + var bottom = top + this.answerArea.outerHeight(); + if (x >= left && (x <= right) && (y >= top) && (y <= bottom)) { + return "answer"; + } + // Check if in source area + left = this.sourceArea.offset().left; + right = left + this.sourceArea.outerWidth(); + top = this.sourceArea.offset().top; + bottom = top + this.sourceArea.outerHeight(); + if (x >= left && (x <= right) && (y >= top) && (y <= bottom)) { + return "source"; + } + return "moving"; + } + + // Update the ParsonsWidget view + // This occurs when dragging the moving tile + ParsonsWidget.prototype.updateView = function() { + // Based on the new and the old state, figure out what to update + var state = this.state; + var newState = this.movingState(); + var updateSource = true; + var updateAnswer = true; + var updateMoving = newState == "moving"; + if (state == newState) { + if (newState == "rest") { + updateSource = false; + updateAnswer = false; + } else if (newState == "source") { + updateAnswer = false; + } else if (newState == "answer") { + updateSource = false; + } else if (newState == "moving") { + updateAnswer = false; + updateSource = false; + } + } + var moving = undefined; + var movingHeight; + if (this.movingId !== undefined) { + moving = $("#" + this.movingId); + // Must get height here as detached items don't have height + movingHeight = moving.outerHeight(true); + moving.detach(); + } + + var positionTop, width; + var that = this; + var baseWidth = this.sourceArea.width() - 22; + + // Update the Source Area + if (updateSource) { + positionTop = 0; + if (newState == "source") { + var hasInserted = false; + var x = this.movingX - this.sourceArea.offset().left - baseWidth / 2 - 11; + var y = this.movingY - this.sourceArea.offset().top; + $("#" + this.options.sourceId + " div").each(function(idx, i) { + item = $(i); + if (item[0].id !== "") { + if (!hasInserted) { + if (y - positionTop < (movingHeight + item.outerHeight(true)) / 2) { + hasInserted = true; + moving.insertBefore("#" + item[0].id); + moving.css({ + 'left' : x, + 'top' : y - movingHeight / 2, + 'width' : baseWidth, + 'z-index' : 2 + }); + positionTop = positionTop + movingHeight; + } + } + item.css({ + 'left' : 0, + 'top' : positionTop, + 'width' : baseWidth, + 'z-index' : 1 + }); + positionTop = positionTop + item.outerHeight(true); + } + }); + if (!hasInserted) { + moving.appendTo("#" + this.options.sourceId); + moving.css({ + 'left' : x, + 'top' : y - moving.outerHeight(true) / 2, + 'width' : baseWidth, + 'z-index' : 2 + }); + } + } else { + $("#" + this.options.sourceId + " div").each(function(idx, i) { + item = $(i); + if (item[0].id !== "") { + item.css({ + 'left' : 0, + 'top' : positionTop, + 'width' : baseWidth, + 'z-index' : 1 + }); + positionTop = positionTop + item.outerHeight(true); + } + }); + } + } + + // Update the Answer Area + if (updateAnswer) { + var block, indent; + positionTop = 0; + width = this.answerArea.width() - 22; + var that = this; + if (newState == "answer") { + var hasInserted = false; + var x = this.movingX - this.answerArea.offset().left - baseWidth / 2 - 11; + movingIndent = Math.round(x / this.options.x_indent); + if (movingIndent < 0) { + movingIndent = 0; + } else if (movingIndent > this.indent) { + movingIndent = this.indent; + } else { + x = movingIndent * this.options.x_indent; + } + var y = this.movingY - this.answerArea.offset().top; + block = this.getBlockById(this.movingId); + block.viewIndent = movingIndent; + $("#" + this.options.answerId + " div").each(function(idx, i) { + item = $(i); + if (item[0].id !== "") { + if (!hasInserted) { + if (y - positionTop < (movingHeight + item.outerHeight(true)) / 2) { + hasInserted = true; + moving.insertBefore("#" + item[0].id); + moving.css({ + 'left' : x, + 'top' : y - movingHeight / 2, + 'width' : baseWidth, + 'z-index' : 2 + }); + positionTop = positionTop + movingHeight; + } + } + block = that.getBlockById(item[0].id); + indent = block.viewIndent * that.options.x_indent; + item.css({ + 'left' : indent, + 'top' : positionTop, + 'width' : width - indent, + 'z-index' : 1 + }); + positionTop = positionTop + item.outerHeight(true); + } + }); + if (!hasInserted) { + moving.appendTo("#" + this.options.answerId); + moving.css({ + 'left' : x, + 'top' : y - moving.outerHeight(true) / 2, + 'width' : baseWidth, + 'z-index' : 2 + }); + } + } else { + $("#" + this.options.answerId + " div").each(function(idx, i) { + item = $(i); + if (item[0].id !== "") { + block = that.getBlockById(item[0].id); + indent = block.viewIndent * that.options.x_indent; + item.css({ + 'left' : indent, + 'top' : positionTop, + 'width' : width - indent, + 'z-index' : 1 + }); + positionTop = positionTop + item.outerHeight(true); + } + }); + } + } + + // Update the Moving Area + if (updateMoving) { + moving.appendTo("#" + this.options.sourceId); + width = this.sourceArea.width() - 22; + moving.css({ + 'left' : this.movingX - this.sourceArea.offset().left - (moving.outerWidth(true) / 2), + 'top' : this.movingY - this.sourceArea.offset().top - (movingHeight / 2), + 'width' : width, + 'z-index' : 2 + }); + } + + state = newState; + this.state = state; + }; + + // Reset the view based on this.blocks accounting for + // * shorten to the distractors to maxdist size + // * if an order is specified, then use that + // * else shuffle the blocks randomly, accounting for paired distractors + // * call createView with the shuffled blocks in the source field + ParsonsWidget.prototype.resetView = function() { + var blocks = [], i, aBlock; + for (i = 0; i < this.blocks.length; i++) { + blocks.push(this.blocks[i]); + } + + // Trim the distractors (if necessary) + if (this.options.maxdist !== undefined) { + var distractorIDs = []; + for (i = 0; i < blocks.length; i++) { + distractorIDs.push(blocks[i].id); + } + if (this.options.maxdist < distractorIDs.length) { + distractorIDs = this.shuffled(distractorIDs); + distractorIDs = distractorIDs.slice(0, this.options.maxdist - 1); + var trimmed = []; + for (i = 0; i < blocks.length; i++) { + aBlock = blocks[i]; + if (aBlock.distractor) { + if ($.inArray(aBlock.id, distractorIDs)) { + trimmed.push(aBlock); + } + } else { + trimmed.push(aBlock); + } + } + blocks = trimmed; + } + } + + // Reorder the sourceBlock + var sourceBlocks = []; + if (this.options.order === undefined) { + // Shuffle, respecting paired distractors + var chunks = [], chunk = []; + $.each(blocks, function(index, item) { + if (item.paired) { + chunk.push(item); + } else { + chunk = []; + chunk.push(item); + chunks.push(chunk); + } + }); + chunks = this.shuffled(chunks); + for (var c = 0; c < chunks.length; c++) { + chunk = chunks[c]; + if (chunk.length > 1) { + // shuffle paired distractors + chunk = this.shuffled(chunk); + for (i = 0; i < chunk.length; i++) { + sourceBlocks.push(chunk[i]); + } + } else { + sourceBlocks.push(chunk[0]); + } + } + } else { + // Use the specified order to create the sourceBlocks + // Note that any lines not specified in the order are deleted + var order = this.options.order; + for (i = 0; i < order.length; i++) { + for (var j = 0; j < blocks.length; j++) { + if (blocks[j].index === order[i]) { + sourceBlocks.push(blocks[j]); + } + } + } + } + this.createView(sourceBlocks, []); + }; + + // Based on the blocks, create the view and insert it into the DOM + ParsonsWidget.prototype.createView = function(sourceBlocks, answerBlocks) { + var html, i; + if (this.options.sourceId) { + // Add source area + html = '

    Drag from here

    '; + html += '
    ' + for (i = 0; i < sourceBlocks.length; i++) { + html += sourceBlocks[i].asHTML(); + } + html += '<\/div>'; + $("#" + this.options.sourceRegionId).html(html); + // Add answer area + html = '

    Drop blocks here

    '; + html += '
    ' + for (i = 0; i < answerBlocks.length; i++) { + html += answerBlocks[i].asHTML(); + } + html += '<\/div>'; + $("#" + this.options.answerRegionId).html(html); + } else { + // Add only the answer area + html = ''; + html += '
    ' + for (i = 0; i < answerBlocks.length; i++) { + html += answerBlocks[i].asHTML(); + } + html += '<\/div>'; + $("#" + this.options.answerRegionId).html(html); + } + + if (window.prettyPrint && (typeof(this.options.prettyPrint) === "undefined" || this.options.prettyPrint)) { + prettyPrint(); + } + var answerArea = $("#" + this.options.answerId); + var sourceArea = $("#" + this.options.sourceId); + // Establish the width and height of the droppable areas + var areaWidth = 0; + var areaHeight = 6; + var item; + var maxFunction = function(idx, i) { + item = $(i); + areaHeight = areaHeight + item.outerHeight(true); + areaWidth = Math.max(areaWidth, item.outerWidth(true)); + }; + $("#" + this.options.answerId + " div").each(maxFunction); + $("#" + this.options.sourceId + " div").each(maxFunction); + // Determine how much indent should be possible in the answer area + var indent; + if (this.options.noindent) { + indent = 0; + } else { + // Set the indent so that the solution is possible + indent = 1; + for (var i = 0; i < this.solution.length; i++) { + indent = Math.max(indent, this.solution[i].indent); + } + } + sourceArea.height(areaHeight); + sourceArea.width(areaWidth); + answerArea.height(areaHeight); + answerArea.width(this.options.x_indent * indent + areaWidth); + this.answerArea = answerArea; + this.sourceArea = sourceArea; + this.indent = indent; + if (indent > 0 && indent <= 4) { + answerArea.addClass("answer" + indent); + } else { + answerArea.addClass("answer"); + } + var that = this; + that.state = undefined; // needs to be here for loading from storage + that.updateView(); + + var draggableOptions = { + helper : "clone", + distance : 0, + scope : that.options.answerId, + drag : function(event, ui) { + // Update the view + that.movingX = event.pageX; + that.movingY = event.pageY; + that.updateView(); + }, + start : function(event, ui) { + that.clearFeedback(); + // Move original; hide clone + that.movingId = $(this)[0].id; + $(ui.helper).hide(); + // Update the view + that.movingX = event.pageX; + that.movingY = event.pageY; + that.updateView(); + }, + stop : function(event, ui) { + // Restore functionality to original + $("#" + that.movingId).draggable(draggableOptions); + delete that.movingId; + delete that.movingX; + delete that.movingY; + that.updateView(); + } + }; + + // Assign draggable options to the areas + for (var i = 0; i < sourceBlocks.length; i++) { + $("#" + sourceBlocks[i].id).draggable(draggableOptions); + } + for (var i = 0; i < answerBlocks.length; i++) { + $("#" + answerBlocks[i].id).draggable(draggableOptions); + } + }; + + window['ParsonsWidget'] = ParsonsWidget; +} // allows _ and $ to be modified with noconflict without changing the globals // that parsons uses )($,_); diff --git a/runestone/parsons/js/parsons_setup.js b/runestone/parsons/js/parsons_setup.js index 995bbc8cb..300d14202 100644 --- a/runestone/parsons/js/parsons_setup.js +++ b/runestone/parsons/js/parsons_setup.js @@ -119,12 +119,12 @@ Parsons.prototype.createParsonsView = function () { // Create DOM elemen this.containerDiv.appendChild(this.sortContainerDiv); this.sortTrashDiv = document.createElement("div"); - this.sortTrashDiv.id = "parsons-sortableTrash-" + this.counterId; + this.sortTrashDiv.id = "parsons-sourceRegion-" + this.counterId; $(this.sortTrashDiv).addClass("sortable-code"); this.sortContainerDiv.appendChild(this.sortTrashDiv); this.sortCodeDiv = document.createElement("div"); - this.sortCodeDiv.id = "parsons-sortableCode-" + this.counterId; + this.sortCodeDiv.id = "parsons-answerRegion-" + this.counterId; $(this.sortCodeDiv).addClass("sortable-code"); this.sortContainerDiv.appendChild(this.sortCodeDiv); @@ -163,30 +163,16 @@ Parsons.prototype.createParsonsView = function () { // Create DOM elemen Parsons.prototype.setButtonFunctions = function () { $pjQ(this.resetButt).click(function (event) { event.preventDefault(); - this.pwidget.shuffleLines(); - - // set min width and height - var sortableul = $("#ul-parsons-sortableCode-" + this.counterId); - var trashul = $("#ul-parsons-sortableTrash-" + this.counterId); - var sortableHeight = sortableul.height(); - var sortableWidth = sortableul.width(); - var trashWidth = trashul.width(); - var trashHeight = trashul.height(); - var minHeight = Math.max(trashHeight, sortableHeight); - var minWidth = Math.max(trashWidth, sortableWidth); - trashul.css("min-height", minHeight + "px"); - sortableul.css("min-height", minHeight + "px"); - trashul.css("min-width", minWidth + "px"); - sortableul.css("min-width", minWidth + "px"); + this.pwidget.resetView(); $(this.messageDiv).hide(); - }.bind(this)); + }.bind(this)); $pjQ(this.checkButt).click(function (event) { event.preventDefault(); - this.setLocalStorage(); - + var hash = this.pwidget.answerHash(); + localStorage.setItem(this.divid, hash); + hash = this.pwidget.sourceHash(); + localStorage.setItem(this.divid + "-source", hash); this.pwidget.getFeedback(); - $(this.messageDiv).fadeIn(100); - }.bind(this)); }; @@ -204,61 +190,38 @@ Parsons.prototype.createParsonsWidget = function () { return false; }); - this.pwidget = new ParsonsWidget({ - "sortableId": "parsons-sortableCode-" + this.counterId, - "trashId": "parsons-sortableTrash-" + this.counterId, - "max_wrong_lines": this.maxdist, - "solution_label": "Drop blocks here", - "feedback_cb": this.displayErrors.bind(this) - }); + var options = { + "answerId" : "parsons-answer-" + this.counterId, + "answerRegionId" : "parsons-answerRegion-" + this.counterId, + "sourceId" : "parsons-source-" + this.counterId, + "sourceRegionId" : "parsons-sourceRegion-" + this.counterId, + "codelineId" : "parsons-codeline-" + this.counterId + "-", + "feedbackId" : "parsons-message-" + this.counterId, + "answerLabel" : "Drop blocks here" + }; + // add maxdist and order if present + var maxdist = $(this.origElem).data('maxdist'); + var order = $(this.origElem).data('order'); + var noindent = $(this.origElem).data('noindent'); + if (maxdist !== undefined) { + options["maxdist"] = maxdist; + } + if (order !== undefined) { + // convert order string to array of numbers + order = order.match(/\d+/g); + for (var i = 0; i < order.length; i++) { + order[i] = parseInt(order[i]); + } + options["order"] = order; + } + options["noindent"] = noindent; + this.pwidget = new ParsonsWidget(this, options); this.pwidget.init($pjQ(this.origDiv).text()); - this.pwidget.shuffleLines(); + this.pwidget.resetView(); this.checkServer(); }; -Parsons.prototype.styleNewHTML = function () { - // set min width and height - var sortableul = $("#ul-parsons-sortableCode-" + this.counterId); - var trashul = $("#ul-parsons-sortableTrash-" + this.counterId); - var sortableHeight = sortableul.height(); - var sortableWidth = sortableul.width(); - var trashWidth = trashul.width(); - var trashHeight = trashul.height(); - var minHeight = Math.max(trashHeight, sortableHeight); - var minWidth = Math.max(trashWidth, sortableWidth); - var test = document.getElementById("ul-parsons-sortableTrash-" + this.counterId); - trashul.css("min-height", minHeight + "px"); - sortableul.css("min-height", minHeight + "px"); - sortableul.height(minHeight); - trashul.css("min-width", minWidth + "px"); - sortableul.css("min-width", minWidth + "px"); - test.minWidth = minWidth + "px"; -}; - -Parsons.prototype.displayErrors = function (fb) { // Feedback function - var correct; - if (fb.errors.length > 0) { - correct = "F"; - $(this.messageDiv).fadeIn(500); - $(this.messageDiv).attr("class", "alert alert-danger"); - $(this.messageDiv).html(fb.errors[0]); - } else { - correct = "T"; - $(this.messageDiv).fadeIn(100); - $(this.messageDiv).attr("class", "alert alert-success"); - $(this.messageDiv).html("Perfect!"); - } - // Don't automatically log event on page load - if (!this.loadingFromStorage) { - var answer = this.pwidget.getHash("#ul-parsons-sortableCode-" + this.counterId); - var trash = this.pwidget.getHash("#ul-parsons-sortableTrash-" + this.counterId); - this.logBookEvent({"event": "parsons", "act": "yes", "correct":correct,"answer": answer, "trash": trash, "div_id": this.divid}); - - } - this.loadingFromStorage = false; -}; - Parsons.prototype.checkServer = function () { // Check if the server has stored answer if (this.useRunestoneServices) { @@ -266,10 +229,10 @@ Parsons.prototype.checkServer = function () { data.div_id = this.divid; data.course = eBookConfig.course; data.event = "parsons"; - jQuery.getJSON(eBookConfig.ajaxURL + "getAssessResults", data, this.repopulateFromStorage.bind(this)).error(this.checkLocalStorage.bind(this)).done(this.styleNewHTML.bind(this)); + jQuery.getJSON(eBookConfig.ajaxURL + "getAssessResults", data, this.repopulateFromStorage.bind(this)).error(this.checkLocalStorage.bind(this)).done(this.pwidget.resetView.bind(this)); } else { this.checkLocalStorage(); - this.styleNewHTML(); + this.pwidget.resetView(); } }; @@ -334,9 +297,9 @@ Parsons.prototype.reInitialize = function () { }; Parsons.prototype.setLocalStorage = function() { - var hash = this.pwidget.getHash("#ul-parsons-sortableCode-" + this.counterId); + var hash = this.pwidget.getAnswerHash(); localStorage.setItem(eBookConfig.email + this.divid, hash); - hash = this.pwidget.getHash("#ul-parsons-sortableTrash-" + this.counterId); + hash = this.pwidget.getSourceHash(); localStorage.setItem(eBookConfig.email + this.divid + "-trash", hash); var timeStamp = new Date(); localStorage.setItem(eBookConfig.email + this.divid + "-date", JSON.stringify(timeStamp)); @@ -348,4 +311,4 @@ $(document).bind("runestone:login-complete", function () { prsList[this.id] = new Parsons({"orig": this, "useRunestoneServices": eBookConfig.useRunestoneServices}); } }); -}); +}); \ No newline at end of file diff --git a/runestone/parsons/parsons.py b/runestone/parsons/parsons.py index de967ba2d..891e554b8 100644 --- a/runestone/parsons/parsons.py +++ b/runestone/parsons/parsons.py @@ -37,14 +37,15 @@ def setup(app): app.add_javascript('parsons_setup.js') app.add_javascript('parsons.js') app.add_javascript('parsons-noconflict.js') - app.add_javascript('timedparsons.js') class ParsonsProblem(Assessment): required_arguments = 1 optional_arguments = 1 final_argument_whitespace = False option_spec = { - 'maxdist': directives.unchanged + 'maxdist' : directives.unchanged, + 'order' : directives.unchanged, + 'noindent' : directives.flag } has_content = True @@ -82,16 +83,28 @@ def findmax(alist): """ TEMPLATE = ''' -
    +    
             %(qnumber)s: %(instructions)s%(code)s
    ''' self.options['divid'] = self.arguments[0] self.options['qnumber'] = self.getNumber() self.options['instructions'] = "" self.options['code'] = self.content - - if 'maxdist' not in self.options: - self.options['maxdist'] = '5' + + if 'maxdist' in self.options: + self.options['maxdist'] = ' data-maxdist="' + self.options['maxdist'] + '"' + else: + self.options['maxdist'] = '' + if 'order' in self.options: + self.options['order'] = ' data-order="' + self.options['order'] + '"' + else: + self.options['order'] = '' + if 'noindent' in self.options: + self.options['noindent'] = ' data-noindent="true"' + else: + self.options['noindent'] = '' + + if '-----' in self.content: index = self.content.index('-----') self.options['instructions'] = "\n".join(self.content[:index]) From 17fd6414bd0e9155107cb3546816dae49413d631 Mon Sep 17 00:00:00 2001 From: Jochen Rick Date: Mon, 6 Jun 2016 17:35:48 -0400 Subject: [PATCH 2/8] local storage works again --- runestone/parsons/js/parsons.js | 15 +++++------ runestone/parsons/js/parsons_setup.js | 37 +++++++++++++++++---------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/runestone/parsons/js/parsons.js b/runestone/parsons/js/parsons.js index e455f8a22..920070f7d 100644 --- a/runestone/parsons/js/parsons.js +++ b/runestone/parsons/js/parsons.js @@ -51,9 +51,6 @@ var incorrectIndention = []; for (i = 0; i < solutionLines.length; i++) { if (answerLines[i].viewIndent() !== solutionLines[i].modelIndent()) { - console.log(answerLines[i]); - console.log(answerLines[i].viewIndent()); - console.log(solutionLines[i].modelIndent()); incorrectIndention.push(answerLines[i]); } } @@ -353,11 +350,13 @@ // Create the initial blocks var aBlock, blocks = []; $.each(text.split("\n"), function(index, item) { - aBlock = new ParsonsCodeblock(item, that); - aBlock.index = index; - aBlock.id = id_prefix + index; - aBlock.viewIndent = 0; - blocks.push(aBlock); + if (/\S/.test(item)) { + aBlock = new ParsonsCodeblock(item, that); + aBlock.index = index; + aBlock.id = id_prefix + index; + aBlock.viewIndent = 0; + blocks.push(aBlock); + } }); // Normalize the indents var indents = []; diff --git a/runestone/parsons/js/parsons_setup.js b/runestone/parsons/js/parsons_setup.js index 300d14202..731d383e4 100644 --- a/runestone/parsons/js/parsons_setup.js +++ b/runestone/parsons/js/parsons_setup.js @@ -29,6 +29,13 @@ Parsons.prototype.init = function (opts) { this.origElem = orig; this.useRunestoneServices = opts.useRunestoneServices; this.divid = orig.id; + var storageId = eBookConfig.email; + if (storageId == undefined) { + storageId = this.divid; + } else { + storageId += this.divid; + } + this.storageId = storageId; this.maxdist = $(orig).data('maxdist'); this.children = this.origElem.childNodes; // this contains all of the child elements of the entire tag... this.contentArray = []; @@ -169,9 +176,9 @@ Parsons.prototype.setButtonFunctions = function () { $pjQ(this.checkButt).click(function (event) { event.preventDefault(); var hash = this.pwidget.answerHash(); - localStorage.setItem(this.divid, hash); + localStorage.setItem(this.storageId, hash); hash = this.pwidget.sourceHash(); - localStorage.setItem(this.divid + "-source", hash); + localStorage.setItem(this.storageId + "-source", hash); this.pwidget.getFeedback(); }.bind(this)); }; @@ -229,7 +236,9 @@ Parsons.prototype.checkServer = function () { data.div_id = this.divid; data.course = eBookConfig.course; data.event = "parsons"; - jQuery.getJSON(eBookConfig.ajaxURL + "getAssessResults", data, this.repopulateFromStorage.bind(this)).error(this.checkLocalStorage.bind(this)).done(this.pwidget.resetView.bind(this)); + +// jQuery.getJSON(eBookConfig.ajaxURL + "getAssessResults", data, this.repopulateFromStorage.bind(this)).error(this.checkLocalStorage.bind(this)).done(this.pwidget.resetView.bind(this)); + jQuery.getJSON(eBookConfig.ajaxURL + "getAssessResults", data, this.repopulateFromStorage.bind(this)).error(this.checkLocalStorage.bind(this)).done(this.checkLocalStorage.bind(this)); } else { this.checkLocalStorage(); this.pwidget.resetView(); @@ -242,7 +251,7 @@ Parsons.prototype.repopulateFromStorage = function (data, status, whatever) { if (this.shouldUseServer(data)) { var solution = data.answer; var trash = data.trash; - this.pwidget.createHTMLFromHashes(solution, trash); + this.pwidget.createHTMLFromHashes(trash, solution); this.pwidget.getFeedback(); this.setLocalStorage(); } else { @@ -257,9 +266,9 @@ Parsons.prototype.shouldUseServer = function (data) { // returns true if server data is more recent than local storage or if server storage is correct if (data.correct == "T" || localStorage.length === 0) return true; - var storedAnswer = localStorage.getItem(eBookConfig.email + this.divid); - var storedTrash = localStorage.getItem(eBookConfig.email + this.divid + "-trash"); - var storedDate = localStorage.getItem(eBookConfig.email + this.divid + "-date"); + var storedAnswer = localStorage.getItem(this.storageId); + var storedTrash = localStorage.getItem(this.storageId + "-source"); + var storedDate = localStorage.getItem(this.storageId + "-date"); if (storedAnswer === null || storedTrash === null || storedDate === null) return true; @@ -273,11 +282,11 @@ Parsons.prototype.shouldUseServer = function (data) { return true; }; Parsons.prototype.checkLocalStorage = function () { - if (localStorage.getItem(eBookConfig.email + this.divid) && localStorage.getItem(eBookConfig.email + this.divid + "-trash")) { + if (localStorage.getItem(this.storageId) && localStorage.getItem(this.storageId + "-source")) { try { - var solution = localStorage.getItem(eBookConfig.email + this.divid); - var trash = localStorage.getItem(eBookConfig.email + this.divid + "-trash"); - this.pwidget.createHTMLFromHashes(solution, trash); + var solution = localStorage.getItem(this.storageId); + var trash = localStorage.getItem(this.storageId + "-source"); + this.pwidget.createHTMLFromHashes(trash, solution); if (this.useRunestoneServices) this.loadingFromStorage = false; // Admittedly a non-straightforward way to log, but it works well this.pwidget.getFeedback(); @@ -298,11 +307,11 @@ Parsons.prototype.reInitialize = function () { Parsons.prototype.setLocalStorage = function() { var hash = this.pwidget.getAnswerHash(); - localStorage.setItem(eBookConfig.email + this.divid, hash); + localStorage.setItem(this.storageId, hash); hash = this.pwidget.getSourceHash(); - localStorage.setItem(eBookConfig.email + this.divid + "-trash", hash); + localStorage.setItem(this.storageId + "-source", hash); var timeStamp = new Date(); - localStorage.setItem(eBookConfig.email + this.divid + "-date", JSON.stringify(timeStamp)); + localStorage.setItem(this.storageId + "-date", JSON.stringify(timeStamp)); }; $(document).bind("runestone:login-complete", function () { From 9e99115b9b32edff93b23ea7e9b68de8bd2aa88a Mon Sep 17 00:00:00 2001 From: Jochen Rick Date: Tue, 7 Jun 2016 15:35:35 -0400 Subject: [PATCH 3/8] logging --- CONTRIBUTING.md | 4 +- runestone/parsons/css/parsons.css | 58 ++++++++++++--------------- runestone/parsons/js/parsons.js | 41 +++++++++++++++---- runestone/parsons/js/parsons_setup.js | 14 ++----- 4 files changed, 63 insertions(+), 54 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1ee3ca9f..1bf675668 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,12 +15,12 @@ Coding Standards * All components must remain Python 3/2 compatible. The ``six`` module is already in the requirements.txt file, so feel free to use that. -* No Tabs +* No Tabs for Python files (4 spaces = 1 indention) * Avoid profliferation of jQuery versions. Make your stuff compatible with the version of jQuery in the common folder. * Avoid proliferation of additional third party javascript modules. We are already out of control in this regard and it would be nice to rein it in. - +* When creating a new directive, assign a unique class name to the outermost HTML division. That will allow you to easily confine your CSS declarations to apply only within your directive. Since there are many directives, chances for CSS namespace conflicts are high without that. Provide an example ------------------ diff --git a/runestone/parsons/css/parsons.css b/runestone/parsons/css/parsons.css index afe61412b..348e574d3 100644 --- a/runestone/parsons/css/parsons.css +++ b/runestone/parsons/css/parsons.css @@ -1,4 +1,4 @@ -.sortable-code { +.parsons .sortable-code { position: static; padding-left: 0px; margin: 0px 15px; @@ -6,7 +6,7 @@ text-align: left; vertical-align: top; } -.source, .answer, .answer1, .answer2, .answer3, .answer4 { +.parsons .source, .parsons .answer, .parsons .answer1, .parsons .answer2, .parsons .answer3, .parsons .answer4 { position: relative; font-size: 100%; font-family: monospace; @@ -17,41 +17,41 @@ margin-left: 0; border: 1px solid #efefff; } -.source { +.parsons .source { background-color: #efefff; } -.answer { +.parsons .answer { background-color: #ffa; } -.answer1 { +.parsons .answer1 { background: linear-gradient(#ff7, #ff7) no-repeat border-box; background-size: 30px 100%; background-position: 0 0; background-origin: padding-box; background-color: #ffa; } -.answer2 { +.parsons .answer2 { background: linear-gradient(#ff7, #ff7) no-repeat border-box; background-size: 30px 100%; background-position: 30px 0; background-origin: padding-box; background-color: #ffa; } -.answer3 { +.parsons .answer3 { background: linear-gradient(#ff7, #ff7) no-repeat border-box, linear-gradient(#ff7, #ff7) no-repeat border-box; background-size: 30px 100%, 30px 100%; background-position: 0 0, 60px 0; background-origin: padding-box, padding-box; background-color: #ffa; } -.answer4 { +.parsons .answer4 { background: linear-gradient(#ff7, #ff7) no-repeat border-box, linear-gradient(#ff7, #ff7) no-repeat border-box; background-size: 30px 100%, 30px 100%; background-position: 30px 0, 90px 0; background-origin: padding-box, padding-box; background-color: #ffa; } -.block { +.parsons .block { position: absolute; -moz-border-radius:10px; -webkit-border-radius:10px; @@ -64,77 +64,69 @@ overflow: hidden; cursor: move; } -.block code { +.parsons .block code { display: block; clear: both; float: left; background-color: transparent; } -.block, .block:before, .block:after { +.parsons .block, .parsons .block:before, .parsons .block:after { box-sizing: content-box; } -code.indent1 { +.parsons .indent1 { margin-left: 30px; } -code.indent2 { +.parsons .indent2 { margin-left: 60px; } -code.indent3 { +.parsons .indent3 { margin-left: 90px; } -code.indent4 { +.parsons .indent4 { margin-left: 120px; } -.incorrect { +.parsons .incorrect { border: 1px solid red; background: none; background-color: #ffefef; } - -.correct { +.parsons .correct { background: none; background-color: #efffef; background-color: #DFF2BF; } - -.incorrectIndent { +.parsons .incorrectIndent { border: 1px solid red; border-left: 10px solid red; padding-left: 1px; } - -.correctIndent { +.parsons .correctIndent { border: 1px solid green; border-left: 10px solid green; } - -.incorrectPosition { +.parsons .incorrectPosition { background-color: #FFBABA; border:1px solid red; } - -.correctPosition { +.parsons .correctPosition { background-color: #DFF2BF; border:1px solid green; } -.parsons{ - /*background-color: #fbfcfd; - border-color: #e6e6e6; - box-shadow: inset 0px 0px 6px -1px rgba(50, 50, 50, 0.75);*/ +.parsons { border-left:0; border-right:0; border-radius:0; margin: 10px 0; } -.parsons-text { +.parsons .parsons-text { max-width: 500pt; margin-left: auto; margin-right: auto; } -.sortable-code-container{ +.parsons .sortable-code-container{ text-align:center; } -.parsons-controls{ +.parsons .parsons-controls{ max-width: 500pt; margin-left: auto; margin-right: auto; diff --git a/runestone/parsons/js/parsons.js b/runestone/parsons/js/parsons.js index 920070f7d..e768e4d5f 100644 --- a/runestone/parsons/js/parsons.js +++ b/runestone/parsons/js/parsons.js @@ -22,7 +22,7 @@ // grade that element LineBasedGrader.prototype.grade = function() { var widget = this.widget; - var logAct = widget.answerHash(); + var correct = false; var answerArea = $("#" + widget.options.answerId); var feedbackArea = $("#" + widget.options.feedbackId); var solutionLines = widget.solutionLines(); @@ -60,7 +60,7 @@ feedbackArea.fadeIn(100); feedbackArea.attr("class", "alert alert-success"); feedbackArea.html("Perfect!"); - logAct = "yes"; + correct = true; } else { // Incorrect Indention var incorrectBlocks = []; @@ -109,14 +109,26 @@ feedbackArea.html("Highlighted blocks in your program are wrong or are in the wrong order. This can be fixed by moving, removing, or replacing highlighted blocks."); } } - // log it + // Log It + // this extended format, with correct, answer and trash, is used for grading + var answerHash = widget.answerHash(); + var sourceHash = widget.sourceHash(); + var act = sourceHash + "|" + answerHash; + if (correct) { + act = "correct|" + act; + correct = "yes"; + } else { + act ="incorrect|" + act; + correct = "no"; + } + var divid = widget.problem.divid; widget.problem.logBookEvent({ "event" : "parsons", - "act" : "yes", - "correct" : logAct, - "answer" : widget.answerHash(), - "trash" : widget.sourceHash(), - "div_id" : this.divid + "act" : act, + "div_id" : divid, + "correct" : correct, + "answer" : answerHash, + "trash" : sourceHash }); }; @@ -380,6 +392,7 @@ this.blocks = blocks; this.solution = solution; this.resetView(); + this.log("set"); }; // Create a hash that identifies the block order and indention @@ -444,6 +457,17 @@ this.createView(sourceBlocks, answerBlocks); }; + // Log the activity to the server + ParsonsWidget.prototype.log = function(activity) { + var act = activity + "|" + this.sourceHash + "|" + this.answerHash(); + var divid = this.problem.divid; + this.problem.logBookEvent({ + "event" : "parsons", + "act" : act, + "div_id" : divid + }); + } + ParsonsWidget.prototype.whatWeDidPreviously = function() { var hash = this.answerHash(); var previously = this.states[hash]; @@ -950,6 +974,7 @@ delete that.movingX; delete that.movingY; that.updateView(); + that.log("drop"); } }; diff --git a/runestone/parsons/js/parsons_setup.js b/runestone/parsons/js/parsons_setup.js index 731d383e4..b9b0de708 100644 --- a/runestone/parsons/js/parsons_setup.js +++ b/runestone/parsons/js/parsons_setup.js @@ -171,6 +171,7 @@ Parsons.prototype.setButtonFunctions = function () { $pjQ(this.resetButt).click(function (event) { event.preventDefault(); this.pwidget.resetView(); + this.pwidget.log("reset"); $(this.messageDiv).hide(); }.bind(this)); $pjQ(this.checkButt).click(function (event) { @@ -188,14 +189,6 @@ Parsons.prototype.setButtonFunctions = function () { ================================*/ Parsons.prototype.createParsonsWidget = function () { - // First do animation stuff - $("#parsons-" + this.counterId).not(".sortable-code").not(".parsons-controls").on("click", function () { - $("html, body").animate({ - scrollTop: ($("#parsons-" + this.counterId).offset().top - 50) - }, 700); - }).find(".sortable-code, .parsons-controls").click(function (e) { - return false; - }); var options = { "answerId" : "parsons-answer-" + this.counterId, @@ -237,9 +230,8 @@ Parsons.prototype.checkServer = function () { data.course = eBookConfig.course; data.event = "parsons"; -// jQuery.getJSON(eBookConfig.ajaxURL + "getAssessResults", data, this.repopulateFromStorage.bind(this)).error(this.checkLocalStorage.bind(this)).done(this.pwidget.resetView.bind(this)); - jQuery.getJSON(eBookConfig.ajaxURL + "getAssessResults", data, this.repopulateFromStorage.bind(this)).error(this.checkLocalStorage.bind(this)).done(this.checkLocalStorage.bind(this)); - } else { + jQuery.getJSON(eBookConfig.ajaxURL + "getAssessResults", data, this.repopulateFromStorage.bind(this)).error(this.checkLocalStorage.bind(this)).done(this.pwidget.resetView.bind(this)); + } else { this.checkLocalStorage(); this.pwidget.resetView(); } From 2c64618273b2850260302a0d9b68dcae70bc5319 Mon Sep 17 00:00:00 2001 From: Jochen Rick Date: Wed, 8 Jun 2016 10:43:07 -0400 Subject: [PATCH 4/8] timed Parsons --- runestone/parsons/js/parsons.js | 114 ++++++++++++-------------- runestone/parsons/js/parsons_setup.js | 21 ++++- runestone/parsons/js/timedparsons.js | 42 ++-------- runestone/parsons/parsons.py | 8 +- 4 files changed, 85 insertions(+), 100 deletions(-) diff --git a/runestone/parsons/js/parsons.js b/runestone/parsons/js/parsons.js index e768e4d5f..0e51196f4 100644 --- a/runestone/parsons/js/parsons.js +++ b/runestone/parsons/js/parsons.js @@ -13,11 +13,10 @@ // wrap in anonymous function to not show some helper variables (function($, _) { - var graders = {}; + // An object to grade the Parsons code var LineBasedGrader = function(widget) { this.widget = widget; }; - graders.LineBasedGrader = LineBasedGrader; // grade that element LineBasedGrader.prototype.grade = function() { @@ -161,7 +160,7 @@ // Answer an HTML representation of this codeline ParsonsCodeline.prototype.asHTML = function() { - var html = ' 0; i--) { - s = this.states[this.state_path[i]]; - if (s && outputStepTypes.indexOf(s.type) != -1) { - stepsToLast++; - } - if (hash === this.state_path[i]) { break; } - } - return $.extend(false, {'visits': visits, stepsToLast: stepsToLast}, previously); - }; + "event" : "parsons", + "act" : act, + "div_id" : divid + }); + } // Return a block object by the full id including id prefix ParsonsWidget.prototype.getBlockById = function(id) { @@ -557,9 +534,7 @@ if (this.feedback_exists) { $("#" + this.options.answerId).removeClass("incorrect correct"); var blocks = $("#" + this.options.answerId + " div"); - $.each(this.FEEDBACK_STYLES, function(index, value) { - blocks.removeClass(value); - }); + blocks.removeClass("correctPosition incorrectPosition incorrectIndent"); $("#" + this.options.feedbackId).hide(); } this.feedback_exists = false; @@ -986,6 +961,23 @@ $("#" + answerBlocks[i].id).draggable(draggableOptions); } }; + + // Disable the interface + ParsonsWidget.prototype.disable = function() { + // Disable dragging + var item; + $("#" + this.options.sourceId + " div").each(function(idx, i) { + item = $(i); + item.draggable("destroy"); + }); + $("#" + this.options.answerId + " div").each(function(idx, i) { + item = $(i); + item.draggable("destroy"); + }); + // Hide buttons + $("#" + this.problem.checkButt.id).hide(); + $("#" + this.problem.resetButt.id).hide(); + }; window['ParsonsWidget'] = ParsonsWidget; } diff --git a/runestone/parsons/js/parsons_setup.js b/runestone/parsons/js/parsons_setup.js index b9b0de708..9cb73cdc7 100644 --- a/runestone/parsons/js/parsons_setup.js +++ b/runestone/parsons/js/parsons_setup.js @@ -191,6 +191,7 @@ Parsons.prototype.setButtonFunctions = function () { Parsons.prototype.createParsonsWidget = function () { var options = { + "x_indent" : 30, "answerId" : "parsons-answer-" + this.counterId, "answerRegionId" : "parsons-answerRegion-" + this.counterId, "sourceId" : "parsons-source-" + this.counterId, @@ -214,7 +215,21 @@ Parsons.prototype.createParsonsWidget = function () { } options["order"] = order; } - options["noindent"] = noindent; + options["noindent"] = noindent == "true"; + // add locale and language + var locale = eBookConfig.locale; + if (locale == undefined) { + locale = "en"; + } + options["locale"] = locale; + var language = $(this.origElem).data('language'); + if (language == undefined) { + language = eBookConfig.language; + if (language == undefined) { + language = "python"; + } + } + options["language"] = language; this.pwidget = new ParsonsWidget(this, options); this.pwidget.init($pjQ(this.origDiv).text()); @@ -298,9 +313,9 @@ Parsons.prototype.reInitialize = function () { }; Parsons.prototype.setLocalStorage = function() { - var hash = this.pwidget.getAnswerHash(); + var hash = this.pwidget.answerHash(); localStorage.setItem(this.storageId, hash); - hash = this.pwidget.getSourceHash(); + hash = this.pwidget.sourceHash(); localStorage.setItem(this.storageId + "-source", hash); var timeStamp = new Date(); localStorage.setItem(this.storageId + "-date", JSON.stringify(timeStamp)); diff --git a/runestone/parsons/js/timedparsons.js b/runestone/parsons/js/timedparsons.js index 527342af8..ad3abd157 100644 --- a/runestone/parsons/js/timedparsons.js +++ b/runestone/parsons/js/timedparsons.js @@ -8,46 +8,21 @@ TimedParsons.prototype = new Parsons(); TimedParsons.prototype.timedInit = function (opts) { this.init(opts); - this.renderTimedIcon(this.containerDiv); - this.hideButtons(); -}; - - -TimedParsons.prototype.hideButtons = function () { - $(this.checkButt).hide(); -}; - -TimedParsons.prototype.renderTimedIcon = function (component) { - // renders the clock icon on timed components. The component parameter - // is the element that the icon should be appended to. - var timeIconDiv = document.createElement("div"); - var timeIcon = document.createElement("img"); - $(timeIcon).attr({ - "src": "../_static/clock.png", - "style": "width:15px;height:15px" - }); - timeIconDiv.className = "timeTip"; - timeIconDiv.title = ""; - timeIconDiv.appendChild(timeIcon); - $(component).prepend(timeIconDiv); }; TimedParsons.prototype.checkCorrectTimed = function () { return this.correct ? "T" : "F"; }; -TimedParsons.prototype.hideFeedback = function () { - $(this.messageDiv).hide(); -}; - TimedParsons.prototype.processTimedSubmission = function (logFlag) { // Disable input & evaluate component - this.reInitialize(); if (logFlag) { - var hash = this.pwidget.getHash("#ul-parsons-sortableCode-" + this.counterId); - localStorage.setItem(this.divid, hash); - hash = this.pwidget.getHash("#ul-parsons-sortableTrash-" + this.counterId); - localStorage.setItem(this.divid + "-trash", hash); + var hash = this.pwidget.answerHash(); + localStorage.setItem(this.storageId, hash); + hash = this.pwidget.sourceHash(); + localStorage.setItem(this.storageId + "-source", hash); + var timeStamp = new Date(); + localStorage.setItem(this.storageId + "-date", JSON.stringify(timeStamp)); } else { this.loadingFromStorage = true; } @@ -59,8 +34,5 @@ TimedParsons.prototype.processTimedSubmission = function (logFlag) { } else { this.correct = false; } - - this.resetButt.disabled = true; - $(this.sortContainerDiv).addClass("parsons-disabled"); - + this.pwidget.disable(); }; diff --git a/runestone/parsons/parsons.py b/runestone/parsons/parsons.py index 891e554b8..01c1c39a1 100644 --- a/runestone/parsons/parsons.py +++ b/runestone/parsons/parsons.py @@ -37,6 +37,7 @@ def setup(app): app.add_javascript('parsons_setup.js') app.add_javascript('parsons.js') app.add_javascript('parsons-noconflict.js') + app.add_javascript('timedparsons.js') class ParsonsProblem(Assessment): required_arguments = 1 @@ -45,6 +46,7 @@ class ParsonsProblem(Assessment): option_spec = { 'maxdist' : directives.unchanged, 'order' : directives.unchanged, + 'language' : directives.unchanged, 'noindent' : directives.flag } has_content = True @@ -83,7 +85,7 @@ def findmax(alist): """ TEMPLATE = ''' -
    +    
             %(qnumber)s: %(instructions)s%(code)s
    ''' self.options['divid'] = self.arguments[0] @@ -103,6 +105,10 @@ def findmax(alist): self.options['noindent'] = ' data-noindent="true"' else: self.options['noindent'] = '' + if 'language' in self.options: + self.options['language'] = ' data-language="' + self.options['language'] + '"' + else: + self.options['language'] = '' if '-----' in self.content: From 5b61cb3c6197846c38ba00c6e6c813bf3bf7d3bd Mon Sep 17 00:00:00 2001 From: Jochen Rick Date: Wed, 8 Jun 2016 11:13:22 -0400 Subject: [PATCH 5/8] no text highlighting in widget --- runestone/parsons/css/parsons.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/runestone/parsons/css/parsons.css b/runestone/parsons/css/parsons.css index 348e574d3..39f10d2dc 100644 --- a/runestone/parsons/css/parsons.css +++ b/runestone/parsons/css/parsons.css @@ -125,6 +125,12 @@ } .parsons .sortable-code-container{ text-align:center; + /* Remove text highlighting in widget */ + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: -moz-none; + -o-user-select: none; + user-select: none; } .parsons .parsons-controls{ max-width: 500pt; From c2d1445fdec3709ecf1e3bbc027dce18662d93ff Mon Sep 17 00:00:00 2001 From: Jochen Rick Date: Wed, 8 Jun 2016 15:55:40 -0400 Subject: [PATCH 6/8] somewhat fixed timed rendering bug on reload --- runestone/parsons/js/parsons.js | 49 +++++++++++++++++---------- runestone/parsons/js/parsons_setup.js | 1 - 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/runestone/parsons/js/parsons.js b/runestone/parsons/js/parsons.js index 0e51196f4..d7249ea16 100644 --- a/runestone/parsons/js/parsons.js +++ b/runestone/parsons/js/parsons.js @@ -393,8 +393,6 @@ divs = $(searchString)[0].getElementsByTagName('div'), block; for (var i = 0; i < divs.length; i++) { - console.log(divs[i]); - console.log(divs[i].id); block = this.getBlockById(divs[i].id); if (block !== undefined) { hash.push(block.hash()); @@ -882,19 +880,7 @@ if (window.prettyPrint && (typeof(this.options.prettyPrint) === "undefined" || this.options.prettyPrint)) { prettyPrint(); } - var answerArea = $("#" + this.options.answerId); - var sourceArea = $("#" + this.options.sourceId); - // Establish the width and height of the droppable areas - var areaWidth = 0; - var areaHeight = 6; - var item; - var maxFunction = function(idx, i) { - item = $(i); - areaHeight = areaHeight + item.outerHeight(true); - areaWidth = Math.max(areaWidth, item.outerWidth(true)); - }; - $("#" + this.options.answerId + " div").each(maxFunction); - $("#" + this.options.sourceId + " div").each(maxFunction); + // Determine how much indent should be possible in the answer area var indent; if (this.options.noindent) { @@ -906,18 +892,45 @@ indent = Math.max(indent, this.solution[i].indent); } } + + var answerArea = $("#" + this.options.answerId); + var sourceArea = $("#" + this.options.sourceId); + // Set the size of the areas, but do it only + // otherwise timedparsons will be wrong on reload + var areaWidth, areaHeight; + if (this.areaWidth == undefined) { + // Establish the width and height of the droppable areas + areaWidth = 0; + areaHeight = 6; + var item; + var maxFunction = function(idx, i) { + item = $(i); + areaHeight = areaHeight + item.outerHeight(true); + areaWidth = Math.max(areaWidth, item.outerWidth(true)); + }; + this.areaWidth = areaWidth; + this.areaHeight = areaHeight; + $("#" + this.options.answerId + " div").each(maxFunction); + $("#" + this.options.sourceId + " div").each(maxFunction); + this.answerArea = answerArea; + this.sourceArea = sourceArea; + this.areaWidth = areaWidth; + this.areaHeight = areaHeight; + this.indent = indent; + } else { + areaWidth = this.areaWidth; + areaHeight = this.areaHeight; + } sourceArea.height(areaHeight); sourceArea.width(areaWidth); answerArea.height(areaHeight); answerArea.width(this.options.x_indent * indent + areaWidth); - this.answerArea = answerArea; - this.sourceArea = sourceArea; - this.indent = indent; if (indent > 0 && indent <= 4) { answerArea.addClass("answer" + indent); } else { answerArea.addClass("answer"); } + var that = this; that.state = undefined; // needs to be here for loading from storage that.updateView(); diff --git a/runestone/parsons/js/parsons_setup.js b/runestone/parsons/js/parsons_setup.js index 9cb73cdc7..aec4e7a4e 100644 --- a/runestone/parsons/js/parsons_setup.js +++ b/runestone/parsons/js/parsons_setup.js @@ -233,7 +233,6 @@ Parsons.prototype.createParsonsWidget = function () { this.pwidget = new ParsonsWidget(this, options); this.pwidget.init($pjQ(this.origDiv).text()); - this.pwidget.resetView(); this.checkServer(); }; From 094cbe185cbb4e8c0a123de83499164ae25aa505 Mon Sep 17 00:00:00 2001 From: Jochen Rick Date: Thu, 9 Jun 2016 10:52:28 -0400 Subject: [PATCH 7/8] natural language parsons --- runestone/parsons/css/parsons.css | 35 ++++++++-------- runestone/parsons/js/parsons.js | 66 +++++++++++++++++++------------ 2 files changed, 58 insertions(+), 43 deletions(-) diff --git a/runestone/parsons/css/parsons.css b/runestone/parsons/css/parsons.css index 39f10d2dc..9c0b3e60e 100644 --- a/runestone/parsons/css/parsons.css +++ b/runestone/parsons/css/parsons.css @@ -9,7 +9,6 @@ .parsons .source, .parsons .answer, .parsons .answer1, .parsons .answer2, .parsons .answer3, .parsons .answer4 { position: relative; font-size: 100%; - font-family: monospace; list-style: none; background-color: #efefff; padding-bottom: 10px; @@ -24,31 +23,31 @@ background-color: #ffa; } .parsons .answer1 { - background: linear-gradient(#ff7, #ff7) no-repeat border-box; - background-size: 30px 100%; - background-position: 0 0; + background: linear-gradient(#ee0, #ee0) no-repeat border-box; + background-size: 1px 100%; + background-position: 30px 0; background-origin: padding-box; background-color: #ffa; } .parsons .answer2 { - background: linear-gradient(#ff7, #ff7) no-repeat border-box; - background-size: 30px 100%; - background-position: 30px 0; - background-origin: padding-box; + background: linear-gradient(#ee0, #ee0) no-repeat border-box, linear-gradient(#ee0, #ee0) no-repeat border-box; + background-size: 1px 100%, 1px 100%; + background-position: 30px 0, 60px 0; + background-origin: padding-box, padding-box; background-color: #ffa; } .parsons .answer3 { - background: linear-gradient(#ff7, #ff7) no-repeat border-box, linear-gradient(#ff7, #ff7) no-repeat border-box; - background-size: 30px 100%, 30px 100%; - background-position: 0 0, 60px 0; - background-origin: padding-box, padding-box; + background: linear-gradient(#ee0, #ee0) no-repeat border-box, linear-gradient(#ee0, #ee0) no-repeat border-box, linear-gradient(#ee0, #ee0) no-repeat border-box; + background-size: 1px 100%, 1px 100%, 1px 100%; + background-position: 30px 0, 60px 0, 90px 0; + background-origin: padding-box, padding-box, padding-box; background-color: #ffa; } .parsons .answer4 { - background: linear-gradient(#ff7, #ff7) no-repeat border-box, linear-gradient(#ff7, #ff7) no-repeat border-box; - background-size: 30px 100%, 30px 100%; - background-position: 30px 0, 90px 0; - background-origin: padding-box, padding-box; + background: linear-gradient(#ee0, #ee0) no-repeat border-box, linear-gradient(#ee0, #ee0) no-repeat border-box, linear-gradient(#ee0, #ee0) no-repeat border-box, linear-gradient(#ee0, #ee0) no-repeat border-box; + background-size: 1px 100%, 1px 100%, 1px 100%, 1px 100%; + background-position: 30px 0, 60px 0, 90px 0, 120px 0; + background-origin: padding-box, padding-box, padding-box, padding-box; background-color: #ffa; } .parsons .block { @@ -60,7 +59,6 @@ border: 1px solid lightgray; padding:10px; margin-top: 5px; - white-space: nowrap; overflow: hidden; cursor: move; } @@ -70,6 +68,9 @@ float: left; background-color: transparent; } +.parsons .block p { + margin: 0; +} .parsons .block, .parsons .block:before, .parsons .block:after { box-sizing: content-box; } diff --git a/runestone/parsons/js/parsons.js b/runestone/parsons/js/parsons.js index d7249ea16..4e660bfa5 100644 --- a/runestone/parsons/js/parsons.js +++ b/runestone/parsons/js/parsons.js @@ -160,7 +160,14 @@ // Answer an HTML representation of this codeline ParsonsCodeline.prototype.asHTML = function() { - var html = ' 0) { html += ' indent' + indent; } - html += '">' + this.text + '<\/code>'; + html += '">' + this.text + end; return html; }; @@ -620,7 +627,7 @@ var positionTop, width; var that = this; - var baseWidth = this.sourceArea.width() - 22; + var baseWidth = this.areaWidth - 22; // Update the Source Area if (updateSource) { @@ -683,7 +690,7 @@ if (updateAnswer) { var block, indent; positionTop = 0; - width = this.answerArea.width() - 22; + width = this.areaWidth + this.indent * this.options.x_indent - 22; var that = this; if (newState == "answer") { var hasInserted = false; @@ -756,11 +763,10 @@ // Update the Moving Area if (updateMoving) { moving.appendTo("#" + this.options.sourceId); - width = this.sourceArea.width() - 22; moving.css({ 'left' : this.movingX - this.sourceArea.offset().left - (moving.outerWidth(true) / 2), 'top' : this.movingY - this.sourceArea.offset().top - (movingHeight / 2), - 'width' : width, + 'width' : baseWidth, 'z-index' : 2 }); } @@ -877,54 +883,62 @@ $("#" + this.options.answerRegionId).html(html); } - if (window.prettyPrint && (typeof(this.options.prettyPrint) === "undefined" || this.options.prettyPrint)) { + if (this.prettifyLanguage !== "") { prettyPrint(); } // Determine how much indent should be possible in the answer area - var indent; - if (this.options.noindent) { - indent = 0; - } else { + var indent = 0; + if (!this.options.noindent) { // Set the indent so that the solution is possible - indent = 1; + if (this.options.language !== "natural") { + // Even if no indent is required, have a minimum of 1 indent + indent = 1; + } for (var i = 0; i < this.solution.length; i++) { indent = Math.max(indent, this.solution[i].indent); } } + this.indent = indent; var answerArea = $("#" + this.options.answerId); var sourceArea = $("#" + this.options.sourceId); + this.answerArea = answerArea; + this.sourceArea = sourceArea; // Set the size of the areas, but do it only // otherwise timedparsons will be wrong on reload var areaWidth, areaHeight; if (this.areaWidth == undefined) { // Establish the width and height of the droppable areas - areaWidth = 0; + var item, maxFunction; areaHeight = 6; - var item; - var maxFunction = function(idx, i) { - item = $(i); - areaHeight = areaHeight + item.outerHeight(true); - areaWidth = Math.max(areaWidth, item.outerWidth(true)); - }; - this.areaWidth = areaWidth; - this.areaHeight = areaHeight; + if (this.options.language == "natural") { + areaWidth = 300; + maxFunction = function(idx, i) { + item = $(i); + item.width(areaWidth - 22); + areaHeight += item.outerHeight(true); + }; + } else { + areaWidth = 0; + maxFunction = function(idx, i) { + item = $(i); + areaHeight += item.outerHeight(true); + areaWidth = Math.max(areaWidth, item.outerWidth(true)); + }; + } $("#" + this.options.answerId + " div").each(maxFunction); $("#" + this.options.sourceId + " div").each(maxFunction); - this.answerArea = answerArea; - this.sourceArea = sourceArea; this.areaWidth = areaWidth; this.areaHeight = areaHeight; - this.indent = indent; } else { areaWidth = this.areaWidth; areaHeight = this.areaHeight; } sourceArea.height(areaHeight); - sourceArea.width(areaWidth); + sourceArea.width(areaWidth + 2); answerArea.height(areaHeight); - answerArea.width(this.options.x_indent * indent + areaWidth); + answerArea.width(this.options.x_indent * indent + areaWidth + 2); if (indent > 0 && indent <= 4) { answerArea.addClass("answer" + indent); } else { From 2c1623f8d5ed59d2a6b02e3e8e0593670da9cb6c Mon Sep 17 00:00:00 2001 From: Jochen Rick Date: Thu, 9 Jun 2016 11:21:50 -0400 Subject: [PATCH 8/8] noindent works --- runestone/parsons/js/parsons_setup.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runestone/parsons/js/parsons_setup.js b/runestone/parsons/js/parsons_setup.js index aec4e7a4e..d9fe22b11 100644 --- a/runestone/parsons/js/parsons_setup.js +++ b/runestone/parsons/js/parsons_setup.js @@ -215,7 +215,10 @@ Parsons.prototype.createParsonsWidget = function () { } options["order"] = order; } - options["noindent"] = noindent == "true"; + if (noindent == undefined) { + noindent = false; + } + options["noindent"] = noindent; // add locale and language var locale = eBookConfig.locale; if (locale == undefined) {