mumuki-laboratory 7.6.2 → 7.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +203 -2
  3. data/Rakefile +9 -0
  4. data/app/assets/javascripts/mumuki_laboratory/application.js +0 -1
  5. data/app/assets/javascripts/mumuki_laboratory/application/alias-modes.js +1 -1
  6. data/app/assets/javascripts/mumuki_laboratory/application/assets-loader.js +1 -1
  7. data/app/assets/javascripts/mumuki_laboratory/application/bridge.js +82 -47
  8. data/app/assets/javascripts/mumuki_laboratory/application/button.js +90 -1
  9. data/app/assets/javascripts/mumuki_laboratory/application/codemirror-builder.js +28 -25
  10. data/app/assets/javascripts/mumuki_laboratory/application/codemirror.js +8 -9
  11. data/app/assets/javascripts/mumuki_laboratory/application/confirmation.js +2 -2
  12. data/app/assets/javascripts/mumuki_laboratory/application/console.js +41 -43
  13. data/app/assets/javascripts/mumuki_laboratory/application/csrf-token.js +9 -12
  14. data/app/assets/javascripts/mumuki_laboratory/application/custom-editor.js +46 -8
  15. data/app/assets/javascripts/mumuki_laboratory/application/discussions.js +15 -16
  16. data/app/assets/javascripts/mumuki_laboratory/application/editors.js +104 -0
  17. data/app/assets/javascripts/mumuki_laboratory/application/elipsis.js +5 -4
  18. data/app/assets/javascripts/mumuki_laboratory/application/exercise.js +32 -0
  19. data/app/assets/javascripts/mumuki_laboratory/application/inputs.js +4 -2
  20. data/app/assets/javascripts/mumuki_laboratory/application/interval.js +2 -4
  21. data/app/assets/javascripts/mumuki_laboratory/application/kids.js +74 -37
  22. data/app/assets/javascripts/mumuki_laboratory/application/load-analytics.js +1 -1
  23. data/app/assets/javascripts/mumuki_laboratory/application/load-error-svg.js +1 -1
  24. data/app/assets/javascripts/mumuki_laboratory/application/messages.js +2 -2
  25. data/app/assets/javascripts/mumuki_laboratory/application/multiple-choice.js +1 -1
  26. data/app/assets/javascripts/mumuki_laboratory/application/multiple-scenarios.js +3 -6
  27. data/app/assets/javascripts/mumuki_laboratory/application/pin.js +3 -5
  28. data/app/assets/javascripts/mumuki_laboratory/application/progress.js +27 -6
  29. data/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js +60 -0
  30. data/app/assets/javascripts/mumuki_laboratory/application/speech-bubble-renderer.js +12 -5
  31. data/app/assets/javascripts/mumuki_laboratory/application/submission.js +122 -55
  32. data/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js +93 -0
  33. data/app/assets/javascripts/mumuki_laboratory/application/sync-mode.js +75 -0
  34. data/app/assets/javascripts/mumuki_laboratory/application/timer.js +5 -6
  35. data/app/assets/javascripts/mumuki_laboratory/application/tooltip.js +1 -1
  36. data/app/assets/javascripts/mumuki_laboratory/application/upload.js +1 -1
  37. data/app/assets/javascripts/mumuki_laboratory/application/user.js +1 -1
  38. data/app/assets/stylesheets/mumuki_laboratory/application/_modules.scss +1 -0
  39. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_discussion.scss +43 -5
  40. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_gs-board.scss +3 -0
  41. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kids.scss +3 -4
  42. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kindergarten.scss +55 -0
  43. data/app/controllers/application_controller.rb +1 -0
  44. data/app/controllers/assets_controller.rb +2 -0
  45. data/app/controllers/concerns/with_authorization.rb +4 -0
  46. data/app/controllers/concerns/with_user_discussion_validation.rb +14 -0
  47. data/app/controllers/discussions_controller.rb +6 -14
  48. data/app/controllers/discussions_messages_controller.rb +10 -1
  49. data/app/controllers/exercise_solutions_controller.rb +4 -2
  50. data/app/helpers/application_helper.rb +9 -5
  51. data/app/helpers/discussions_helper.rb +37 -23
  52. data/app/helpers/exercise_input_helper.rb +1 -1
  53. data/app/helpers/{locale_helper.rb → globals_helper.rb} +6 -2
  54. data/app/helpers/icons_helper.rb +3 -3
  55. data/app/mailers/user_mailer.rb +24 -11
  56. data/app/views/book/show.html.erb +1 -1
  57. data/app/views/book_discussions/index.html.erb +3 -3
  58. data/app/views/discussions/_message.html.erb +20 -8
  59. data/app/views/discussions/index.html.erb +0 -1
  60. data/app/views/discussions/new.html.erb +33 -0
  61. data/app/views/discussions/show.html.erb +18 -46
  62. data/app/views/exercise_solutions/_contextualization_results_container.html.erb +1 -1
  63. data/app/views/exercise_solutions/_results_title.html.erb +2 -2
  64. data/app/views/exercises/_read_only.html.erb +33 -6
  65. data/app/views/exercises/show.html.erb +2 -0
  66. data/app/views/layouts/_copyright.html.erb +1 -1
  67. data/app/views/layouts/_discussions.html.erb +21 -3
  68. data/app/views/layouts/_main.html.erb +1 -2
  69. data/app/views/layouts/_progress.html.erb +1 -1
  70. data/app/views/layouts/_progress_bar.html.erb +7 -1
  71. data/app/views/layouts/_test_results.html.erb +1 -1
  72. data/app/views/layouts/application.html.erb +1 -1
  73. data/app/views/layouts/exercise_inputs/editors/_custom.html.erb +1 -1
  74. data/app/views/layouts/exercise_inputs/forms/_kids_form.html.erb +1 -1
  75. data/app/views/layouts/exercise_inputs/forms/_problem_form.html.erb +1 -1
  76. data/app/views/layouts/exercise_inputs/layouts/_input_bottom.html.erb +1 -1
  77. data/app/views/layouts/exercise_inputs/layouts/_input_kindergarten.html.erb +40 -0
  78. data/app/views/layouts/exercise_inputs/layouts/{_input_kids.html.erb → _input_primary.html.erb} +1 -1
  79. data/app/views/layouts/exercise_inputs/layouts/_input_right.html.erb +1 -1
  80. data/app/views/layouts/modals/_kids_context.html.erb +1 -8
  81. data/app/views/user_mailer/1st_reminder.html.erb +1 -1
  82. data/app/views/user_mailer/1st_reminder.text.erb +1 -1
  83. data/app/views/user_mailer/2nd_reminder.html.erb +1 -1
  84. data/app/views/user_mailer/2nd_reminder.text.erb +1 -1
  85. data/app/views/user_mailer/3rd_reminder.html.erb +1 -1
  86. data/app/views/user_mailer/3rd_reminder.text.erb +1 -1
  87. data/app/views/user_mailer/no_submissions_reminder.html.erb +1 -1
  88. data/app/views/user_mailer/no_submissions_reminder.text.erb +1 -1
  89. data/config/routes.rb +2 -1
  90. data/lib/mumuki/laboratory/controllers.rb +1 -0
  91. data/lib/mumuki/laboratory/controllers/incognito_mode.rb +28 -0
  92. data/lib/mumuki/laboratory/controllers/results_rendering.rb +1 -2
  93. data/lib/mumuki/laboratory/locales/en.yml +14 -6
  94. data/lib/mumuki/laboratory/locales/es-CL.yml +292 -0
  95. data/lib/mumuki/laboratory/locales/es.yml +13 -5
  96. data/lib/mumuki/laboratory/locales/pt.yml +12 -6
  97. data/lib/mumuki/laboratory/version.rb +1 -1
  98. data/spec/controllers/confirmations_controller_spec.rb +1 -1
  99. data/spec/controllers/discussions_messages_controller_spec.rb +73 -0
  100. data/spec/controllers/exercise_solutions_controller_spec.rb +41 -6
  101. data/spec/dummy/db/schema.rb +13 -1
  102. data/spec/features/chapter_spec.rb +17 -0
  103. data/spec/features/discussion_flow_spec.rb +190 -0
  104. data/spec/features/exercise_flow_spec.rb +48 -3
  105. data/spec/features/home_public_flow_spec.rb +16 -0
  106. data/spec/features/menu_bar_spec.rb +88 -7
  107. data/spec/helpers/breadcrumbs_helper_spec.rb +1 -1
  108. data/spec/javascripts/bridge-spec.js +5 -0
  109. data/spec/javascripts/csrf-token-spec.js +7 -0
  110. data/spec/javascripts/editors-spec.js +54 -0
  111. data/spec/javascripts/elipsis-spec.js +25 -0
  112. data/spec/javascripts/exercise-spec.js +22 -0
  113. data/spec/javascripts/global-spec.js +6 -0
  114. data/spec/javascripts/results-renderers-spec.js +17 -0
  115. data/spec/javascripts/spec-helper.js +34 -0
  116. data/spec/javascripts/speech-bubble-renderer-spec.js +11 -0
  117. data/spec/javascripts/submissions-store-spec.js +44 -0
  118. data/spec/javascripts/sync-mode-spec.js +15 -0
  119. data/spec/javascripts/timeout-spec.js +5 -0
  120. data/spec/javascripts/timer-spec.js +5 -0
  121. data/spec/mailers/user_mailer_spec.rb +18 -3
  122. data/spec/teaspoon_env.rb +193 -0
  123. metadata +50 -11
  124. data/app/helpers/version_helper.rb +0 -5
  125. data/app/views/layouts/modals/_new_discussion.html.erb +0 -27
  126. data/vendor/assets/javascripts/hotjar.js +0 -8
@@ -1,4 +1,4 @@
1
- mumuki.load(function(){
1
+ mumuki.load(() => {
2
2
  ga('create', 'UA-58353823-1', 'auto');
3
3
  ga('send', 'pageview');
4
4
  });
@@ -1,4 +1,4 @@
1
- mumuki.load(function () {
1
+ mumuki.load(() => {
2
2
  var error_svgs = ['403', '404', '500', 'timeout_1', 'timeout_2', 'timeout_3'];
3
3
 
4
4
  mumuki.errors = mumuki.errors || {};
@@ -1,4 +1,4 @@
1
- mumuki.load(function () {
1
+ mumuki.load(() => {
2
2
  var Chat = {
3
3
  $body: function () {
4
4
  return $('body')
@@ -61,7 +61,7 @@ mumuki.load(function () {
61
61
  Chat.readMessages(readUrl);
62
62
  }
63
63
 
64
- function error(xhr) {
64
+ function error(_xhr) {
65
65
  Chat.tokenRequest({
66
66
  url: errorUrl,
67
67
  success: renderHTML,
@@ -1,5 +1,5 @@
1
1
  mumuki.load(() => {
2
- function dumpChoices(evt) {
2
+ function dumpChoices(_evt) {
3
3
  var indexes = $('.solution-choice:checked').map(function () {
4
4
  return $(this).data('index')
5
5
  }).get().join(':');
@@ -1,6 +1,4 @@
1
- var mumuki = mumuki || {};
2
-
3
- (function (mumuki) {
1
+ mumuki.MultipleScenarios = (() => {
4
2
 
5
3
  const setControlVisibility = function ($control, visible) {
6
4
  visible ? $control.show() : $control.hide();
@@ -147,6 +145,5 @@ var mumuki = mumuki || {};
147
145
  }
148
146
  }
149
147
 
150
- mumuki.MultipleScenarios = MultipleScenarios;
151
-
152
- }(mumuki));
148
+ return MultipleScenarios;
149
+ })();
@@ -1,12 +1,10 @@
1
- var mumuki = mumuki || {};
2
-
3
- (function (mumuki) {
1
+ mumuki.pin = (() => {
4
2
  function smoothScrollToElement(domElement) {
5
3
  var SPEED = 1000;
6
4
  $('html, body').animate({scrollTop: domElement.offset().top}, SPEED);
7
5
  }
8
6
 
9
- mumuki.pin = {
7
+ return {
10
8
  scroll: function () {
11
9
  var scrollPin = $('.scroll-pin');
12
10
  if (scrollPin.length) {
@@ -14,4 +12,4 @@ var mumuki = mumuki || {};
14
12
  }
15
13
  }
16
14
  }
17
- })(mumuki);
15
+ })();
@@ -1,10 +1,31 @@
1
- var mumuki = mumuki || {};
2
-
3
- (function (mumuki) {
4
-
5
- mumuki.updateProgressBarAndShowModal = function (data) {
1
+ mumuki.progress = (() => {
2
+ /**
3
+ * Updates the current exercise progress indicator
4
+ *
5
+ * @param {SubmissionResult} data
6
+ * */
7
+ function updateProgressBarAndShowModal(data) {
6
8
  $('.progress-list-item.active').attr('class', data.class_for_progress_list_item);
7
9
  if(data.guide_finished_by_solution) $('#guide-done').modal();
10
+ }
11
+
12
+ /**
13
+ * Update all links in the progress bar with the given function
14
+ *
15
+ * @param {(anchor: JQuery) => string} f
16
+ */
17
+ function updateWholeProgressBar(f) {
18
+ $('.progress-list-item').each((_, it) => {
19
+ const $anchor = $(it);
20
+ $anchor.attr('class', f($anchor))
21
+ });
22
+ }
23
+
24
+ return {
25
+ updateProgressBarAndShowModal,
26
+ updateWholeProgressBar
8
27
  };
28
+ })();
9
29
 
10
- })(mumuki);
30
+ /** @deprecated use {@code mumuki.progress.updateProgressBarAndShowModal} instead */
31
+ mumuki.updateProgressBarAndShowModal = mumuki.progress.updateProgressBarAndShowModal;
@@ -0,0 +1,60 @@
1
+ mumuki.renderers = mumuki.renderers || {};
2
+ mumuki.renderers.results = (() => {
3
+
4
+
5
+ // ==========================
6
+ // View function for building
7
+ // the results UI
8
+ // ==========================
9
+
10
+ /**
11
+ * @param {SubmissionStatus} status
12
+ * @returns {string}
13
+ */
14
+ function iconForStatus(status) {
15
+ switch (status) {
16
+ case "errored": return "fa-minus-circle";
17
+ case "failed": return "fa-times-circle";
18
+ case "passed_with_warnings": return "fa-exclamation-circle";
19
+ case "passed": return "fa-check-circle";
20
+ case "pending": return "fa-circle";
21
+ }
22
+ }
23
+
24
+ /**
25
+ * @param {SubmissionStatus} status
26
+ * @returns {string}
27
+ */
28
+ function classForStatus(status) {
29
+ switch (status) {
30
+ case "errored": return "broken";
31
+ case "failed": return "danger";
32
+ case "passed_with_warnings": return "warning";
33
+ case "passed": return "success";
34
+ case "pending": return "muted";
35
+ }
36
+ }
37
+
38
+
39
+ /**
40
+ * @param {SubmissionStatus} status
41
+ * @param {boolean} [active]
42
+ * @returns {string}
43
+ */
44
+ function progressListItemClassForStatus(status, active = false) {
45
+ return `progress-list-item text-center ${classForStatus(status)} ${active ? 'active' : ''}`;
46
+ }
47
+
48
+ return {
49
+ classForStatus,
50
+ iconForStatus,
51
+ progressListItemClassForStatus
52
+ }
53
+ })();
54
+
55
+ /** @deprecated use {@code mumuki.renderers.results.classForStatus} instead */
56
+ mumuki.renderers.classForStatus = mumuki.renderers.results.classForStatus;
57
+ /** @deprecated use {@code mumuki.renderers.results.iconForStatus} instead */
58
+ mumuki.renderers.iconForStatus = mumuki.renderers.results.iconForStatus;
59
+ /** @deprecated use {@code mumuki.renderers.results.progressListItemClassForStatus} instead */
60
+ mumuki.renderers.progressListItemClassForStatus = mumuki.renderers.results.progressListItemClassForStatus;
@@ -1,4 +1,5 @@
1
- ((mumuki)=> {
1
+ mumuki.renderers = mumuki.renderers || {};
2
+ mumuki.renderers.speechBubble = (()=> {
2
3
 
3
4
  function renderSpeechBubbleResultItem(item) {
4
5
  return `
@@ -93,7 +94,13 @@
93
94
  }
94
95
  }
95
96
 
96
- mumuki.renderers = mumuki.renderers || {};
97
- mumuki.renderers.SpeechBubbleRenderer = SpeechBubbleRenderer;
98
- mumuki.renderers.renderSpeechBubbleResultItem = renderSpeechBubbleResultItem;
99
- })(mumuki)
97
+ return {
98
+ SpeechBubbleRenderer,
99
+ renderSpeechBubbleResultItem
100
+ }
101
+ })();
102
+
103
+ /** @deprecated use {@code mumuki.renderers.speechBubble.SpeechBubbleRenderer} instead */
104
+ mumuki.renderers.SpeechBubbleRenderer = mumuki.renderers.speechBubble.SpeechBubbleRenderer;
105
+ /** @deprecated use {@code mumuki.renderers.speechBubble.renderSpeechBubbleResultItem} instead */
106
+ mumuki.renderers.renderSpeechBubbleResultItem = mumuki.renderers.speechBubble.renderSpeechBubbleResultItem;
@@ -1,32 +1,42 @@
1
- var mumuki = mumuki || {};
1
+ mumuki.submission = (() => {
2
2
 
3
- (function (mumuki) {
4
- function ResultsBox(submissionsResults) {
5
- this.submissionsResultsArea = submissionsResults;
6
- this.processingTemplate = $('#processing-template');
7
- this.submissionsErrorTemplate = $(".submission-result-error");
3
+ // =============
4
+ // UI Components
5
+ // =============
6
+
7
+ function animateTimeoutError(submitButton) {
8
+ let scene = new muvment.Scene($('.submission-result-error-animation'));
9
+ scene.addState(mumuki.errorState('timeout_1').onStart(submitButton.setOriginalContent.bind(submitButton)).onEndSwitch(scene, 'timeout_2'))
10
+ .addState(mumuki.errorState('timeout_2').onEndSwitch(scene, 'timeout_3'))
11
+ .addState(mumuki.errorState('timeout_3').onStart(submitButton.enable.bind(submitButton)))
12
+ .play();
8
13
  }
9
14
 
10
- ResultsBox.prototype = {
11
- waiting: function () {
15
+ class ResultsBox {
16
+ constructor(submissionsResults) {
17
+ this.submissionsResultsArea = submissionsResults;
18
+ this.processingTemplate = $('#processing-template');
19
+ this.submissionsErrorTemplate = $(".submission-result-error");
20
+ }
21
+ waiting() {
12
22
  this.submissionsResultsArea.html(this.processingTemplate.html());
13
23
  this.submissionsErrorTemplate.hide();
14
- },
15
- success: function (data, submitButton) {
24
+ }
25
+ success(data, submitButton) {
16
26
  this.submissionsResultsArea.html(data.html);
17
27
  data.status === 'aborted' ? this.error(submitButton) : submitButton.enable();
18
28
  mumuki.updateProgressBarAndShowModal(data);
19
- },
20
- error: function (submitButton) {
29
+ }
30
+ error(submitButton) {
21
31
  this.submissionsResultsArea.html('');
22
32
  this.submissionsErrorTemplate.show();
23
33
  animateTimeoutError(submitButton);
24
- },
25
- done: function (data, submitButton) {
34
+ }
35
+ done(data, submitButton) {
26
36
  submitButton.updateAttemptsLeft(data);
27
37
  mumuki.pin.scroll();
28
38
  }
29
- };
39
+ }
30
40
 
31
41
  class SubmitButton extends mumuki.Button {
32
42
 
@@ -44,31 +54,61 @@ var mumuki = mumuki || {};
44
54
  this.preventClick();
45
55
  }
46
56
  }
47
-
48
57
  }
49
58
 
50
- mumuki.load(function () {
51
- var submissionsResults = $('.submission-results');
52
- if (!submissionsResults) return;
53
59
 
54
- var resultsBox = new ResultsBox(submissionsResults);
55
-
56
- var btnSubmit = $('.btn-submit');
57
- var submissionControl = $('.submission_control');
60
+ // ==========
61
+ // Processing
62
+ // ==========
63
+
64
+ /**
65
+ * Process solution, which consist of making buttons wait, sending to server, rendering results,
66
+ * restoring buttons state.
67
+ *
68
+ * The actual implementation of this method depends on contextual {@link _solutionProcessor}, which can
69
+ * be configured using {@link _registerSolutionProcessor}. Currently there are only two available processors -
70
+ * {@link _kidsSolutionProcessor} and {@link _classicSolutionProcessor} - which are automatically choosen depending
71
+ * on the exercise DOM.
72
+ *
73
+ * @param {Submission} solution
74
+ */
75
+ function processSolution(solution) {
76
+ mumuki.submission._solutionProcessor(solution);
77
+ }
58
78
 
59
- var submitButton = new SubmitButton(btnSubmit, submissionControl);
79
+ /**
80
+ * Configures a callback for processing a solution.
81
+ *
82
+ * This method is called internally by {@link _selectSolutionProcessor}
83
+ * and should normally not be called by runners editor, but is exposed
84
+ * for further non-standard customizations.
85
+ *
86
+ * @param {({solution: object}) => void} processor
87
+ */
88
+ function _registerSolutionProcessor(processor) {
89
+ mumuki.submission._solutionProcessor = processor;
90
+ }
60
91
 
61
- var bridge = new mumuki.bridge.Laboratory;
92
+ /** Processor for kids layouts */
93
+ function _kidsSolutionProcessor(bridge, submitButton) {
94
+ return (solution) => {
95
+ submitButton.wait();
96
+ bridge._submitSolution(solution).always(function (data) {
97
+ submitButton.ready(() => {
98
+ mumuki.kids.restart();
99
+ submitButton.continue();
100
+ });
101
+ mumuki.kids.showResult(data);
102
+ });
103
+ }
104
+ }
62
105
 
63
- btnSubmit.on('click', function (e) {
64
- e.preventDefault();
106
+ /** Processor for non-kids layouts */
107
+ function _classicSolutionProcessor(bridge, submitButton, resultsBox) {
108
+ return (solution) => {
65
109
  submitButton.disable();
66
110
  submitButton.setWaitingText();
67
111
  resultsBox.waiting();
68
-
69
- mumuki.editor.syncContent();
70
- var solution = getContent();
71
-
72
112
  bridge._submitSolution(solution).done(function (data) {
73
113
  resultsBox.success(data, submitButton);
74
114
  }).fail(function () {
@@ -77,36 +117,63 @@ var mumuki = mumuki || {};
77
117
  $(document).renderMuComponents();
78
118
  resultsBox.done(data, submitButton);
79
119
  });
80
- });
81
-
82
- submitButton.checkAttemptsLeft();
83
- });
120
+ }
121
+ }
84
122
 
85
- function getEditorsContent() {
86
- return $('.new_solution').serializeArray().concat(mumuki.CustomEditor.getContent())
123
+ /** Selects the most appropriate solution processor */
124
+ function _selectSolutionProcessor(submitButton, $submissionsResults) {
125
+ const bridge = new mumuki.bridge.Laboratory();
126
+ let processor;
127
+ if ($('.mu-kids-exercise').length) {
128
+ processor = _kidsSolutionProcessor(bridge, submitButton);
129
+ } else {
130
+ processor = _classicSolutionProcessor(bridge, submitButton, new ResultsBox($submissionsResults));
131
+ }
132
+ mumuki.submission._registerSolutionProcessor(processor);
87
133
  }
88
134
 
89
- function getContent(){
90
- var content = {};
91
135
 
92
- getEditorsContent().forEach(function(it) {
93
- content[it.name] = it.value;
136
+ // ===========
137
+ // Entry Point
138
+ // ===========
139
+
140
+ mumuki.load(() => {
141
+ var $submissionsResults = $('.submission-results');
142
+ if (!$submissionsResults) return;
143
+
144
+ const $btnSubmit = $('.btn-submit');
145
+ const submitButton = new SubmitButton($btnSubmit, $('.submission_control'));
146
+
147
+ mumuki.submission._selectSolutionProcessor(submitButton, $submissionsResults);
148
+
149
+ submitButton.start(() => {
150
+ mumuki.submission.processSolution(mumuki.editors.getSubmission());
94
151
  });
95
152
 
96
- return content;
97
- }
153
+ submitButton.checkAttemptsLeft();
154
+ });
98
155
 
99
- function animateTimeoutError(submitButton) {
100
- let scene = new muvment.Scene($('.submission-result-error-animation'));
101
- scene.addState(mumuki.errorState('timeout_1').onStart(submitButton.setOriginalContent.bind(submitButton)).onEndSwitch(scene, 'timeout_2'))
102
- .addState(mumuki.errorState('timeout_2').onEndSwitch(scene, 'timeout_3'))
103
- .addState(mumuki.errorState('timeout_3').onStart(submitButton.enable.bind(submitButton)))
104
- .play();
105
- }
106
156
 
107
- mumuki.submission = {
108
- animateTimeoutError: animateTimeoutError,
109
- SubmitButton: SubmitButton
157
+ /**
158
+ * This module contains methods for submitting solution in at high level, dealing with network communication,
159
+ * and layout-sensitive UI updates. It is intended to be both used internally by standard editors and by runners
160
+ * custom editors.
161
+ *
162
+ * Runners can choose to bypass this module under kids layouts, and handling all that low-level details. In order
163
+ * to do that {@code .mu-kids-submit-button} selector must be overiden. Customizing submission in classic layout
164
+ * or in a layout-agnostic way can be accomplish by overriding {@code .mu-submit-button}.
165
+ *
166
+ * @see mumuki.kids.showResult
167
+ * @see mumuki.bridge.Laboratory.runTests
168
+ *
169
+ * @module mumuki.submission
170
+ */
171
+ return {
172
+ processSolution,
173
+ _registerSolutionProcessor,
174
+ _selectSolutionProcessor,
175
+
176
+ animateTimeoutError,
177
+ SubmitButton,
110
178
  };
111
-
112
- })(mumuki);
179
+ })();
@@ -0,0 +1,93 @@
1
+ mumuki.SubmissionsStore = (() => {
2
+ const SubmissionsStore = new class {
3
+ /**
4
+ * Returns the submission's result status for the last submission to
5
+ * the given exercise, or pending, if not present
6
+ *
7
+ * @param {number} exerciseId
8
+ * @returns {SubmissionStatus}
9
+ */
10
+ getLastSubmissionStatus(exerciseId) {
11
+ const submission = this.getLastSubmissionAndResult(exerciseId);
12
+ return submission ? submission.result.status : 'pending';
13
+ }
14
+
15
+ /**
16
+ * Returns the submission and result for the last submission to
17
+ * the given exercise
18
+ *
19
+ * @param {number} exerciseId
20
+ * @returns {SubmissionAndResult}
21
+ */
22
+ getLastSubmissionAndResult(exerciseId) {
23
+ const submissionAndResult = window.localStorage.getItem(this._keyFor(exerciseId));
24
+ if (!submissionAndResult) return null;
25
+ return JSON.parse(submissionAndResult);
26
+ }
27
+
28
+ /**
29
+ * Saves the result for the given exercise
30
+ *
31
+ * @param {number} exerciseId
32
+ * @param {SubmissionAndResult} submissionAndResult
33
+ */
34
+ setSubmissionResultFor(exerciseId, submissionAndResult) {
35
+ window.localStorage.setItem(this._keyFor(exerciseId), this._asString(submissionAndResult));
36
+ }
37
+
38
+ /**
39
+ * Retrieves the last cached, non-aborted result for the given submission of the given exercise
40
+ *
41
+ * @param {number} exerciseId
42
+ * @param {Submission} submission
43
+ * @returns {SubmissionResult} the cached result for this submission
44
+ */
45
+ getSubmissionResultFor(exerciseId, submission) {
46
+ const last = this.getLastSubmissionAndResult(exerciseId);
47
+ if (!last
48
+ || last.result.status === 'aborted'
49
+ || !this.submissionSolutionEquals(last.submission, submission)) {
50
+ return null;
51
+ }
52
+ return last.result;
53
+ }
54
+
55
+ /**
56
+ * Extract the submission's solution content
57
+ *
58
+ * @param {Submission} submission
59
+ * @returns {string}
60
+ */
61
+ submissionSolutionContent(submission) {
62
+ if (submission.solution) {
63
+ return submission.solution.content;
64
+ } else {
65
+ return submission['solution[content]'];
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Compares two solutions to determine if they are equivalent
71
+ * from the point of view of the code evaluation
72
+ *
73
+ * @param {Submission} one
74
+ * @param {Submission} other
75
+ * @returns {boolean}
76
+ */
77
+ submissionSolutionEquals(one, other) {
78
+ return this.submissionSolutionContent(one) === this.submissionSolutionContent(other);
79
+ }
80
+
81
+ // private API
82
+
83
+ _asString(object) {
84
+ return JSON.stringify(object);
85
+ }
86
+
87
+ _keyFor(exerciseId) {
88
+ return `/exercise/${exerciseId}/submission`;
89
+ }
90
+ };
91
+
92
+ return SubmissionsStore;
93
+ })();