diff --git a/.jshintrc b/.jshintrc index 5950318e8..84c4059c0 100644 --- a/.jshintrc +++ b/.jshintrc @@ -7,7 +7,7 @@ "allVisualizers", "console" ], - "esversion": 6, + "esversion": 8, "quotmark": true, "strict": "implied", "sub": true, diff --git a/index.rst b/index.rst index 29ee51628..4aa8134d4 100644 --- a/index.rst +++ b/index.rst @@ -3,6 +3,8 @@ Runestone Components ******************** This site documents the working of the Runestone Components. See the `Runestone Interactive Overview `_ or the `Runestone instructor's guide `_. +Demo linking to the Runestone Server docs: :ref:`assignments/grades_report endpoint`. + Getting started =============== diff --git a/runestone/activecode/js/activecode.js b/runestone/activecode/js/activecode.js index bf60c122b..278213ac1 100755 --- a/runestone/activecode/js/activecode.js +++ b/runestone/activecode/js/activecode.js @@ -470,8 +470,8 @@ export class ActiveCode extends RunestoneBase { if (!didAgree) { didAgree = confirm( "Pair Programming should only be used with the consent of your instructor." + - "Your partner must be a registered member of the class and have agreed to pair with you." + - "By clicking OK you certify that both of these conditions have been met." + "Your partner must be a registered member of the class and have agreed to pair with you." + + "By clicking OK you certify that both of these conditions have been met." ); if (didAgree) { localStorage.setItem("partnerAgree", "true"); @@ -514,13 +514,13 @@ export class ActiveCode extends RunestoneBase { $(butt).attr( "href", "http://" + - chatcodesServer + - "/new?" + - $.param({ - topic: window.location.host + "-" + this.divid, - code: this.editor.getValue(), - lang: "Python", - }) + chatcodesServer + + "/new?" + + $.param({ + topic: window.location.host + "-" + this.divid, + code: this.editor.getValue(), + lang: "Python", + }) ); this.chatButton = butt; chatBar.appendChild(butt); @@ -554,6 +554,8 @@ export class ActiveCode extends RunestoneBase { $(this.runButton).text($.i18n("msg_activecode_save_run")); } + // _`addHistoryScrubber` + // --------------------- // Activecode -- If the code has not changed wrt the scrubber position value then don't save the code or reposition the scrubber // -- still call runlog, but add a parameter to not save the code // add an initial load history button @@ -574,7 +576,7 @@ export class ActiveCode extends RunestoneBase { // If this is timed and already taken we should restore history info this.renderScrubber(); } else { - let request = new Request(eBookConfig.ajaxURL + "gethist.json", { + let request = new Request("/assessment/gethist", { method: "POST", headers: this.jsonHeaders, body: JSON.stringify(reqData), @@ -582,6 +584,10 @@ export class ActiveCode extends RunestoneBase { try { response = await fetch(request); let data = await response.json(); + if (!response.ok) { + throw new Error(`Failed to get the history data: ${data.detail}`); + } + data = data.detail; if (data.history !== undefined) { this.history = this.history.concat(data.history); for (let t in data.timestamps) { @@ -591,7 +597,7 @@ export class ActiveCode extends RunestoneBase { } } } catch (e) { - console.log("unable to fetch history"); + console.log(`unable to fetch history: ${e}`); } this.renderScrubber(pos_last); } @@ -907,7 +913,7 @@ export class ActiveCode extends RunestoneBase { }); } - toggleEditorVisibility() {} + toggleEditorVisibility() { } addErrorMessage(err) { // Add the error message @@ -1045,7 +1051,7 @@ Yet another is that there is an internal error. The internal error message is: var xl = eval(x); xl = xl.map(pyStr); x = xl.join(" "); - } catch (err) {} + } catch (err) { } } } $(this.output).css("visibility", "visible"); @@ -1149,7 +1155,7 @@ Yet another is that there is an internal error. The internal error message is: if ( this.historyScrubber && this.history[$(this.historyScrubber).slider("value")] != - this.editor.getValue() + this.editor.getValue() ) { saveCode = "True"; this.history.push(this.editor.getValue()); @@ -1186,7 +1192,7 @@ Yet another is that there is an internal error. The internal error message is: this.logRunEvent({ div_id: this.divid, code: this.editor.getValue(), - lang: this.language, + language: this.language, errinfo: this.errinfo, to_save: this.saveCode, prefix: this.pretext, diff --git a/runestone/activecode/js/activecode_sql.js b/runestone/activecode/js/activecode_sql.js index 43777d850..d0befa1c5 100644 --- a/runestone/activecode/js/activecode_sql.js +++ b/runestone/activecode/js/activecode_sql.js @@ -238,7 +238,7 @@ export default class SQLActiveCode extends ActiveCode { this.logRunEvent({ div_id: this.divid, code: this.editor.getValue(), - lang: this.language, + language: this.language, errinfo: this.results[this.results.length - 1].status, to_save: this.saveCode, prefix: this.pretext, @@ -292,9 +292,8 @@ export default class SQLActiveCode extends ActiveCode { } let pct = (100 * this.passed) / (this.passed + this.failed); pct = pct.toLocaleString(undefined, { maximumFractionDigits: 2 }); - result += `You passed ${this.passed} out of ${ - this.passed + this.failed - } tests for ${pct}%`; + result += `You passed ${this.passed} out of ${this.passed + this.failed + } tests for ${pct}%`; this.unit_results = `percent:${pct}:passed:${this.passed}:failed:${this.failed}`; return result; } diff --git a/runestone/clickableArea/js/clickable.js b/runestone/clickableArea/js/clickable.js index 9329fe901..3a0b19327 100644 --- a/runestone/clickableArea/js/clickable.js +++ b/runestone/clickableArea/js/clickable.js @@ -400,9 +400,11 @@ export default class ClickableArea extends RunestoneBase { } logCurrentAnswer() { + const answer = this.givenIndexArray.join(";"); this.logBookEvent({ event: "clickableArea", - act: this.givenIndexArray.join(";"), + answer: answer, + act: answer, div_id: this.divid, correct: this.correct ? "T" : "F", }); diff --git a/runestone/common/css/runestone-custom-sphinx-bootstrap.css b/runestone/common/css/runestone-custom-sphinx-bootstrap.css index a5b65f95f..a8c5f80a4 100644 --- a/runestone/common/css/runestone-custom-sphinx-bootstrap.css +++ b/runestone/common/css/runestone-custom-sphinx-bootstrap.css @@ -863,7 +863,7 @@ ul.dropdown-menu.globaltoc { /* Style lp textareas. */ -textarea#lp-result { +textarea.lp-result { width: 100%; height: 10em; font-family: monospace; diff --git a/runestone/common/js/bookfuncs.js b/runestone/common/js/bookfuncs.js index 977c4f775..3a887c841 100644 --- a/runestone/common/js/bookfuncs.js +++ b/runestone/common/js/bookfuncs.js @@ -105,8 +105,7 @@ function addReadingList() { }); } else { l = $("
", { - text: - "This page is not part of the last reading assignment you visited.", + text: "This page is not part of the last reading assignment you visited.", }); } $("#main-content").append(l); @@ -206,12 +205,26 @@ class PageProgressBar { export var pageProgressTracker = {}; -function handlePageSetup() { +async function handlePageSetup() { var mess; - if (eBookConfig.useRunestoneServices) { - jQuery.get(eBookConfig.ajaxURL + "set_tz_offset", { - timezoneoffset: new Date().getTimezoneOffset() / 60, - }); + let headers = new Headers({ + "Content-type": "application/json; charset=utf-8", + Accept: "application/json", + }); + let data = { timezoneoffset: new Date().getTimezoneOffset() / 60 }; + let request = new Request("/logger/set_tz_offset", { + method: "POST", + body: JSON.stringify(data), + headers: headers, + }); + try { + let response = await fetch(request); + if (!response.ok) { + console.error(`Failed to set timezone! ${response.statusText}`); + } + data = await response.json(); + } catch (e) { + console.error(`Error setting timezone ${e}`); } if (eBookConfig.isLoggedIn) { diff --git a/runestone/common/js/runestonebase.js b/runestone/common/js/runestonebase.js index 619e8474e..2d9b2e23b 100644 --- a/runestone/common/js/runestonebase.js +++ b/runestone/common/js/runestonebase.js @@ -20,7 +20,9 @@ import { pageProgressTracker } from "./bookfuncs.js"; export default class RunestoneBase { constructor(opts) { - this.component_ready_promise = new Promise(resolve => this._component_ready_resolve_fn = resolve) + this.component_ready_promise = new Promise( + (resolve) => (this._component_ready_resolve_fn = resolve) + ); this.optional = false; if (opts) { this.sid = opts.sid; @@ -57,7 +59,7 @@ export default class RunestoneBase { // is to look for doAssignment in the URL and then grab // the assignment name from the heading. if (location.href.indexOf("doAssignment") >= 0) { - this.timedWrapper = $("h1#assignment_name").text() + this.timedWrapper = $("h1#assignment_name").text(); } else { this.timedWrapper = null; } @@ -72,24 +74,22 @@ export default class RunestoneBase { }); } - // .. _logBookEvent: - // - // logBookEvent - // ------------ + // _`logBookEvent` + //---------------- // This function sends the provided ``eventInfo`` to the `hsblog endpoint` of the server. Awaiting this function returns either ``undefined`` (if Runestone services are not available) or the data returned by the server as a JavaScript object (already JSON-decoded). async logBookEvent(eventInfo) { if (this.graderactive) { return; } let post_return; - eventInfo.course = eBookConfig.course; + eventInfo.course_name = eBookConfig.course; eventInfo.clientLoginStatus = eBookConfig.isLoggedIn; eventInfo.timezoneoffset = new Date().getTimezoneOffset() / 60; if (this.percent) { eventInfo.percent = this.percent; } if (eBookConfig.useRunestoneServices && eBookConfig.logLevel > 0) { - let request = new Request(eBookConfig.ajaxURL + "hsblog", { + let request = new Request("/logger/bookevent", { method: "POST", headers: this.jsonHeaders, body: JSON.stringify(eventInfo), @@ -97,12 +97,17 @@ export default class RunestoneBase { try { let response = await fetch(request); if (!response.ok) { - throw new Error("Failed to save the log entry"); + let detail = await response.json(); + console.error(detail); + throw new Error(`Failed to save the log entry ${detail}`); + } else { + post_return = response.json(); } - post_return = response.json(); } catch (e) { if (this.isTimed) { - alert(`Error: Your action was not saved! The error was ${e}`); + alert( + `Error: Your action was not saved! The error was ${e}` + ); } console.log(`Error: ${e}`); } @@ -120,10 +125,8 @@ export default class RunestoneBase { return post_return; } - // .. _logRunEvent: - // - // logRunEvent - // ----------- + // -`logRunEvent` + //--------------- // This function sends the provided ``eventInfo`` to the `runlog endpoint`. When awaited, this function returns the data (decoded from JSON) the server sent back. async logRunEvent(eventInfo) { let post_promise = "done"; @@ -137,7 +140,7 @@ export default class RunestoneBase { eventInfo.save_code = "True"; } if (eBookConfig.useRunestoneServices && eBookConfig.logLevel > 0) { - let request = new Request(eBookConfig.ajaxURL + "runlog.json", { + let request = new Request("/logger/runlog", { method: "POST", headers: this.jsonHeaders, body: JSON.stringify(eventInfo), @@ -192,19 +195,23 @@ export default class RunestoneBase { data.sid = this.sid; } if (!eBookConfig.practice_mode && this.assessmentTaken) { - let request = new Request( - eBookConfig.ajaxURL + "getAssessResults", - { - method: "POST", - body: JSON.stringify(data), - headers: this.jsonHeaders, - } - ); + let request = new Request("/assessment/results", { + method: "POST", + body: JSON.stringify(data), + headers: this.jsonHeaders, + }); try { let response = await fetch(request); - data = await response.json(); - this.repopulateFromStorage(data); - this.csresolver("server"); + if (response.ok) { + data = await response.json(); + data = data.detail; + this.repopulateFromStorage(data); + this.csresolver("server"); + } else { + alert( + `HTTP Error getting results: ${response.statusText}` + ); + } } catch (err) { try { this.checkLocalStorage(); @@ -252,7 +259,7 @@ export default class RunestoneBase { */ repopulateFromStorage(data) { // decide whether to use the server's answer (if there is one) or to load from storage - if (data !== null && this.shouldUseServer(data)) { + if (data !== null && data !== "no data" && this.shouldUseServer(data)) { this.restoreAnswers(data); this.setLocalStorage(data); } else { diff --git a/runestone/common/js/user-highlights.js b/runestone/common/js/user-highlights.js index 68d73283a..4be2cdba5 100644 --- a/runestone/common/js/user-highlights.js +++ b/runestone/common/js/user-highlights.js @@ -25,13 +25,13 @@ function getCompletions() { var data = { lastPageUrl: currentPathname }; jQuery .ajax({ - url: eBookConfig.ajaxURL + "getCompletionStatus", + url: "/logger/getCompletionStatus", data: data, async: false, }) .done(function (data) { if (data != "None") { - var completionData = $.parseJSON(data); + var completionData = data.detail; var completionClass, completionMsg; if (completionData[0].completionStatus == 1) { completionClass = "buttonConfirmCompletion"; @@ -43,10 +43,10 @@ function getCompletions() { } $("#main-content").append( '
" + completionClass + + '" id="completionButton">' + + completionMsg + + "
" ); } }); @@ -57,8 +57,8 @@ function showLastPositionBanner() { if (typeof lastPositionVal !== "undefined") { $("body").append( '' + parseInt(lastPositionVal) + + 'px;"/>' ); $("html, body").animate({ scrollTop: parseInt(lastPositionVal) }, 1000); } @@ -122,6 +122,9 @@ function addNavigationAndCompletionButtons() { } else { completionFlag = 1; } + // Make sure we mark this page as visited regardless of how flakey + // the onunload handlers become. + processPageState(completionFlag); $("#completionButton").on("click", function () { if ($(this).hasClass("buttonAskCompletion")) { $(this) @@ -158,17 +161,19 @@ function addNavigationAndCompletionButtons() { }); } +// _ decorateTableOfContents +// ------------------------- function decorateTableOfContents() { if ( window.location.href.toLowerCase().indexOf("toc.html") != -1 || window.location.href.toLowerCase().indexOf("index.html") != -1 ) { - jQuery.get(eBookConfig.ajaxURL + "getAllCompletionStatus", function ( + jQuery.get("/logger/getAllCompletionStatus", function ( data ) { var subChapterList; if (data != "None") { - subChapterList = $.parseJSON(data); + subChapterList = data.detail; var allSubChapterURLs = $("#main-content div li a"); $.each(subChapterList, function (index, item) { @@ -183,8 +188,8 @@ function decorateTableOfContents() { .addClass("completed") .append( '- Completed this topic on ' + - item.endDate + - "" + item.endDate + + "" ) .children() .first() @@ -205,8 +210,8 @@ function decorateTableOfContents() { .addClass("active") .append( 'Last read this topic on ' + - item.endDate + - "" + item.endDate + + "" ) .children() .first() @@ -229,25 +234,25 @@ function decorateTableOfContents() { } }); var data = { course: eBookConfig.course }; - jQuery.get(eBookConfig.ajaxURL + "getlastpage", data, function (data) { + jQuery.get("/logger/getlastpage", data, function (data) { var lastPageData; if (data != "None") { - lastPageData = $.parseJSON(data); - if (lastPageData[0].lastPageChapter != null) { + lastPageData = data.detail; + if (lastPageData.lastPageChapter != null) { $("#continue-reading") .show() .html( '
You were Last Reading: ' + - lastPageData[0].lastPageChapter + - (lastPageData[0].lastPageSubchapter - ? " > " + - lastPageData[0].lastPageSubchapter - : "") + - ' Continue Reading
' + lastPageData.lastPageChapter + + (lastPageData.lastPageSubchapter + ? " > " + + lastPageData.lastPageSubchapter + : "") + + ' Continue Reading' ); } } @@ -265,6 +270,8 @@ function enableCompletions() { // call enable user highlights after login $(document).bind("runestone:login", enableCompletions); +// _ processPageState +// ------------------------- function processPageState(completionFlag) { /*Log last page visited*/ var currentPathname = window.location.pathname; @@ -285,8 +292,11 @@ function processPageState(completionFlag) { console.log(e); }); jQuery.ajax({ - url: eBookConfig.ajaxURL + "updatelastpage", - data: data, + url: "/logger/updatelastpage", + contentType: "application/json; charset=utf-8", + dataType: "json", + data: JSON.stringify(data), + method: "POST", async: true, }); } diff --git a/runestone/common/project_template/_templates/plugin_layouts/sphinx_bootstrap/layout.html b/runestone/common/project_template/_templates/plugin_layouts/sphinx_bootstrap/layout.html index 77d19456b..0dadc54ff 100644 --- a/runestone/common/project_template/_templates/plugin_layouts/sphinx_bootstrap/layout.html +++ b/runestone/common/project_template/_templates/plugin_layouts/sphinx_bootstrap/layout.html @@ -1,7 +1,7 @@ {% extends "basic/layout.html" %} {% if dynamic_pages == 'True' %} - {% set appname = '{{ =request.application }}' %} + {% set appname = '{{ request.application }}' %} {% endif %} @@ -41,7 +41,7 @@ {% if dynamic_pages == 'True' %} {% raw %} - {{ =course_name }} + {{ course_name }} {% endraw %} {% else %} {% if course_title -%}{{ course_title|e }}{%- else -%}{{ course_id|e }}{%- endif -%} @@ -234,18 +234,18 @@ {% raw %} eBookConfig.useRunestoneServices = true; eBookConfig.host = ''; - eBookConfig.app = eBookConfig.host + '/' + '{{= request.application }}'; - eBookConfig.course = '{{= course_name }}'; - eBookConfig.basecourse = '{{= base_course }}'; - eBookConfig.isLoggedIn = {{= is_logged_in}}; - eBookConfig.email = '{{= user_email }}'; - eBookConfig.isInstructor = {{= is_instructor }}; - eBookConfig.username = '{{= user_id}}'; - eBookConfig.readings = {{= readings}}; - eBookConfig.activities = {{= XML(activity_info) }} - eBookConfig.downloadsEnabled = {{=downloads_enabled}}; - eBookConfig.allow_pairs = {{=allow_pairs}} - eBookConfig.enableCompareMe = {{=enable_compare_me }}; + eBookConfig.app = eBookConfig.host + '/' + '{{ request.application }}'; + eBookConfig.course = '{{ course_name }}'; + eBookConfig.basecourse = '{{ base_course }}'; + eBookConfig.isLoggedIn = {{ is_logged_in}}; + eBookConfig.email = '{{ user_email }}'; + eBookConfig.isInstructor = {{ is_instructor }}; + eBookConfig.username = '{{ user_id}}'; + eBookConfig.readings = {{ readings}}; + eBookConfig.activities = {{ activity_info }} + eBookConfig.downloadsEnabled = {{downloads_enabled}}; + eBookConfig.allow_pairs = {{allow_pairs}} + eBookConfig.enableCompareMe = {{enable_compare_me}}; {% endraw %} {% else %} eBookConfig.useRunestoneServices = {% if use_services == 'true' -%}true{%- else -%}false{%- endif -%}; @@ -274,9 +274,9 @@ {% if dynamic_pages == 'True' %} {% raw %} - {{ if response.serve_ad and settings.adsenseid: }} - - {{ pass }} + {% if settings.serve_ad and settings.adsenseid %} + + {% endif %} {% endraw %} {% endif %} @@ -299,16 +299,16 @@ {% if dynamic_pages == 'True' %} {% raw %} - {{ if settings.num_banners > 0 and settings.show_rs_banner: }} + {% if settings.num_banners > 0 and settings.show_rs_banner %}
- + Please Support Runestone
- {{ pass }} + {% endif %} {% endraw %} {% endif %} @@ -359,10 +359,10 @@ {% if dynamic_pages == 'True' %} {% raw %} - {{ if request.application == 'runestone':}} + {% if request.application == 'runestone' %} - {{ pass }} + {% endif %} {% endraw %} {% if minimal_outside_links != 'True' %} diff --git a/runestone/common/project_template/_templates/plugin_layouts/sphinx_bootstrap/subchaptoc.html b/runestone/common/project_template/_templates/plugin_layouts/sphinx_bootstrap/subchaptoc.html index 5f0c3639d..145058256 100644 --- a/runestone/common/project_template/_templates/plugin_layouts/sphinx_bootstrap/subchaptoc.html +++ b/runestone/common/project_template/_templates/plugin_layouts/sphinx_bootstrap/subchaptoc.html @@ -1,14 +1,19 @@ - + diff --git a/runestone/dragndrop/js/dragndrop.js b/runestone/dragndrop/js/dragndrop.js index 54fdd4ed4..a179bac18 100644 --- a/runestone/dragndrop/js/dragndrop.js +++ b/runestone/dragndrop/js/dragndrop.js @@ -403,7 +403,7 @@ export default class DragNDrop extends RunestoneBase { event: "dragNdrop", act: answer, answer: answer, - minHeight: this.minheight, + min_height: this.minheight, div_id: this.divid, correct: this.correct, correctNum: this.correctNum, @@ -454,7 +454,7 @@ export default class DragNDrop extends RunestoneBase { restoreAnswers(data) { // Restore answers from storage retrieval done in RunestoneBase this.hasStoredDropzones = true; - this.minheight = data.minHeight; + this.minheight = data.min_height; this.pregnantIndexArray = data.answer.split(";"); this.finishSettingUp(); } @@ -471,7 +471,7 @@ export default class DragNDrop extends RunestoneBase { this.hasStoredDropzones = true; try { storedObj = JSON.parse(ex); - this.minheight = storedObj.minHeight; + this.minheight = storedObj.min_height; } catch (err) { // error while parsing; likely due to bad value stored in storage console.log(err.message); @@ -488,7 +488,7 @@ export default class DragNDrop extends RunestoneBase { event: "dragNdrop", act: answer, answer: answer, - minHeight: this.minheight, + min_height: this.minheight, div_id: this.divid, correct: storedObj.correct, }); @@ -522,7 +522,7 @@ export default class DragNDrop extends RunestoneBase { var correct = data.correct; var storageObj = { answer: this.pregnantIndexArray.join(";"), - minHeight: this.minheight, + min_height: this.minheight, timestamp: timeStamp, correct: correct, }; diff --git a/runestone/fitb/fitb.py b/runestone/fitb/fitb.py index 1112d00d3..8aabd609c 100644 --- a/runestone/fitb/fitb.py +++ b/runestone/fitb/fitb.py @@ -141,6 +141,7 @@ class FillInTheBlank(RunestoneIdDirective): option_spec = RunestoneIdDirective.option_spec.copy() option_spec.update( { + "dynamic": directives.unchanged, "casei": directives.flag, # case insensitive matching } ) @@ -180,8 +181,17 @@ def run(self): self.updateContent() - self.state.nested_parse(self.content, self.content_offset, fitbNode) + # Process dynamic problem content. env = self.state.document.settings.env + dynamic = self.options.get("dynamic") + if dynamic: + # Make sure we're server-side. + if not env.config.runestone_server_side_grading: + raise self.error("Dynamic problems require server-side grading.") + # Add in a header to set up the RNG. + fitbNode.template_start = "{{{{ random = get_random({})\n exec({}) }}}}\n".format(repr(self.options["divid"]), repr(dynamic)) + fitbNode.template_start + + self.state.nested_parse(self.content, self.content_offset, fitbNode) self.options["divclass"] = env.config.fitb_div_class # Expected _`structure`, with assigned variable names and transformations made: @@ -211,17 +221,21 @@ def run(self): # self.feedbackArray = [ # [ # blankArray # { # blankFeedbackDict: feedback 1 - # "regex" : feedback_field_name # (An answer, as a regex; - # "regexFlags" : "x" # "i" if ``:casei:`` was specified, otherwise "".) OR - # "number" : [min, max] # a range of correct numeric answers. - # "feedback": feedback_field_body (after being rendered as HTML) # Provides feedback for this answer. + # "regex" : feedback_field_name, # (An answer, as a regex; + # "regexFlags" : "x", # "i" if ``:casei:`` was specified, otherwise "".) OR + # "number" : [min, max], # a range of correct numeric answers OR + # "solution_code" : source_code, # (For dynamic problems -- the dynamically-computed answer. + # "dynamic_code" : source_code, # The first blank also contains setup code.) + # "feedback": feedback_field_body, (after being rendered as HTML) # Provides feedback for this answer. # }, # { # Feedback 2 # Same as above. # } # ], # [ # Blank 2, same as above. - # ] + # ], + # ..., + # [dynamic_source] # For dynamic problems only. # ] # # ...and a transformed node structure: @@ -263,47 +277,54 @@ def run(self): feedback_field_name = feedback_field[0] assert isinstance(feedback_field_name, nodes.field_name) feedback_field_name_raw = feedback_field_name.rawsource - # See if this is a number, optinonally followed by a tolerance. - try: - # Parse the number. In Python 3 syntax, this would be ``str_num, *list_tol = feedback_field_name_raw.split()``. - tmp = feedback_field_name_raw.split() - str_num = tmp[0] - list_tol = tmp[1:] - num = ast.literal_eval(str_num) - assert isinstance(num, Number) - # If no tolerance is given, use a tolarance of 0. - if len(list_tol) == 0: - tol = 0 - else: - assert len(list_tol) == 1 - tol = ast.literal_eval(list_tol[0]) - assert isinstance(tol, Number) - # We have the number and a tolerance. Save that. - blankFeedbackDict = {"number": [num - tol, num + tol]} - except (SyntaxError, ValueError, AssertionError): - # We can't parse this as a number, so assume it's a regex. - regex = ( - # The given regex must match the entire string, from the beginning (which may be preceded by whitespaces) ... - r"^\s*" - + - # ... to the contents (where a single space in the provided pattern is treated as one or more whitespaces in the student's answer) ... - feedback_field_name.rawsource.replace(" ", r"\s+") - # ... to the end (also with optional spaces). - + r"\s*$" - ) - blankFeedbackDict = { - "regex": regex, - "regexFlags": "i" if "casei" in self.options else "", - } - # Test out the regex to make sure it compiles without an error. + # Simply store the solution code for a dynamic problem. + if dynamic: + blankFeedbackDict = {"solution_code": feedback_field_name_raw} + # For the first blank, also include the dynamic source code. + if not blankArray: + blankFeedbackDict["dynamic_code"] = dynamic + else: + # See if this is a number, optionally followed by a tolerance. try: - re.compile(regex) - except Exception as ex: - raise self.error( - 'Error when compiling regex "{}": {}.'.format( - regex, str(ex) - ) + # Parse the number. In Python 3 syntax, this would be ``str_num, *list_tol = feedback_field_name_raw.split()``. + tmp = feedback_field_name_raw.split() + str_num = tmp[0] + list_tol = tmp[1:] + num = ast.literal_eval(str_num) + assert isinstance(num, Number) + # If no tolerance is given, use a tolerance of 0. + if len(list_tol) == 0: + tol = 0 + else: + assert len(list_tol) == 1 + tol = ast.literal_eval(list_tol[0]) + assert isinstance(tol, Number) + # We have the number and a tolerance. Save that. + blankFeedbackDict = {"number": [num - tol, num + tol]} + except (SyntaxError, ValueError, AssertionError): + # We can't parse this as a number, so assume it's a regex. + regex = ( + # The given regex must match the entire string, from the beginning (which may be preceded by whitespaces) ... + r"^\s*" + + + # ... to the contents (where a single space in the provided pattern is treated as one or more whitespaces in the student's answer) ... + feedback_field_name.rawsource.replace(" ", r"\s+") + # ... to the end (also with optional spaces). + + r"\s*$" ) + blankFeedbackDict = { + "regex": regex, + "regexFlags": "i" if "casei" in self.options else "", + } + # Test out the regex to make sure it compiles without an error. + try: + re.compile(regex) + except Exception as ex: + raise self.error( + 'Error when compiling regex "{}": {}.'.format( + regex, str(ex) + ) + ) blankArray.append(blankFeedbackDict) feedback_field_body = feedback_field[1] diff --git a/runestone/fitb/js/fitb.js b/runestone/fitb/js/fitb.js index df280139e..6cb5dca1e 100644 --- a/runestone/fitb/js/fitb.js +++ b/runestone/fitb/js/fitb.js @@ -217,6 +217,7 @@ export default class FITB extends RunestoneBase { correct: this.correct ? "T" : "F", div_id: this.divid, }); + data = data.detail; if (!this.feedbackArray) { // On success, update the feedback from the server's grade. this.setLocalStorage({ @@ -229,7 +230,7 @@ export default class FITB extends RunestoneBase { this.renderFeedback(); } return data; -} + } /*============================== === Evaluation of answer and === @@ -341,19 +342,20 @@ export default class FITB extends RunestoneBase { enableCompareButton() { this.compareButton.disabled = false; } + // _`compareFITBAnswers` compareFITBAnswers() { var data = {}; data.div_id = this.divid; data.course = eBookConfig.course; jQuery.get( - eBookConfig.ajaxURL + "gettop10Answers", + "/assessment/gettop10Answers", data, this.compareFITB ); } compareFITB(data, status, whatever) { - var answers = eval(data)[0]; - var misc = eval(data)[1]; + var answers = data.detail.res; + var misc = data.detail.miscdata; var body = ""; body += ""; for (var row in answers) { @@ -365,12 +367,6 @@ export default class FITB extends RunestoneBase { " times"; } body += "
AnswerCount
"; - if (misc["yourpct"] !== "unavailable") { - body += - "

You have " + - misc["yourpct"] + - "% correct for all questions

"; - } var html = "