diff --git a/app/assets/javascripts/mumuki_laboratory/application/bridge.js b/app/assets/javascripts/mumuki_laboratory/application/bridge.js index 63b01a84c..7e2f9efe3 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/bridge.js +++ b/app/assets/javascripts/mumuki_laboratory/application/bridge.js @@ -1,60 +1,41 @@ /** - * @typedef {{status: string, test_results: [{status: string, title: string}]}} ClientResult + * @typedef {"errored"|"failed"|"passed_with_warnings"|"passed"|"pending"|"aborted"} SubmissionStatus */ /** - * @typedef {{solution: object, client_result?: ClientResult}} Submission + * @typedef {{ + * status: SubmissionStatus, + * test_results: [{status: SubmissionStatus, title: string}] + * }} SubmissionClientResult */ -var mumuki = mumuki || {}; - -(function (mumuki) { - var lastSubmission = {}; - - function Laboratory(exerciseId){ - this.exerciseId = exerciseId; - } - - function asString(json){ - return JSON.stringify(json); - } - - function sameAsLastSolution(newSolution){ - return asString(lastSubmission.content) === asString(newSolution); - } - - function lastSubmissionFinishedSuccessfully(){ - return lastSubmission.result && lastSubmission.result.status !== 'aborted'; - } - - function sendNewSolution(submission){ - var token = new mumuki.CsrfToken(); - var request = token.newRequest({ - type: 'POST', - url: window.location.origin + window.location.pathname + '/solutions' + window.location.search, - data: submission - }); - - return $.ajax(request).then(preRenderResult).done(function (result) { - lastSubmission = { content: {solution: submission.solution}, result: result }; - }); - } +/** + * @typedef {{ + * status: SubmissionStatus, + * class_for_progress_list_item?: string, + * guide_finished_by_solution?: boolean + * }} SubmissionResult + */ +/** + * @typedef {object} Solution + */ - /** - * Pre-renders some html parts of submission UI - * */ - function preRenderResult(result) { - result.class_for_progress_list_item = mumuki.renderers.progressListItemClassForStatus(result.status, true) - return result; - } +/** + * @typedef {{ + * "solution[content]"?:string, + * solution?: Solution, + * client_result?: SubmissionClientResult + * }} Submission + */ - mumuki.load(function () { - lastSubmission = {}; - }); +/** + * @typedef {{submission?: Submission, result?: SubmissionResult}} SubmissionAndResult + */ - Laboratory.prototype = { +mumuki.bridge = (() => { + class Laboratory { // ========== // Public API // ========== @@ -65,9 +46,9 @@ var mumuki = mumuki || {}; * * @param {object} content the content object * */ - runTests: function(content) { + runTests(content) { return this._submitSolution({ solution: content }); - }, + } // =========== // Private API @@ -77,18 +58,46 @@ var mumuki = mumuki || {}; * Sends a solution object * * @param {Submission} submission the submission object + * @returns {JQuery.Promise} */ - _submitSolution: function (submission) { - if(lastSubmissionFinishedSuccessfully() && sameAsLastSolution(submission)){ - return $.Deferred().resolve(lastSubmission.result); + _submitSolution(submission) { + const lastSubmission = mumuki.SubmissionsStore.getCachedResultFor(mumuki.currentExerciseId, submission); + if (lastSubmission) { + return $.Deferred().resolve(lastSubmission); } else { - return sendNewSolution(submission); + return this._sendNewSolution(submission).done((result) => { + mumuki.SubmissionsStore.setLastSubmission(mumuki.currentExerciseId, {submission, result}); + }); } } - }; - mumuki.bridge = { - Laboratory: Laboratory - }; + /** + * @param {Submission} submission the submission object + * @returns {JQuery.Promise} + */ + _sendNewSolution(submission){ + var token = new mumuki.CsrfToken(); + var request = token.newRequest({ + type: 'POST', + url: window.location.origin + window.location.pathname + '/solutions' + window.location.search, + data: submission + }); + return $.ajax(request).then((result) => this._preRenderResult(result)); + } + + /** + * Pre-renders some html parts of submission UI, adding them to the given result + * + * @param {SubmissionResult} result + * @returns {SubmissionResult} + */ + _preRenderResult(result) { + result.class_for_progress_list_item = mumuki.renderers.progressListItemClassForStatus(result.status, true) + return result; + } + } -}(mumuki)); + return { + Laboratory + }; +})(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/current-exercise.js b/app/assets/javascripts/mumuki_laboratory/application/current-exercise.js new file mode 100644 index 000000000..6f8837e20 --- /dev/null +++ b/app/assets/javascripts/mumuki_laboratory/application/current-exercise.js @@ -0,0 +1,13 @@ +/** @type {number} */ +mumuki.currentExerciseId = null; +(() => { + mumuki.load(() => { + // Set global currentExerciseId + const $muExerciseId = $('#mu-exercise-id'); + if ($muExerciseId) { + mumuki.currentExerciseId = Number($muExerciseId.val()); + } else { + mumuki.currentExerciseId = null; + } + }) +})(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js b/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js index 90ff77ba2..bb1b3d7cb 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js +++ b/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js @@ -6,7 +6,7 @@ // ========================== /** - * @param {string} status + * @param {SubmissionStatus} status * @returns {string} */ function iconForStatus(status) { @@ -20,8 +20,7 @@ } /** - * - * @param {string} status + * @param {SubmissionStatus} status * @returns {string} */ function classForStatus(status) { @@ -36,7 +35,7 @@ /** - * @param {string} status + * @param {SubmissionStatus} status * @param {boolean} [active] * @returns {string} */ diff --git a/app/assets/javascripts/mumuki_laboratory/application/submission.js b/app/assets/javascripts/mumuki_laboratory/application/submission.js index fc6b020aa..81357e25d 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/submission.js +++ b/app/assets/javascripts/mumuki_laboratory/application/submission.js @@ -82,6 +82,8 @@ var mumuki = mumuki || {}; * * This method will use CustomEditor's sources if availble, or * standard editor's content sources otherwise + * + * @returns {Submission} */ function getContent() { let content = {}; @@ -97,6 +99,7 @@ var mumuki = mumuki || {}; content[it.name] = it.value; }); + // @ts-ignore return content; } diff --git a/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js b/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js new file mode 100644 index 000000000..f1ed2d703 --- /dev/null +++ b/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js @@ -0,0 +1,85 @@ +mumuki.SubmissionsStore = (() => { + const SubmissionsStore = new class { + /** + * @param {number} exerciseId + * @returns {SubmissionStatus} + */ + getLastSubmissionStatus(exerciseId) { + const submission = this.getLastSubmission(exerciseId); + return submission ? submission.result.status : 'pending'; + } + + /** + * @param {number} exerciseId + * @returns {SubmissionAndResult} + */ + getLastSubmission(exerciseId) { + const submissionAndResult = window.localStorage.getItem(this._keyFor(exerciseId)); + if (!submissionAndResult) return null; + return JSON.parse(submissionAndResult); + } + + /** + * @param {number} exerciseId + * @param {SubmissionAndResult} submissionAndResult + */ + setLastSubmission(exerciseId, submissionAndResult) { + window.localStorage.setItem(this._keyFor(exerciseId), this._asString(submissionAndResult)); + } + + /** + * Retrieves the last cached, non-aborted result for the given submission + * + * @param {number} exerciseId + * @param {Submission} submission + * @returns {SubmissionResult} the cached result for this submission + */ + getCachedResultFor(exerciseId, submission) { + const lastSubmission = this.getLastSubmission(exerciseId); + if (!lastSubmission + || lastSubmission.result.status === 'aborted' + || !this.submissionSolutionEquals(lastSubmission.submission, submission)) { + return null; + } + return lastSubmission.result; + } + + /** + * Extract the submission's solution content + * + * @param {Submission} submission + * @returns {string} + */ + submissionSolutionContent(submission) { + if (submission.solution) { + return submission.solution.content; + } else { + return submission['solution[content]']; + } + } + + /** + * Compares two solutions to determine if they are equivalent + * from the point of view of the code evaluation + * + * @param {Submission} one + * @param {Submission} other + * @returns {boolean} + */ + submissionSolutionEquals(one, other) { + return this.submissionSolutionContent(one) === this.submissionSolutionContent(other); + } + + // private API + + _asString(object) { + return JSON.stringify(object); + } + + _keyFor(exerciseId) { + return `/exercise/${exerciseId}/submission`; + } + }; + + return SubmissionsStore; +})(); diff --git a/app/views/exercises/show.html.erb b/app/views/exercises/show.html.erb index 742b76cbf..782a6f5e9 100644 --- a/app/views/exercises/show.html.erb +++ b/app/views/exercises/show.html.erb @@ -45,6 +45,7 @@ <%= render_exercise_input_layout(@exercise) %> <%= hidden_field_tag default_content_tag_id(@exercise), @default_content %> +<%= hidden_field_tag "mu-exercise-id", @exercise.id %>