From 043c2dac49fad3a62e2c0276fe1fc6b855dfcd3c Mon Sep 17 00:00:00 2001 From: Franco Bulgarelli Date: Mon, 14 Oct 2019 15:51:12 -0300 Subject: [PATCH 01/20] WIP --- .../mumuki_laboratory/application.js | 10 --- .../mumuki_laboratory/application/bridge.js | 84 +++++++++++++++---- .../application/load-analytics.js | 4 - app/views/exercises/show.html.erb | 2 + app/views/layouts/_progress_bar.html.erb | 8 +- app/views/layouts/application.html.erb | 24 ------ spec/dummy/config/environments/development.rb | 2 +- vendor/assets/javascripts/hotjar.js | 8 -- 8 files changed, 80 insertions(+), 62 deletions(-) delete mode 100644 app/assets/javascripts/mumuki_laboratory/application/load-analytics.js delete mode 100644 vendor/assets/javascripts/hotjar.js diff --git a/app/assets/javascripts/mumuki_laboratory/application.js b/app/assets/javascripts/mumuki_laboratory/application.js index 98007c6374..d4a3ac974a 100644 --- a/app/assets/javascripts/mumuki_laboratory/application.js +++ b/app/assets/javascripts/mumuki_laboratory/application.js @@ -15,23 +15,13 @@ //= require moment/pt.js //= require webcomponents-lite //= require rails-ujs -//= require turbolinks //= require mumuki-styles -//= require nprogress -//= require nprogress-turbolinks -//= require nprogress-ajax //= require jquery-console //= require codemirror.min //= require codemirror-simple-mode.js //= require codemirror-autorefresh //= require codemirror-modes -//= require analytics -//= require hotjar //= require muvment //= require_tree ./application - -NProgress.configure({ - showSpinner: false -}); diff --git a/app/assets/javascripts/mumuki_laboratory/application/bridge.js b/app/assets/javascripts/mumuki_laboratory/application/bridge.js index 87fb7bdd5b..f9177beb56 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/bridge.js +++ b/app/assets/javascripts/mumuki_laboratory/application/bridge.js @@ -3,8 +3,8 @@ var mumuki = mumuki || {}; (function (mumuki) { var lastSubmission = {}; - function Laboratory(exerciseId){ - this.exerciseId = exerciseId; + function Laboratory(){ + this.exerciseId = $('#mu-exercise-id')[0].value; } function asString(json){ @@ -19,21 +19,77 @@ var mumuki = mumuki || {}; return lastSubmission.result && lastSubmission.result.status !== 'aborted'; } - function sendNewSolution(solution){ - var token = new mumuki.CsrfToken(); - var request = token.newRequest({ - type: 'POST', - url: window.location.origin + window.location.pathname + '/solutions' + window.location.search, - data: solution - }); - - return $.ajax(request).done(function (result) { - lastSubmission = { content: solution, result: result }; - }); + function progressListItemClassForStatus(status) { + return `progress-list-item text-center ${classForStatus(status)}`; + } + + function classForStatus(status) { + switch (status) { + case "passed": return "success"; + case "failed": return "danger"; + case "passed_with_warnings": return "warning"; + case "errored": return "broken"; + } + } + + function messageForStatus(status) { + switch (status) { + case "passed": return "¡Muy bien! Tu solución pasó todas las pruebas"; + case "failed": return "Tu solución no pasó las pruebas"; + case "passed_with_warnings": return "Tu solución funcionó, pero hay cosas que mejorar"; + case "errored": return "¡Ups! Tu solución no se puede ejecutar"; + } + } + + function titleHtmlForStatus(status) { + return `

\n ${messageForStatus(status)}\n

\n`; + } + + function sendNewSolution(exerciseId, solution) { + const status = "failed"; // FIXME hardcoded + const result = { + "status":status, + "guide_finished_by_solution": false, + "class_for_progress_list_item": progressListItemClassForStatus(status), + "html":"\n
\n \n
\n\n \n \n
\n

Como ves, para que el tractor siembre lechuga, tenemos que decirle a Mukinita que ponga 2 bolitas verdes. Y en este ejemplo acabamos de sembrar una hilera de 4 lechugas.

\n

Pero fue un poco difícil entenderlo, ¿no?

\n

\n
\n \n", + "title_html": titleHtmlForStatus(status), + "button_html":"
\n
\n
\n \n
\n
\n
\n", + "expectations_html":"", + "remaining_attempts_html":null, + "test_results":[ + { + "title":null, + "status":"failed", + "result":"\n\n\n

\n Se obtuvo un tablero distinto al esperado.\n

\n\n\n
\n\n \n
\n

Tablero inicial

\n

\n \n \n GBB/1.0\nsize 4 2\nhead 0 0\n\n \n \n

\n
\n \n
\n

Tablero final esperado

\n

\n \n \n GBB/1.0\nsize 4 2\ncell 0 0 Verde 2\ncell 1 0 Verde 2\ncell 2 0 Verde 2\ncell 3 0 Verde 2\nhead 3 0\n\n \n \n

\n
\n \n
\n

Tablero final obtenido

\n

\n \n \n GBB/1.0\nsize 4 2\ncell 0 0 Verde 1\nhead 0 0\n\n \n \n

\n
\n \n\n
\n\n\n" + } + ] + }; + lastSubmission = { content: solution, result: result }; + window.localStorage.setItem(localStorageExerciseKey(exerciseId), JSON.stringify(lastSubmission)); + result.class_for_progress_list_item += ' active'; + return Promise.resolve(result); + } + + function localStorageExerciseKey(exerciseId) { + return `/exercise/${exerciseId}/submission`; + } + + function lastSubmissionForExercise(exerciseId) { + return window.localStorage.getItem(localStorageExerciseKey(exerciseId)); + + } + + function updateProgressListItemClass(a) { + const submission = lastSubmissionForExercise(a.data('mu-exercise-id')); + + if (submission) { + a.attr('class', JSON.parse(submission).result.class_for_progress_list_item); + } } mumuki.load(function () { lastSubmission = {}; + $('.progress-list-item').each((_, it) => updateProgressListItemClass($(it))); }); Laboratory.prototype = { @@ -56,7 +112,7 @@ var mumuki = mumuki || {}; if(lastSubmissionFinishedSuccessfully() && sameAsLastSolution(solution)){ return $.Deferred().resolve(lastSubmission.result); } else { - return sendNewSolution(solution); + return sendNewSolution(this.exerciseId, solution); } }, }; diff --git a/app/assets/javascripts/mumuki_laboratory/application/load-analytics.js b/app/assets/javascripts/mumuki_laboratory/application/load-analytics.js deleted file mode 100644 index 51a9af449a..0000000000 --- a/app/assets/javascripts/mumuki_laboratory/application/load-analytics.js +++ /dev/null @@ -1,4 +0,0 @@ -mumuki.load(function(){ - ga('create', 'UA-58353823-1', 'auto'); - ga('send', 'pageview'); -}); diff --git a/app/views/exercises/show.html.erb b/app/views/exercises/show.html.erb index 12791b36f1..83c6988bec 100644 --- a/app/views/exercises/show.html.erb +++ b/app/views/exercises/show.html.erb @@ -45,6 +45,8 @@ <%= render_exercise_input_layout(@exercise) %> <%= hidden_field_tag default_content_tag_id(@exercise), @default_content %> +<%= hidden_field_tag "mu-exercise-id", @exercise.id %> +<%= hidden_field_tag "mu-exercise-resource", @exercise.to_resource_h.to_json %> <% end %> diff --git a/spec/dummy/config/environments/development.rb b/spec/dummy/config/environments/development.rb index 1c9aaa5be3..19feb89b1a 100644 --- a/spec/dummy/config/environments/development.rb +++ b/spec/dummy/config/environments/development.rb @@ -40,7 +40,7 @@ # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. - config.assets.debug = true + config.assets.debug = false # Suppress logger output for asset requests. config.assets.quiet = true diff --git a/vendor/assets/javascripts/hotjar.js b/vendor/assets/javascripts/hotjar.js deleted file mode 100644 index dc0ad2f807..0000000000 --- a/vendor/assets/javascripts/hotjar.js +++ /dev/null @@ -1,8 +0,0 @@ -(function(h,o,t,j,a,r){ - h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)}; - h._hjSettings={hjid:646165,hjsv:6}; - a=o.getElementsByTagName('head')[0]; - r=o.createElement('script');r.async=1; - r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv; - a.appendChild(r); -})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv='); From fa85250f03a9cc3e9b75a1585bc02ac047921b73 Mon Sep 17 00:00:00 2001 From: Franco Bulgarelli Date: Tue, 15 Oct 2019 18:14:15 -0300 Subject: [PATCH 02/20] Refactor: introducing SubmissionsStore --- .../mumuki_laboratory/application/bridge.js | 108 ++++-------------- .../mumuki_laboratory/application/progress.js | 43 ++++++- .../application/submissions-store.js | 43 +++++++ 3 files changed, 102 insertions(+), 92 deletions(-) create mode 100644 app/assets/javascripts/mumuki_laboratory/application/submissions-store.js diff --git a/app/assets/javascripts/mumuki_laboratory/application/bridge.js b/app/assets/javascripts/mumuki_laboratory/application/bridge.js index f9177beb56..84c62d7d13 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/bridge.js +++ b/app/assets/javascripts/mumuki_laboratory/application/bridge.js @@ -1,38 +1,5 @@ -var mumuki = mumuki || {}; - -(function (mumuki) { - var lastSubmission = {}; - - function Laboratory(){ - this.exerciseId = $('#mu-exercise-id')[0].value; - } - - 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 progressListItemClassForStatus(status) { - return `progress-list-item text-center ${classForStatus(status)}`; - } - - function classForStatus(status) { - switch (status) { - case "passed": return "success"; - case "failed": return "danger"; - case "passed_with_warnings": return "warning"; - case "errored": return "broken"; - } - } - - function messageForStatus(status) { +(() => { + function _messageForStatus(status) { switch (status) { case "passed": return "¡Muy bien! Tu solución pasó todas las pruebas"; case "failed": return "Tu solución no pasó las pruebas"; @@ -41,84 +8,53 @@ var mumuki = mumuki || {}; } } - function titleHtmlForStatus(status) { - return `

\n ${messageForStatus(status)}\n

\n`; + function _titleHtmlForStatus(status) { + return `

\n ${_messageForStatus(status)}\n

\n`; } - function sendNewSolution(exerciseId, solution) { - const status = "failed"; // FIXME hardcoded + function _sendNewSolution(exerciseId, solution) { + const status = "passed"; // FIXME hardcoded const result = { - "status":status, + "status": status, "guide_finished_by_solution": false, - "class_for_progress_list_item": progressListItemClassForStatus(status), "html":"\n
\n \n
\n\n \n \n
\n

Como ves, para que el tractor siembre lechuga, tenemos que decirle a Mukinita que ponga 2 bolitas verdes. Y en este ejemplo acabamos de sembrar una hilera de 4 lechugas.

\n

Pero fue un poco difícil entenderlo, ¿no?

\n

\n
\n \n", - "title_html": titleHtmlForStatus(status), + "title_html": _titleHtmlForStatus(status), "button_html":"
\n
\n
\n \n
\n
\n
\n", "expectations_html":"", "remaining_attempts_html":null, - "test_results":[ - { - "title":null, - "status":"failed", - "result":"\n\n\n

\n Se obtuvo un tablero distinto al esperado.\n

\n\n\n
\n\n \n
\n

Tablero inicial

\n

\n \n \n GBB/1.0\nsize 4 2\nhead 0 0\n\n \n \n

\n
\n \n
\n

Tablero final esperado

\n

\n \n \n GBB/1.0\nsize 4 2\ncell 0 0 Verde 2\ncell 1 0 Verde 2\ncell 2 0 Verde 2\ncell 3 0 Verde 2\nhead 3 0\n\n \n \n

\n
\n \n
\n

Tablero final obtenido

\n

\n \n \n GBB/1.0\nsize 4 2\ncell 0 0 Verde 1\nhead 0 0\n\n \n \n

\n
\n \n\n
\n\n\n" - } - ] + "test_results":[] }; lastSubmission = { content: solution, result: result }; - window.localStorage.setItem(localStorageExerciseKey(exerciseId), JSON.stringify(lastSubmission)); - result.class_for_progress_list_item += ' active'; + mumuki.SubmissionsStore.setLastSubmission(exerciseId, lastSubmission); return Promise.resolve(result); } - function localStorageExerciseKey(exerciseId) { - return `/exercise/${exerciseId}/submission`; - } - - function lastSubmissionForExercise(exerciseId) { - return window.localStorage.getItem(localStorageExerciseKey(exerciseId)); - - } - - function updateProgressListItemClass(a) { - const submission = lastSubmissionForExercise(a.data('mu-exercise-id')); - - if (submission) { - a.attr('class', JSON.parse(submission).result.class_for_progress_list_item); - } - } - - mumuki.load(function () { - lastSubmission = {}; - $('.progress-list-item').each((_, it) => updateProgressListItemClass($(it))); - }); - - Laboratory.prototype = { - + class Laboratory { // ========== // Public API // ========== // Runs tests for the current exercise using the given submission // content. - runTests: function(content) { + runTests(content) { return this._submitSolution({ solution: content }); - }, + } // =========== // Private API // =========== - _submitSolution: function (solution) { - if(lastSubmissionFinishedSuccessfully() && sameAsLastSolution(solution)){ - return $.Deferred().resolve(lastSubmission.result); - } else { - return sendNewSolution(this.exerciseId, solution); + _submitSolution(solution) { + const cachedSolution = mumuki.SubmissionsStore.getCachedResultFor(mumuki.currentExerciseId, solution); + if(cachedSolution){ + return $.Deferred().resolve(cachedSolution); } - }, - }; + + return _sendNewSolution(mumuki.currentExerciseId, solution); + } + } mumuki.bridge = { Laboratory: Laboratory }; - -}(mumuki)); +})(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/progress.js b/app/assets/javascripts/mumuki_laboratory/application/progress.js index 0a0dce3645..f511688697 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/progress.js +++ b/app/assets/javascripts/mumuki_laboratory/application/progress.js @@ -1,10 +1,41 @@ -var mumuki = mumuki || {}; +(() => { + // Updates the current exercise progress indicator + mumuki.updateProgressBarAndShowModal = function (submission) { + $('.progress-list-item.active').attr('class', mumuki.progressListItemClassForStatus(submission.status, true)); + if(submission.guide_finished_by_solution) $('#guide-done').modal(); + }; -(function (mumuki) { + mumuki.progressListItemClassForStatus = function (status, active = false) { + return `progress-list-item text-center ${mumuki.classForStatus(status)} ${active ? 'active' : ''}`; + }; - mumuki.updateProgressBarAndShowModal = function (data) { - $('.progress-list-item.active').attr('class', data.class_for_progress_list_item); - if(data.guide_finished_by_solution) $('#guide-done').modal(); + mumuki.classForStatus = function (status) { + switch (status) { + case "passed": return "success"; + case "failed": return "danger"; + case "passed_with_warnings": return "warning"; + case "errored": return "broken"; + case "pending": return "muted"; + } }; -})(mumuki); + function _updateProgressListItemClass(a) { + const exerciseId = a.data('mu-exercise-id'); + const status = mumuki.SubmissionsStore.getLastSubmissionStatus(exerciseId); + + a.attr('class', mumuki.progressListItemClassForStatus(status, exerciseId == mumuki.currentExerciseId)); + } + + mumuki.load(() => { + // Set global currentExerciseId + const $muExerciseId = $('#mu-exercise-id')[0]; + if ($muExerciseId) { + mumuki.currentExerciseId = $muExerciseId.value; + } else { + mumuki.currentExerciseId = null; + } + + // Update all exercises progress indicators + $('.progress-list-item').each((_, it) => _updateProgressListItemClass($(it))); + }) +})(); 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 0000000000..e6d22f8fd2 --- /dev/null +++ b/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js @@ -0,0 +1,43 @@ +(() => { + const SubmissionsStore = new class { + getLastSubmissionStatus(exerciseId) { + const submission = this.getLastSubmission(exerciseId); + return submission ? submission.result.status : 'pending'; + } + + getLastSubmission(exerciseId) { + const submission = window.localStorage.getItem(this._keyFor(exerciseId)); + if (!submission) return null; + return JSON.parse(submission); + } + + setLastSubmission(exerciseId, submission) { + window.localStorage.setItem(this._keyFor(exerciseId), this._asString(submission)); + } + + getCachedResultFor(exerciseId, newSolution) { + const lastSubmission = this.getLastSubmission(exerciseId); + if (!lastSubmission + || lastSubmission.result.status === 'aborted' + || !this._solutionEquals(lastSubmission, newSolution)) { + return null; + } + return lastSubmission.result; + } + + // private API + + _asString(object) { + return JSON.stringify(object); + } + + _keyFor(exerciseId) { + return `/exercise/${exerciseId}/submission`; + } + + _solutionEquals(submission, solution) { + return this._asString(submission.content) === this._asString(solution); + } + }; + mumuki.SubmissionsStore = SubmissionsStore; +})(); From 87e07aa6df7e2c44e3a49d8683b044d15b8c4c67 Mon Sep 17 00:00:00 2001 From: Franco Bulgarelli Date: Wed, 16 Oct 2019 00:38:22 -0300 Subject: [PATCH 03/20] First draft of corollary generation on client side --- .../mumuki_laboratory/application/bridge.js | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/mumuki_laboratory/application/bridge.js b/app/assets/javascripts/mumuki_laboratory/application/bridge.js index 84c62d7d13..8fa04709d5 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/bridge.js +++ b/app/assets/javascripts/mumuki_laboratory/application/bridge.js @@ -1,15 +1,49 @@ (() => { function _messageForStatus(status) { - switch (status) { - case "passed": return "¡Muy bien! Tu solución pasó todas las pruebas"; - case "failed": return "Tu solución no pasó las pruebas"; + switch (status) { // FIXME i18n + case "errored": return "¡Ups! Tu solución no se puede ejecutar"; + case "failed": return "Tu solución no pasó las pruebas"; case "passed_with_warnings": return "Tu solución funcionó, pero hay cosas que mejorar"; - case "errored": return "¡Ups! Tu solución no se puede ejecutar"; + case "passed": return "¡Muy bien! Tu solución pasó todas las pruebas"; + } + } + + function _iconForStatus(status) { + switch (status) { + case "errored": return "fa-minus-circle"; + case "failed": return "fa-times-circle"; + case "passed_with_warnings": return "fa-exclamation-circle"; + case "passed": return "fa-check-circle"; } } function _titleHtmlForStatus(status) { - return `

\n ${_messageForStatus(status)}\n

\n`; + return `

${_messageForStatus(status)}

`; + } + + function _closeModalButtonHtml() { + const keepLearning = "¡Seguí aprendiendo!"; // FIXME i18n + return ``; + } + + function _retryButtonHtml() { + const retryMessage = "Reintentar"; // FIXME i18n + return ``; + } + + function _nextExerciseButton() { + return _closeModalButtonHtml(); // TODO render next button + } + + function _buttonHtmlForStatus(status) { + return ` +
+
+
+ ${status === 'passed' ? _nextExerciseButton() : _retryButtonHtml()} +
+
+
`; } function _sendNewSolution(exerciseId, solution) { @@ -17,9 +51,9 @@ const result = { "status": status, "guide_finished_by_solution": false, - "html":"\n
\n \n
\n\n \n \n
\n

Como ves, para que el tractor siembre lechuga, tenemos que decirle a Mukinita que ponga 2 bolitas verdes. Y en este ejemplo acabamos de sembrar una hilera de 4 lechugas.

\n

Pero fue un poco difícil entenderlo, ¿no?

\n

\n
\n \n", + "html":"

Como ves, para que el tractor siembre lechuga, tenemos que decirle a Mukinita que ponga 2 bolitas verdes. Y en este ejemplo acabamos de sembrar una hilera de 4 lechugas.

Pero fue un poco difícil entenderlo, ¿no?

", "title_html": _titleHtmlForStatus(status), - "button_html":"
\n
\n
\n \n
\n
\n
\n", + "button_html":_buttonHtmlForStatus(status), "expectations_html":"", "remaining_attempts_html":null, "test_results":[] From 153bbfa44e01a1133a9a2fdb030b72097b6fb0aa Mon Sep 17 00:00:00 2001 From: Franco Bulgarelli Date: Wed, 16 Oct 2019 14:59:46 -0300 Subject: [PATCH 04/20] Introducing connection modes --- .../mumuki_laboratory/application/bridge.js | 101 ++++++------------ .../application/confirmation.js | 2 +- .../application/connection.js | 34 ++++++ .../mumuki_laboratory/application/console.js | 2 +- .../application/current-exercise.js | 11 ++ .../mumuki_laboratory/application/kids.js | 2 +- .../application/offline-runner.js | 14 +++ .../mumuki_laboratory/application/polyfill.js | 4 + .../application/progress-bar.js | 16 +++ .../mumuki_laboratory/application/progress.js | 41 ------- .../application/results-renderer.js | 60 +++++++++++ .../application/submission.js | 4 +- 12 files changed, 174 insertions(+), 117 deletions(-) create mode 100644 app/assets/javascripts/mumuki_laboratory/application/connection.js create mode 100644 app/assets/javascripts/mumuki_laboratory/application/current-exercise.js create mode 100644 app/assets/javascripts/mumuki_laboratory/application/offline-runner.js create mode 100644 app/assets/javascripts/mumuki_laboratory/application/polyfill.js create mode 100644 app/assets/javascripts/mumuki_laboratory/application/progress-bar.js delete mode 100644 app/assets/javascripts/mumuki_laboratory/application/progress.js create mode 100644 app/assets/javascripts/mumuki_laboratory/application/results-renderer.js diff --git a/app/assets/javascripts/mumuki_laboratory/application/bridge.js b/app/assets/javascripts/mumuki_laboratory/application/bridge.js index 8fa04709d5..f4cf1fc49f 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/bridge.js +++ b/app/assets/javascripts/mumuki_laboratory/application/bridge.js @@ -1,68 +1,4 @@ (() => { - function _messageForStatus(status) { - switch (status) { // FIXME i18n - case "errored": return "¡Ups! Tu solución no se puede ejecutar"; - case "failed": return "Tu solución no pasó las pruebas"; - case "passed_with_warnings": return "Tu solución funcionó, pero hay cosas que mejorar"; - case "passed": return "¡Muy bien! Tu solución pasó todas las pruebas"; - } - } - - function _iconForStatus(status) { - switch (status) { - case "errored": return "fa-minus-circle"; - case "failed": return "fa-times-circle"; - case "passed_with_warnings": return "fa-exclamation-circle"; - case "passed": return "fa-check-circle"; - } - } - - function _titleHtmlForStatus(status) { - return `

${_messageForStatus(status)}

`; - } - - function _closeModalButtonHtml() { - const keepLearning = "¡Seguí aprendiendo!"; // FIXME i18n - return ``; - } - - function _retryButtonHtml() { - const retryMessage = "Reintentar"; // FIXME i18n - return ``; - } - - function _nextExerciseButton() { - return _closeModalButtonHtml(); // TODO render next button - } - - function _buttonHtmlForStatus(status) { - return ` -
-
-
- ${status === 'passed' ? _nextExerciseButton() : _retryButtonHtml()} -
-
-
`; - } - - function _sendNewSolution(exerciseId, solution) { - const status = "passed"; // FIXME hardcoded - const result = { - "status": status, - "guide_finished_by_solution": false, - "html":"

Como ves, para que el tractor siembre lechuga, tenemos que decirle a Mukinita que ponga 2 bolitas verdes. Y en este ejemplo acabamos de sembrar una hilera de 4 lechugas.

Pero fue un poco difícil entenderlo, ¿no?

", - "title_html": _titleHtmlForStatus(status), - "button_html":_buttonHtmlForStatus(status), - "expectations_html":"", - "remaining_attempts_html":null, - "test_results":[] - }; - lastSubmission = { content: solution, result: result }; - mumuki.SubmissionsStore.setLastSubmission(exerciseId, lastSubmission); - return Promise.resolve(result); - } - class Laboratory { // ========== // Public API @@ -71,20 +7,43 @@ // Runs tests for the current exercise using the given submission // content. runTests(content) { - return this._submitSolution({ solution: content }); + return this.runCurrentExerciseSolution({ solution: content }); + } + + // Runs the current exercise solution, trying to get the response from cache first + runCurrentExerciseSolution(solution) { + const cachedSolution = mumuki.SubmissionsStore.getCachedResultFor(mumuki.currentExerciseId, solution); + if(cachedSolution) { + return $.Deferred().resolve(cachedSolution); + } + return this._runNewSolution(mumuki.currentExerciseId, solution); + } + + // Actually sends the solution to server + submitCurrentExerciseSolution(_exerciseId, solution) { // TODO use exerciseId instead of window.location + const token = new mumuki.CsrfToken(); + const request = token.newRequest({ + type: 'POST', + url: window.location.origin + window.location.pathname + '/solutions' + window.location.search, + data: solution + }); + return $.ajax(request); } // =========== // Private API // =========== - _submitSolution(solution) { - const cachedSolution = mumuki.SubmissionsStore.getCachedResultFor(mumuki.currentExerciseId, solution); - if(cachedSolution){ - return $.Deferred().resolve(cachedSolution); - } + _runNewSolution(exerciseId, solution){ + const responsePromise = mumuki.Connection.runNewSolution(exerciseId, solution); + return responsePromise.done((result) => { + + result.title_html = mumuki.titleHtmlForStatus(status); // TODO defer rendering calculation + result.button_html = mumuki.buttonHtmlForStatus(status); // TODO defer rendering calculation - return _sendNewSolution(mumuki.currentExerciseId, solution); + const lastSubmission = { content: solution, result: result }; + mumuki.SubmissionsStore.setLastSubmission(exerciseId, lastSubmission); + }); } } diff --git a/app/assets/javascripts/mumuki_laboratory/application/confirmation.js b/app/assets/javascripts/mumuki_laboratory/application/confirmation.js index 42eec4525d..c6dc315719 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/confirmation.js +++ b/app/assets/javascripts/mumuki_laboratory/application/confirmation.js @@ -7,7 +7,7 @@ mumuki.load(function () { url: $(this).data('confirmation-url'), xhrFields: {withCredentials: true}, success: function(data){ - mumuki.updateProgressBarAndShowModal(data); + mumuki.updateCurrentExerciseProgressBarAndShowModal(data); } })); diff --git a/app/assets/javascripts/mumuki_laboratory/application/connection.js b/app/assets/javascripts/mumuki_laboratory/application/connection.js new file mode 100644 index 0000000000..261144bd21 --- /dev/null +++ b/app/assets/javascripts/mumuki_laboratory/application/connection.js @@ -0,0 +1,34 @@ +(() => { + + const OfflineMode = new class { + // Runs solution by evaluating it locally + runNewSolution(exerciseId, solution, bridge) { + return mumuki.runSolutionLocally(exerciseId, solution); + } + + // Renders progress from SubmissionsStore + renderExercisesProgressBar() { + $('.progress-list-item').each((_, it) => this._updateProgressListItemClass($(it))); + } + + _updateProgressListItemClass(a) { + const exerciseId = a.data('mu-exercise-id'); + const status = mumuki.SubmissionsStore.getLastSubmissionStatus(exerciseId); + a.attr('class', mumuki.progressListItemClassForStatus(status, exerciseId == mumuki.currentExerciseId)); + } + } + + const OnlineMode = new class { + // Runs solution by sending it to server + runNewSolution(exerciseId, solution, _bridge) { + return bridge.submitCurrentExerciseSolution(exerciseId, solution); + } + + // Does nothing. Progress is rendered by server + renderExercisesProgressBar() { + } + } + + // mumuki.Connection = OnlineMode; + mumuki.Connection = OfflineMode; +})(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/console.js b/app/assets/javascripts/mumuki_laboratory/application/console.js index c8fa97758b..31b8d9b2a7 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/console.js +++ b/app/assets/javascripts/mumuki_laboratory/application/console.js @@ -116,7 +116,7 @@ var mumuki = mumuki || {}; $('.submission-results').show(); $('.submission-results').html(response.html); mumuki.pin.scroll(); - mumuki.updateProgressBarAndShowModal(response); + mumuki.updateCurrentExerciseProgressBarAndShowModal(response); } else { $('.submission-results').hide(); $('.progress-list-item.active').attr('class', "progress-list-item text-center danger active"); 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 0000000000..2f31389b05 --- /dev/null +++ b/app/assets/javascripts/mumuki_laboratory/application/current-exercise.js @@ -0,0 +1,11 @@ +(() => { + mumuki.load(() => { + // Set global currentExerciseId + const $muExerciseId = $('#mu-exercise-id')[0]; + if ($muExerciseId) { + mumuki.currentExerciseId = $muExerciseId.value; + } else { + mumuki.currentExerciseId = null; + } + }) +})(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/kids.js b/app/assets/javascripts/mumuki_laboratory/application/kids.js index 9c712bbd62..2b2821b031 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/kids.js +++ b/app/assets/javascripts/mumuki_laboratory/application/kids.js @@ -110,7 +110,7 @@ mumuki.load(function () { // This method needs to be called by the runner's editor.html extension // in order to finish an exercise showResult: function (data) { - mumuki.updateProgressBarAndShowModal(data); + mumuki.updateCurrentExerciseProgressBarAndShowModal(data); if (data.guide_finished_by_solution) return; mumuki.kids.resultAction[data.status](data); }, diff --git a/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js b/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js new file mode 100644 index 0000000000..3dfffa0ccb --- /dev/null +++ b/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js @@ -0,0 +1,14 @@ +(() => { + mumuki.runSolutionLocally = function (exerciseId, solution) { + const status = "passed"; // FIXME hardcoded + const result = { + "status": status, + "guide_finished_by_solution": false, + "html":"

Como ves, para que el tractor siembre lechuga, tenemos que decirle a Mukinita que ponga 2 bolitas verdes. Y en este ejemplo acabamos de sembrar una hilera de 4 lechugas.

Pero fue un poco difícil entenderlo, ¿no?

", + "expectations_html":"", + "remaining_attempts_html":null, + "test_results":[] + }; + return Promise.resolve(result) + } +}); diff --git a/app/assets/javascripts/mumuki_laboratory/application/polyfill.js b/app/assets/javascripts/mumuki_laboratory/application/polyfill.js new file mode 100644 index 0000000000..98e4cfbf16 --- /dev/null +++ b/app/assets/javascripts/mumuki_laboratory/application/polyfill.js @@ -0,0 +1,4 @@ +(() => { + // Allow polimorphism between standard promise and jquery promise + Promise.prototype.done = Promise.prototype.finally; +}) diff --git a/app/assets/javascripts/mumuki_laboratory/application/progress-bar.js b/app/assets/javascripts/mumuki_laboratory/application/progress-bar.js new file mode 100644 index 0000000000..c4f06bdd65 --- /dev/null +++ b/app/assets/javascripts/mumuki_laboratory/application/progress-bar.js @@ -0,0 +1,16 @@ +(() => { + // Updates the current exercise progress indicator + mumuki.updateCurrentExerciseProgressBarAndShowModal = function (submission) { + $('.progress-list-item.active').attr('class', mumuki.progressListItemClassForStatus(submission.status, true)); + if(submission.guide_finished_by_solution) $('#guide-done').modal(); + }; + + mumuki.progressListItemClassForStatus = function (status, active = false) { + return `progress-list-item text-center ${mumuki.classForStatus(status)} ${active ? 'active' : ''}`; + }; + + mumuki.load(() => { + // Update all exercises progress indicators + mumuki.Connection.renderExercisesProgressBar(); + }) +})(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/progress.js b/app/assets/javascripts/mumuki_laboratory/application/progress.js deleted file mode 100644 index f511688697..0000000000 --- a/app/assets/javascripts/mumuki_laboratory/application/progress.js +++ /dev/null @@ -1,41 +0,0 @@ -(() => { - // Updates the current exercise progress indicator - mumuki.updateProgressBarAndShowModal = function (submission) { - $('.progress-list-item.active').attr('class', mumuki.progressListItemClassForStatus(submission.status, true)); - if(submission.guide_finished_by_solution) $('#guide-done').modal(); - }; - - mumuki.progressListItemClassForStatus = function (status, active = false) { - return `progress-list-item text-center ${mumuki.classForStatus(status)} ${active ? 'active' : ''}`; - }; - - mumuki.classForStatus = function (status) { - switch (status) { - case "passed": return "success"; - case "failed": return "danger"; - case "passed_with_warnings": return "warning"; - case "errored": return "broken"; - case "pending": return "muted"; - } - }; - - function _updateProgressListItemClass(a) { - const exerciseId = a.data('mu-exercise-id'); - const status = mumuki.SubmissionsStore.getLastSubmissionStatus(exerciseId); - - a.attr('class', mumuki.progressListItemClassForStatus(status, exerciseId == mumuki.currentExerciseId)); - } - - mumuki.load(() => { - // Set global currentExerciseId - const $muExerciseId = $('#mu-exercise-id')[0]; - if ($muExerciseId) { - mumuki.currentExerciseId = $muExerciseId.value; - } else { - mumuki.currentExerciseId = null; - } - - // Update all exercises progress indicators - $('.progress-list-item').each((_, it) => _updateProgressListItemClass($(it))); - }) -})(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js b/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js new file mode 100644 index 0000000000..79e2a60b06 --- /dev/null +++ b/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js @@ -0,0 +1,60 @@ +(() => { + // TODO temporary module + + function _messageForStatus(status) { + switch (status) { // FIXME i18n + case "errored": return "¡Ups! Tu solución no se puede ejecutar"; + case "failed": return "Tu solución no pasó las pruebas"; + case "passed_with_warnings": return "Tu solución funcionó, pero hay cosas que mejorar"; + case "passed": return "¡Muy bien! Tu solución pasó todas las pruebas"; + } + } + + function _iconForStatus(status) { + switch (status) { + case "errored": return "fa-minus-circle"; + case "failed": return "fa-times-circle"; + case "passed_with_warnings": return "fa-exclamation-circle"; + case "passed": return "fa-check-circle"; + } + } + + function _closeModalButtonHtml() { + const keepLearning = "¡Seguí aprendiendo!"; // FIXME i18n + return ``; + } + + function _retryButtonHtml() { + const retryMessage = "Reintentar"; // FIXME i18n + return ``; + } + + function _nextExerciseButton() { + return `Siguiente Ejercicio `; // TODO missing exercise title + } + + mumuki.classForStatus = function (status) { + switch (status) { + case "passed": return "success"; + case "failed": return "danger"; + case "passed_with_warnings": return "warning"; + case "errored": return "broken"; + case "pending": return "muted"; + } + }; + + mumuki.titleHtmlForStatus = function (status) { + return `

${_messageForStatus(status)}

`; + }; + + mumuki.buttonHtmlForStatus = function () { + return ` +
+
+
+ ${status === 'passed' ? _nextExerciseButton() : _retryButtonHtml()} +
+
+
`; + }; +}); diff --git a/app/assets/javascripts/mumuki_laboratory/application/submission.js b/app/assets/javascripts/mumuki_laboratory/application/submission.js index b639c27001..f53ff67c91 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/submission.js +++ b/app/assets/javascripts/mumuki_laboratory/application/submission.js @@ -15,7 +15,7 @@ var mumuki = mumuki || {}; success: function (data, submitButton) { this.submissionsResultsArea.html(data.html); data.status === 'aborted' ? this.error(submitButton) : submitButton.enable(); - mumuki.updateProgressBarAndShowModal(data); + mumuki.updateCurrentExerciseProgressBarAndShowModal(data); }, error: function (submitButton) { this.submissionsResultsArea.html(''); @@ -69,7 +69,7 @@ var mumuki = mumuki || {}; mumuki.editor.syncContent(); var solution = getContent(); - bridge._submitSolution(solution).done(function (data) { + bridge.runCurrentExerciseSolution(solution).done(function (data) { resultsBox.success(data, submitButton); }).fail(function () { resultsBox.error(submitButton); From 8b18ce3275eb74c954ebc1c7c78b02954bc55202 Mon Sep 17 00:00:00 2001 From: Franco Bulgarelli Date: Wed, 16 Oct 2019 15:00:42 -0300 Subject: [PATCH 05/20] Removing unnecessary exposed field --- lib/mumuki/laboratory/controllers/results_rendering.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/mumuki/laboratory/controllers/results_rendering.rb b/lib/mumuki/laboratory/controllers/results_rendering.rb index 1dfabf1bea..0734cb4536 100644 --- a/lib/mumuki/laboratory/controllers/results_rendering.rb +++ b/lib/mumuki/laboratory/controllers/results_rendering.rb @@ -20,7 +20,6 @@ def render_results_json(assignment, results = {}) def progress_json { guide_finished_by_solution: guide_finished_by_solution?, - class_for_progress_list_item: class_for_progress_list_item(@exercise, true) } end From e009f1815d83ec84d96fe432f63f5a2022c6e238 Mon Sep 17 00:00:00 2001 From: Franco Bulgarelli Date: Wed, 16 Oct 2019 15:30:08 -0300 Subject: [PATCH 06/20] Fixes --- .../javascripts/mumuki_laboratory/application/bridge.js | 6 ++++-- .../javascripts/mumuki_laboratory/application/connection.js | 4 ++-- .../mumuki_laboratory/application/offline-runner.js | 4 ++-- .../javascripts/mumuki_laboratory/application/polyfill.js | 5 +++-- .../mumuki_laboratory/application/results-renderer.js | 2 +- .../javascripts/mumuki_laboratory/application/submission.js | 2 +- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/mumuki_laboratory/application/bridge.js b/app/assets/javascripts/mumuki_laboratory/application/bridge.js index f4cf1fc49f..221cc4daa7 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/bridge.js +++ b/app/assets/javascripts/mumuki_laboratory/application/bridge.js @@ -35,14 +35,16 @@ // =========== _runNewSolution(exerciseId, solution){ - const responsePromise = mumuki.Connection.runNewSolution(exerciseId, solution); - return responsePromise.done((result) => { + const responsePromise = mumuki.Connection.runNewSolution(exerciseId, solution, this); + return responsePromise.then((result) => { + const status = result.status; result.title_html = mumuki.titleHtmlForStatus(status); // TODO defer rendering calculation result.button_html = mumuki.buttonHtmlForStatus(status); // TODO defer rendering calculation const lastSubmission = { content: solution, result: result }; mumuki.SubmissionsStore.setLastSubmission(exerciseId, lastSubmission); + return result; }); } } diff --git a/app/assets/javascripts/mumuki_laboratory/application/connection.js b/app/assets/javascripts/mumuki_laboratory/application/connection.js index 261144bd21..5e2713753c 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/connection.js +++ b/app/assets/javascripts/mumuki_laboratory/application/connection.js @@ -2,7 +2,7 @@ const OfflineMode = new class { // Runs solution by evaluating it locally - runNewSolution(exerciseId, solution, bridge) { + runNewSolution(exerciseId, solution, _bridge) { return mumuki.runSolutionLocally(exerciseId, solution); } @@ -20,7 +20,7 @@ const OnlineMode = new class { // Runs solution by sending it to server - runNewSolution(exerciseId, solution, _bridge) { + runNewSolution(exerciseId, solution, bridge) { return bridge.submitCurrentExerciseSolution(exerciseId, solution); } diff --git a/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js b/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js index 3dfffa0ccb..c866441c7c 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js +++ b/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js @@ -9,6 +9,6 @@ "remaining_attempts_html":null, "test_results":[] }; - return Promise.resolve(result) + return Promise.resolve(result); } -}); +})(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/polyfill.js b/app/assets/javascripts/mumuki_laboratory/application/polyfill.js index 98e4cfbf16..950b0e779a 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/polyfill.js +++ b/app/assets/javascripts/mumuki_laboratory/application/polyfill.js @@ -1,4 +1,5 @@ (() => { // Allow polimorphism between standard promise and jquery promise - Promise.prototype.done = Promise.prototype.finally; -}) + Promise.prototype.always = Promise.prototype.finally; + Promise.prototype.fail = Promise.prototype.catch; +})(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js b/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js index 79e2a60b06..aae8bfe607 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js +++ b/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js @@ -57,4 +57,4 @@ `; }; -}); +})(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/submission.js b/app/assets/javascripts/mumuki_laboratory/application/submission.js index f53ff67c91..6f52204406 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/submission.js +++ b/app/assets/javascripts/mumuki_laboratory/application/submission.js @@ -69,7 +69,7 @@ var mumuki = mumuki || {}; mumuki.editor.syncContent(); var solution = getContent(); - bridge.runCurrentExerciseSolution(solution).done(function (data) { + bridge.runCurrentExerciseSolution(solution).then(function (data) { resultsBox.success(data, submitButton); }).fail(function () { resultsBox.error(submitButton); From 14f45c9480ee20baf12500876492b38e361c573a Mon Sep 17 00:00:00 2001 From: Franco Bulgarelli Date: Wed, 16 Oct 2019 15:39:23 -0300 Subject: [PATCH 07/20] Fixing bug with attempts left --- .../mumuki_laboratory/application/submission.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/mumuki_laboratory/application/submission.js b/app/assets/javascripts/mumuki_laboratory/application/submission.js index 6f52204406..2103bce537 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/submission.js +++ b/app/assets/javascripts/mumuki_laboratory/application/submission.js @@ -15,6 +15,7 @@ var mumuki = mumuki || {}; success: function (data, submitButton) { this.submissionsResultsArea.html(data.html); data.status === 'aborted' ? this.error(submitButton) : submitButton.enable(); + submitButton.updateAttemptsLeft(data); mumuki.updateCurrentExerciseProgressBarAndShowModal(data); }, error: function (submitButton) { @@ -22,8 +23,7 @@ var mumuki = mumuki || {}; this.submissionsErrorTemplate.show(); animateTimeoutError(submitButton); }, - done: function (data, submitButton) { - submitButton.updateAttemptsLeft(data); + done: function (submitButton) { mumuki.pin.scroll(); } }; @@ -73,9 +73,9 @@ var mumuki = mumuki || {}; resultsBox.success(data, submitButton); }).fail(function () { resultsBox.error(submitButton); - }).always(function (data) { + }).always(function () { $(document).renderMuComponents(); - resultsBox.done(data, submitButton); + resultsBox.done(submitButton); }); }); From be2efbb4680a5a23837912fc3994208d1b4c3eae Mon Sep 17 00:00:00 2001 From: Franco Bulgarelli Date: Wed, 16 Oct 2019 16:36:39 -0300 Subject: [PATCH 08/20] Avoding rendering of title_html in the server --- README.md | 2 -- .../javascripts/mumuki_laboratory/application/bridge.js | 1 - .../javascripts/mumuki_laboratory/application/kids.js | 8 ++++++-- lib/mumuki/laboratory/controllers/results_rendering.rb | 6 ------ spec/controllers/exercise_solutions_controller_spec.rb | 3 +-- 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index ca9d4146e6..33341efe7e 100644 --- a/README.md +++ b/README.md @@ -204,9 +204,7 @@ which are granted to be safe and stable. { "status": "failed", "guide_finished_by_solution": false, - "class_for_progress_list_item":"progress-list-item text-center danger active", "html":"...", - "title_html":"...", "button_html":"...", "expectations_html":"...", "remaining_attempts_html":null, diff --git a/app/assets/javascripts/mumuki_laboratory/application/bridge.js b/app/assets/javascripts/mumuki_laboratory/application/bridge.js index 221cc4daa7..a41a8e99c5 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/bridge.js +++ b/app/assets/javascripts/mumuki_laboratory/application/bridge.js @@ -39,7 +39,6 @@ return responsePromise.then((result) => { const status = result.status; - result.title_html = mumuki.titleHtmlForStatus(status); // TODO defer rendering calculation result.button_html = mumuki.buttonHtmlForStatus(status); // TODO defer rendering calculation const lastSubmission = { content: solution, result: result }; diff --git a/app/assets/javascripts/mumuki_laboratory/application/kids.js b/app/assets/javascripts/mumuki_laboratory/application/kids.js index 2b2821b031..93478d1967 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/kids.js +++ b/app/assets/javascripts/mumuki_laboratory/application/kids.js @@ -172,11 +172,15 @@ mumuki.load(function () { mumuki.kids._getOverlay().hide(); }, + _titleHtml: function (data) { + return mumuki.titleHtmlForStatus(data.status); + }, + _showMessageOnCharacterBubble: function (data) { var $bubble = mumuki.kids._getCharacterBubble(); $bubble.find('.mu-kids-character-speech-bubble-tabs').hide(); $bubble.find('.mu-kids-character-speech-bubble-normal').hide(); - $bubble.find('.mu-kids-character-speech-bubble-failed').show().html(data.title_html); + $bubble.find('.mu-kids-character-speech-bubble-failed').show().html(this._titleHtml(data)); $bubble.addClass(data.status); if (data.status === 'passed_with_warnings') { $bubble.find('.mu-kids-character-speech-bubble-failed').append(data.expectations_html); @@ -203,7 +207,7 @@ mumuki.load(function () { backdrop: 'static', keyboard: false }); - $resultsKidsModal.find('.modal-header').first().html(data.title_html); + $resultsKidsModal.find('.modal-header').first().html(this._titleHtml(data)); $resultsKidsModal.find('.modal-footer').first().html(data.button_html); mumuki.kids._showCorollaryCharacter(); $('.mu-close-modal').click(() => mumuki.kids._getResultsModal().modal('hide')); diff --git a/lib/mumuki/laboratory/controllers/results_rendering.rb b/lib/mumuki/laboratory/controllers/results_rendering.rb index 0734cb4536..0afb72b5bf 100644 --- a/lib/mumuki/laboratory/controllers/results_rendering.rb +++ b/lib/mumuki/laboratory/controllers/results_rendering.rb @@ -10,7 +10,6 @@ module Mumuki::Laboratory::Controllers::ResultsRendering def render_results_json(assignment, results = {}) render json: results.merge(progress_json).merge( html: render_results_html(assignment), - title_html: render_results_title_html(assignment), button_html: render_results_button_html(assignment), expectations_html: render_results_expectations_html(assignment), remaining_attempts_html: remaining_attempts_text(assignment), @@ -33,11 +32,6 @@ def results_partial(assignment) assignment.input_kids? ? 'exercise_solutions/kids_results' : 'exercise_solutions/results' end - def render_results_title_html(assignment) - render_to_string partial: 'exercise_solutions/results_title', - locals: {contextualization: assignment} - end - def render_results_button_html(assignment) render_to_string partial: 'exercise_solutions/kids_results_button', locals: {assignment: assignment} diff --git a/spec/controllers/exercise_solutions_controller_spec.rb b/spec/controllers/exercise_solutions_controller_spec.rb index c35e56fe87..0a1fb128b8 100644 --- a/spec/controllers/exercise_solutions_controller_spec.rb +++ b/spec/controllers/exercise_solutions_controller_spec.rb @@ -18,8 +18,7 @@ it { expect(response.status).to eq 200 } it { expect(response.body).to json_eq( {status: :failed, guide_finished_by_solution: false}, - except: [:class_for_progress_list_item, - :html, :title_html, :button_html, + except: [:html, :button_html, :expectations_html, :remaining_attempts_html, :test_results]) } it { expect(Assignment.last.solution).to eq('asd')} From 6de3bcbb92ef91bd753a84630749b2cbed366cf3 Mon Sep 17 00:00:00 2001 From: Franco Bulgarelli Date: Wed, 16 Oct 2019 16:49:30 -0300 Subject: [PATCH 09/20] Fixing this-reference error --- .../mumuki_laboratory/application/connection.js | 11 +++++++++-- .../javascripts/mumuki_laboratory/application/kids.js | 6 +++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/mumuki_laboratory/application/connection.js b/app/assets/javascripts/mumuki_laboratory/application/connection.js index 5e2713753c..cce2f24f5d 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/connection.js +++ b/app/assets/javascripts/mumuki_laboratory/application/connection.js @@ -29,6 +29,13 @@ } } - // mumuki.Connection = OnlineMode; - mumuki.Connection = OfflineMode; + mumuki.goOnline = function () { + mumuki.Connection = OnlineMode; + }; + + mumuki.goOffline = function () { + mumuki.Connection = OfflineMode; + }; + + mumuki.goOnline(); })(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/kids.js b/app/assets/javascripts/mumuki_laboratory/application/kids.js index 93478d1967..5157d3473d 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/kids.js +++ b/app/assets/javascripts/mumuki_laboratory/application/kids.js @@ -180,7 +180,7 @@ mumuki.load(function () { var $bubble = mumuki.kids._getCharacterBubble(); $bubble.find('.mu-kids-character-speech-bubble-tabs').hide(); $bubble.find('.mu-kids-character-speech-bubble-normal').hide(); - $bubble.find('.mu-kids-character-speech-bubble-failed').show().html(this._titleHtml(data)); + $bubble.find('.mu-kids-character-speech-bubble-failed').show().html(mumuki.kids._titleHtml(data)); $bubble.addClass(data.status); if (data.status === 'passed_with_warnings') { $bubble.find('.mu-kids-character-speech-bubble-failed').append(data.expectations_html); @@ -207,7 +207,7 @@ mumuki.load(function () { backdrop: 'static', keyboard: false }); - $resultsKidsModal.find('.modal-header').first().html(this._titleHtml(data)); + $resultsKidsModal.find('.modal-header').first().html(mumuki.kids._titleHtml(data)); $resultsKidsModal.find('.modal-footer').first().html(data.button_html); mumuki.kids._showCorollaryCharacter(); $('.mu-close-modal').click(() => mumuki.kids._getResultsModal().modal('hide')); @@ -232,7 +232,7 @@ mumuki.load(function () { _stateScaler: function ($state, fullMargin, preferredWidth, preferredHeight) { var $table = $state.find('gs-board > table'); - if (!$table.length) return setTimeout(() => this.scaleState($state, fullMargin)); + if (!$table.length) return setTimeout(() => mumuki.kids.scaleState($state, fullMargin)); console.warn("You are using the default states scaler, which is gobstones-specific. Please register your own scaler in the future"); From 67dbaaa180362e62bf62865fe76a233562c7da6f Mon Sep 17 00:00:00 2001 From: Franco Bulgarelli Date: Wed, 16 Oct 2019 17:33:19 -0300 Subject: [PATCH 10/20] Setting editor current value --- .../mumuki_laboratory/application/codemirror.js | 7 +++++++ .../mumuki_laboratory/application/connection.js | 17 +++++++++++++++++ .../{progress-bar.js => progress.js} | 3 +++ .../exercise_inputs/editors/_custom.html.erb | 1 - 4 files changed, 27 insertions(+), 1 deletion(-) rename app/assets/javascripts/mumuki_laboratory/application/{progress-bar.js => progress.js} (87%) diff --git a/app/assets/javascripts/mumuki_laboratory/application/codemirror.js b/app/assets/javascripts/mumuki_laboratory/application/codemirror.js index 5429369c03..78e89614ac 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/codemirror.js +++ b/app/assets/javascripts/mumuki_laboratory/application/codemirror.js @@ -60,10 +60,17 @@ var mumuki = mumuki || {}; }); } + function setContent(content) { + mumuki.page.editors.each(function (_, editor) { + editor.getDoc().setValue(content); + }); + } + mumuki.editor = mumuki.editor || {}; mumuki.editor.toggleFullscreen = toggleFullscreen; mumuki.editor.indentWithSpaces = indentWithSpaces; mumuki.editor.syncContent = syncContent; + mumuki.editor.setContent = setContent; mumuki.page = mumuki.page || {}; mumuki.page.dynamicEditors = []; diff --git a/app/assets/javascripts/mumuki_laboratory/application/connection.js b/app/assets/javascripts/mumuki_laboratory/application/connection.js index cce2f24f5d..b67dc123b0 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/connection.js +++ b/app/assets/javascripts/mumuki_laboratory/application/connection.js @@ -11,6 +11,18 @@ $('.progress-list-item').each((_, it) => this._updateProgressListItemClass($(it))); } + configureExerciseEditorValue() { + const lastSubmission = mumuki.SubmissionsStore.getLastSubmission(mumuki.currentExerciseId); + if (lastSubmission) { + // TODO extract + if (lastSubmission.content.solution) { + $('#mu-custom-editor-value').val(lastSubmission.content.solution.content); + } else { + mumuki.editor.setContent(lastSubmission.content['solution[content]']); + } + } + } + _updateProgressListItemClass(a) { const exerciseId = a.data('mu-exercise-id'); const status = mumuki.SubmissionsStore.getLastSubmissionStatus(exerciseId); @@ -27,6 +39,11 @@ // Does nothing. Progress is rendered by server renderExercisesProgressBar() { } + + // Does nothing. Editor value is configured by server + configureExerciseEditorValue() { + } + } mumuki.goOnline = function () { diff --git a/app/assets/javascripts/mumuki_laboratory/application/progress-bar.js b/app/assets/javascripts/mumuki_laboratory/application/progress.js similarity index 87% rename from app/assets/javascripts/mumuki_laboratory/application/progress-bar.js rename to app/assets/javascripts/mumuki_laboratory/application/progress.js index c4f06bdd65..bcdc3ff26a 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/progress-bar.js +++ b/app/assets/javascripts/mumuki_laboratory/application/progress.js @@ -12,5 +12,8 @@ mumuki.load(() => { // Update all exercises progress indicators mumuki.Connection.renderExercisesProgressBar(); + + // Set the editor's current value + mumuki.Connection.configureExerciseEditorValue(); }) })(); diff --git a/app/views/layouts/exercise_inputs/editors/_custom.html.erb b/app/views/layouts/exercise_inputs/editors/_custom.html.erb index dcb5104cfb..602210d42f 100644 --- a/app/views/layouts/exercise_inputs/editors/_custom.html.erb +++ b/app/views/layouts/exercise_inputs/editors/_custom.html.erb @@ -1,7 +1,6 @@ <%= render_runner_assets @exercise.language, :editor %> <%= render_custom_editor(@exercise) %> - <%= form.hidden_field :content, id: "mu-custom-editor-value", value: @current_content %> <%= form.hidden_field :content_extra, id: "mu-custom-editor-extra", value: @exercise.extra %> <%= form.hidden_field :content, id: "mu-custom-editor-test", value: @exercise.test %> From 088f21370ef72b1df17565e75dd10dfdf7a89087 Mon Sep 17 00:00:00 2001 From: Franco Leonardo Bulgarelli Date: Thu, 17 Oct 2019 19:42:53 -0300 Subject: [PATCH 11/20] Adding offline runner module --- README.md | 2 + .../application/offline-runner.js | 48 +++++++++++++++---- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 33341efe7e..c0dce61302 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,8 @@ which are granted to be safe and stable. * `setUpDeleteFiles` * `setUpDeleteFile` * `updateButtonsVisibility` +* `mumuki.registerLocalTestRunner` +* `mumuki.registerLocalExpectationsRunner` * `mumuki.version` ### Bridge Response Format diff --git a/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js b/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js index c866441c7c..8f3049bdb2 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js +++ b/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js @@ -1,14 +1,42 @@ (() => { + const BasicLocalTestRunner = new class { + runTests(solution, exercise, result) { + result.status = "passed"; // FIXME hardcoded + result.guide_finished_by_solution = false; + result.html = "

Como ves, para que el tractor siembre lechuga, tenemos que decirle a Mukinita que ponga 2 bolitas verdes. Y en este ejemplo acabamos de sembrar una hilera de 4 lechugas.

Pero fue un poco difícil entenderlo, ¿no?

", + result.remaining_attempts_html = null; + result.test_results = [] + } + } + + const MulangLocalExpectationsRunner = new class { + runExpectations(solution, exercise, result) { + result.expectations_html = ''; + } + } + mumuki.runSolutionLocally = function (exerciseId, solution) { - const status = "passed"; // FIXME hardcoded - const result = { - "status": status, - "guide_finished_by_solution": false, - "html":"

Como ves, para que el tractor siembre lechuga, tenemos que decirle a Mukinita que ponga 2 bolitas verdes. Y en este ejemplo acabamos de sembrar una hilera de 4 lechugas.

Pero fue un poco difícil entenderlo, ¿no?

", - "expectations_html":"", - "remaining_attempts_html":null, - "test_results":[] - }; + const exercise = mumuki.ExerciseStore.find(exerciseId); + let result = {}; + mumuki.localTestRunner.runTests(solution, exercise, result); + mumuki.localExpectationsRunner.runExpectations(solution, exercise, result); return Promise.resolve(result); - } + }; + + // Runners may call this function to set local test runners + mumuki.registerLocalTestRunner = function (runner) { + mumuki.localTestRunner = runner; + }; + + // Runners may call this function to set local expectation runners + mumuki.registerLocalExpectationsRunner = function (runner) { + mumuki.localExpectationsRunner = runner; + }; + + mumuki.load(() => { + mumuki.registerLocalTestRunner(BasicLocalTestRunner) + mumuki.registerLocalTestRunner(MulangLocalExpectationsRunner) + }); })(); + + From 5c6a1709d994e4a3ed6c90ac2631e20ba242c2d7 Mon Sep 17 00:00:00 2001 From: Franco Bulgarelli Date: Thu, 17 Oct 2019 21:55:49 -0300 Subject: [PATCH 12/20] Introducing ExercisesStore --- Gemfile | 2 +- .../application/current-exercise.js | 4 ++++ .../application/exercisess-store.js | 24 +++++++++++++++++++ .../application/offline-runner.js | 2 +- app/views/exercises/show.html.erb | 2 +- 5 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/mumuki_laboratory/application/exercisess-store.js diff --git a/Gemfile b/Gemfile index 42ca2e3150..83683e9546 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ ruby '~> 2.3' gem 'puma' -gem 'mumuki-domain', github: 'mumuki/mumuki-domain' +gem 'mumuki-domain', github: 'mumuki/mumuki-domain', branch: 'feature-cumparsita' gem 'execjs' gem 'therubyracer', platforms: :ruby diff --git a/app/assets/javascripts/mumuki_laboratory/application/current-exercise.js b/app/assets/javascripts/mumuki_laboratory/application/current-exercise.js index 2f31389b05..b3600968b7 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/current-exercise.js +++ b/app/assets/javascripts/mumuki_laboratory/application/current-exercise.js @@ -2,10 +2,14 @@ mumuki.load(() => { // Set global currentExerciseId const $muExerciseId = $('#mu-exercise-id')[0]; + const $muExerciseResource = $('#mu-exercise-resource')[0]; if ($muExerciseId) { mumuki.currentExerciseId = $muExerciseId.value; + mumuki.currentExerciseResource = $muExerciseResource.value; + mumuki.ExercisesStore.saveJson(mumuki.currentExerciseId, mumuki.currentExerciseResource); } else { mumuki.currentExerciseId = null; + mumuki.currentExerciseResource = null; } }) })(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/exercisess-store.js b/app/assets/javascripts/mumuki_laboratory/application/exercisess-store.js new file mode 100644 index 0000000000..f51bdbc15a --- /dev/null +++ b/app/assets/javascripts/mumuki_laboratory/application/exercisess-store.js @@ -0,0 +1,24 @@ +(() => { + const ExercisesStore = new class { + find(exerciseId) { + const exercise = window.localStorage.getItem(this._keyFor(exerciseId)); + if (!exercise) return null; + return JSON.parse(exercise); + } + + // Saves an exercise object + save(exerciseId, exercise) { + this.saveJson(exerciseId, JSON.stringify(exercise)); + } + + // Saves an exercise json string + saveJson(exerciseId, exerciseJson) { + window.localStorage.setItem(this._keyFor(exerciseId), exerciseJson); + } + + _keyFor(exerciseId) { + return `/exercise/${exerciseId}`; + } + }; + mumuki.ExercisesStore = ExercisesStore; +})(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js b/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js index 8f3049bdb2..c7c73a2ea0 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js +++ b/app/assets/javascripts/mumuki_laboratory/application/offline-runner.js @@ -16,7 +16,7 @@ } mumuki.runSolutionLocally = function (exerciseId, solution) { - const exercise = mumuki.ExerciseStore.find(exerciseId); + const exercise = mumuki.ExercisesStore.find(exerciseId); let result = {}; mumuki.localTestRunner.runTests(solution, exercise, result); mumuki.localExpectationsRunner.runExpectations(solution, exercise, result); diff --git a/app/views/exercises/show.html.erb b/app/views/exercises/show.html.erb index 83c6988bec..9e47082032 100644 --- a/app/views/exercises/show.html.erb +++ b/app/views/exercises/show.html.erb @@ -46,7 +46,7 @@ <%= hidden_field_tag default_content_tag_id(@exercise), @default_content %> <%= hidden_field_tag "mu-exercise-id", @exercise.id %> -<%= hidden_field_tag "mu-exercise-resource", @exercise.to_resource_h.to_json %> +<%= hidden_field_tag "mu-exercise-resource", @exercise.to_resource_h(markdownified: true).to_json %>