mumuki-laboratory 7.6.1 → 7.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +193 -2
  3. data/Rakefile +3 -0
  4. data/app/assets/javascripts/mumuki_laboratory/application.js +0 -1
  5. data/app/assets/javascripts/mumuki_laboratory/application/assets-loader.js +1 -1
  6. data/app/assets/javascripts/mumuki_laboratory/application/bridge.js +36 -10
  7. data/app/assets/javascripts/mumuki_laboratory/application/button.js +90 -1
  8. data/app/assets/javascripts/mumuki_laboratory/application/codemirror.js +1 -0
  9. data/app/assets/javascripts/mumuki_laboratory/application/custom-editor.js +46 -4
  10. data/app/assets/javascripts/mumuki_laboratory/application/discussions.js +14 -13
  11. data/app/assets/javascripts/mumuki_laboratory/application/kids.js +73 -36
  12. data/app/assets/javascripts/mumuki_laboratory/application/progress.js +3 -0
  13. data/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js +51 -0
  14. data/app/assets/javascripts/mumuki_laboratory/application/submission.js +184 -35
  15. data/app/assets/stylesheets/mumuki_laboratory/application/_modules.scss +1 -0
  16. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_discussion.scss +43 -5
  17. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kids.scss +3 -3
  18. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kindergarten.scss +55 -0
  19. data/app/controllers/assets_controller.rb +2 -0
  20. data/app/controllers/concerns/with_authorization.rb +4 -0
  21. data/app/controllers/concerns/with_user_discussion_validation.rb +14 -0
  22. data/app/controllers/discussions_controller.rb +6 -14
  23. data/app/controllers/discussions_messages_controller.rb +10 -1
  24. data/app/controllers/exercise_solutions_controller.rb +4 -2
  25. data/app/helpers/application_helper.rb +9 -5
  26. data/app/helpers/discussions_helper.rb +37 -23
  27. data/app/helpers/exercise_input_helper.rb +1 -1
  28. data/app/helpers/icons_helper.rb +3 -3
  29. data/app/views/book_discussions/index.html.erb +3 -3
  30. data/app/views/discussions/_message.html.erb +20 -8
  31. data/app/views/discussions/index.html.erb +0 -1
  32. data/app/views/discussions/new.html.erb +33 -0
  33. data/app/views/discussions/show.html.erb +18 -46
  34. data/app/views/exercise_solutions/_contextualization_results_container.html.erb +1 -1
  35. data/app/views/exercise_solutions/_results_title.html.erb +2 -2
  36. data/app/views/exercises/_read_only.html.erb +33 -6
  37. data/app/views/layouts/_copyright.html.erb +1 -1
  38. data/app/views/layouts/_discussions.html.erb +21 -3
  39. data/app/views/layouts/_social_media.html.erb +3 -3
  40. data/app/views/layouts/_test_results.html.erb +1 -1
  41. data/app/views/layouts/exercise_inputs/editors/_custom.html.erb +1 -1
  42. data/app/views/layouts/exercise_inputs/forms/_kids_form.html.erb +1 -1
  43. data/app/views/layouts/exercise_inputs/forms/_problem_form.html.erb +1 -1
  44. data/app/views/layouts/exercise_inputs/layouts/_input_bottom.html.erb +1 -1
  45. data/app/views/layouts/exercise_inputs/layouts/_input_kindergarten.html.erb +40 -0
  46. data/app/views/layouts/exercise_inputs/layouts/{_input_kids.html.erb → _input_primary.html.erb} +1 -1
  47. data/app/views/layouts/exercise_inputs/layouts/_input_right.html.erb +1 -1
  48. data/app/views/layouts/modals/_kids_context.html.erb +1 -8
  49. data/app/views/user_mailer/1st_reminder.html.erb +3 -3
  50. data/app/views/user_mailer/1st_reminder.text.erb +1 -1
  51. data/app/views/user_mailer/2nd_reminder.html.erb +3 -3
  52. data/app/views/user_mailer/2nd_reminder.text.erb +1 -1
  53. data/app/views/user_mailer/3rd_reminder.html.erb +3 -3
  54. data/app/views/user_mailer/3rd_reminder.text.erb +1 -1
  55. data/app/views/user_mailer/no_submissions_reminder.html.erb +3 -3
  56. data/app/views/user_mailer/no_submissions_reminder.text.erb +1 -1
  57. data/config/routes.rb +2 -1
  58. data/lib/mumuki/laboratory/controllers/results_rendering.rb +1 -2
  59. data/lib/mumuki/laboratory/locales/en.yml +8 -2
  60. data/lib/mumuki/laboratory/locales/es.yml +7 -1
  61. data/lib/mumuki/laboratory/locales/pt.yml +8 -4
  62. data/lib/mumuki/laboratory/version.rb +1 -1
  63. data/spec/controllers/confirmations_controller_spec.rb +1 -1
  64. data/spec/controllers/discussions_messages_controller_spec.rb +73 -0
  65. data/spec/controllers/exercise_solutions_controller_spec.rb +41 -6
  66. data/spec/dummy/db/schema.rb +12 -1
  67. data/spec/features/discussion_flow_spec.rb +190 -0
  68. data/spec/features/exercise_flow_spec.rb +1 -1
  69. data/spec/features/menu_bar_spec.rb +88 -7
  70. data/spec/helpers/breadcrumbs_helper_spec.rb +1 -1
  71. data/spec/javascripts/bridge-spec.js +5 -0
  72. data/spec/javascripts/csrf-token-spec.js +7 -0
  73. data/spec/javascripts/elipsis-spec.js +25 -0
  74. data/spec/javascripts/results-renderers-spec.js +17 -0
  75. data/spec/javascripts/spec-helper.js +30 -0
  76. data/spec/javascripts/speech-bubble-renderer-spec.js +11 -0
  77. data/spec/javascripts/timeout-spec.js +5 -0
  78. data/spec/javascripts/timer-spec.js +5 -0
  79. data/spec/teaspoon_env.rb +187 -0
  80. metadata +33 -9
  81. data/app/views/layouts/modals/_new_discussion.html.erb +0 -27
  82. data/vendor/assets/javascripts/hotjar.js +0 -8
@@ -82,6 +82,7 @@ var mumuki = mumuki || {};
82
82
 
83
83
  mumuki.load(function () {
84
84
  mumuki.page.editors = createCodeMirrors();
85
+ mumuki.submission.registerContentSyncer(mumuki.editor.syncContent);
85
86
  updateCodeMirrorLanguage();
86
87
  onSelectUpdateCodeMirror();
87
88
 
@@ -1,21 +1,63 @@
1
+ /**
2
+ * @typedef {{name: string, value: string}} EditorProperty
3
+ */
4
+
5
+ /**
6
+ * @typedef {{getContent: () => EditorProperty}} CustomEditorSource
7
+ */
8
+
1
9
  var mumuki = mumuki || {};
2
10
 
3
11
  (function (mumuki) {
4
12
 
5
13
  var CustomEditor = {
14
+ /**
15
+ * @type {CustomEditorSource[]}
16
+ */
6
17
  sources: [],
7
18
 
8
- addSource: function (source) {
19
+ /**
20
+ * @param {CustomEditorSource} source
21
+ */
22
+ addSource(source) {
9
23
  CustomEditor.sources.push(source);
10
24
  },
11
25
 
12
- // Each external source must implement getContent method
26
+ /**
27
+ * @deprecated use getContents instead
28
+ */
29
+ getContent() {
30
+ return this.getContents();
31
+ },
13
32
 
14
- getContent: function () {
33
+ /**
34
+ * @returns {EditorProperty[]}
35
+ */
36
+ getContents() {
15
37
  return CustomEditor.sources.map( e => e.getContent() );
38
+ },
39
+
40
+ clearSources() {
41
+ this.sources = [];
42
+ },
43
+
44
+ get hasSources() {
45
+ return this.sources.length > 0;
16
46
  }
17
47
  };
18
48
 
19
- mumuki.CustomEditor = CustomEditor;
49
+ mumuki.load(() => {
50
+ mumuki.CustomEditor.clearSources();
51
+ });
20
52
 
53
+ /**
54
+ * This module allows custom editors to register
55
+ * content sources that can not me mapped to standard selectors {@code mu-custom-editor-value},
56
+ * {@code mu-custom-editor-extra} and {@code mu-custom-editor-test}
57
+ *
58
+ * CustomEditor sources are cleared after page reload even when using turbolinks
59
+ *
60
+ * @module mumuki.CustomEditor
61
+ */
62
+ mumuki.CustomEditor = CustomEditor;
21
63
  }(mumuki));
@@ -1,23 +1,13 @@
1
1
  var mumuki = mumuki || {};
2
2
 
3
3
  mumuki.load(function () {
4
- var $newDiscussionModal = $('.new-discussion-modal');
5
- var $newDiscussion = $('.discussion-create');
6
-
7
- $newDiscussion.click(function () {
8
- $newDiscussionModal.modal({
9
- backdrop: 'static',
10
- keyboard: false
11
- });
12
- });
13
-
14
4
  var $subscriptionSpans = $('.discussion-subscription > span');
15
5
  var $upvoteSpans = $('.discussion-upvote > span');
16
6
 
17
7
  function createNewMessageEditor() {
18
8
  var $textarea = $("#new-discussion-message");
19
9
  var textarea = $textarea[0];
20
- if(!textarea) return;
10
+ if (!textarea) return;
21
11
 
22
12
  return new mumuki.editor.CodeMirrorBuilder(textarea)
23
13
  .setupSimpleEditor()
@@ -39,7 +29,7 @@ mumuki.load(function () {
39
29
 
40
30
  createReadOnlyEditors();
41
31
  createNewMessageEditor();
42
-
32
+
43
33
  var Forum = {
44
34
  toggleButton: function (spans) {
45
35
  spans.toggleClass('hidden');
@@ -64,10 +54,21 @@ mumuki.load(function () {
64
54
  discussionPostAndToggle: function (url, elem) {
65
55
  Forum.discussionPost(url).done(Forum.toggleButton(elem))
66
56
  },
67
- discussionMessageToggleApprove : function (url, elem) {
57
+ discussionMessageToggleApprove: function (url, elem) {
68
58
  Forum.discussionPost(url).done(function () {
69
59
  elem.toggleClass("selected");
70
60
  })
61
+ },
62
+ discussionMessageToggleNotActuallyAQuestion: function (url, elem) {
63
+ Forum.discussionPost(url).done(function () {
64
+ elem.toggleClass("selected");
65
+ })
66
+ },
67
+ discussionsToggleCheckbox: function (elem) {
68
+ const key = elem.attr('name');
69
+ const params = new URLSearchParams(location.search);
70
+ elem.is(':checked') ? params.set(key, elem.val()) : params.delete(key);
71
+ location.search = params.toString();
71
72
  }
72
73
  };
73
74
 
@@ -1,20 +1,36 @@
1
1
  mumuki.load(function () {
2
- var $bubble = $('.mu-kids-character-speech-bubble').children('.mu-kids-character-speech-bubble-normal');
3
-
4
- var availableTabs = ['.description', '.hint'];
5
- var $speechParagraphs, paragraphHeight, scrollHeight, nextSpeechBlinking;
6
- var currentParagraphIndex = 0;
7
- var paragraphCount = 1;
8
- var paragraphsLines = 2;
9
- var $prevSpeech = $('.mu-kids-character-speech-bubble-normal > .mu-kids-prev-speech').hide();
10
- var $nextSpeech = $('.mu-kids-character-speech-bubble-normal > .mu-kids-next-speech');
11
- var $speechTabs = $('.mu-kids-character-speech-bubble-tabs > li:not(.separator)');
12
- var $defaultSpeechTabName = 'description';
13
- var $texts = $bubble.children(availableTabs.join(", "));
14
- var $hint = $('.mu-kids-hint');
15
- var $description = $('.mu-kids-description');
16
- var discussionsLinkHtml = $('#mu-kids-discussion-link-html').html();
17
- var contextModalButton = new mumuki.Button($('.mu-kids-context .modal-footer button'));
2
+ let $bubble = $('.mu-kids-character-speech-bubble').children('.mu-kids-character-speech-bubble-normal');
3
+
4
+ let availableTabs = ['.description', '.hint'];
5
+ let $speechParagraphs, paragraphHeight, scrollHeight, nextSpeechBlinking;
6
+ let currentParagraphIndex = 0;
7
+ let paragraphCount = 1;
8
+ let paragraphsLines = 2;
9
+ let $prevSpeech = $('.mu-kids-character-speech-bubble-normal > .mu-kids-prev-speech').hide();
10
+ let $nextSpeech = $('.mu-kids-character-speech-bubble-normal > .mu-kids-next-speech');
11
+ let $speechTabs = $('.mu-kids-character-speech-bubble-tabs > li:not(.separator)');
12
+ let $defaultSpeechTabName = 'description';
13
+ let $texts = $bubble.children(availableTabs.join(", "));
14
+ let $hint = $('.mu-kids-hint');
15
+ let $description = $('.mu-kids-description');
16
+ let discussionsLinkHtml = $('#mu-kids-discussion-link-html').html();
17
+ let $kidsContext = $('#mu-kids-context');
18
+ let contextModalButton = new mumuki.Button($('.mu-kids-context .modal-footer button'));
19
+
20
+ // It is important that context is shown as early as possible
21
+ // in order to display the loading animation
22
+ function showContext() {
23
+ $kidsContext.modal({
24
+ backdrop: 'static',
25
+ keyboard: false
26
+ });
27
+ }
28
+
29
+ $kidsContext.on('hidden.bs.modal', function () {
30
+ animateSpeech();
31
+ });
32
+
33
+ showContext();
18
34
 
19
35
  function floatFromPx(value) {
20
36
  return parseFloat(value.substring(0, value.length - 2));
@@ -57,6 +73,25 @@ mumuki.load(function () {
57
73
  isVisible ? element.show() : element.hide();
58
74
  }
59
75
 
76
+ /**
77
+ * Assigns propert widths to the states and blocks areas
78
+ * depending on the presence and type of available states
79
+ *
80
+ * @param {*} $muKidsStateImage
81
+ * @param {*} $muKidsStatesContainer
82
+ * @param {*} $muKidsBlocks
83
+ * @param {number} fullMargin
84
+ */
85
+ function distributeAreas($muKidsStateImage, $muKidsStatesContainer, $muKidsBlocks, fullMargin) {
86
+ if ($muKidsStateImage.children().length) {
87
+ var ratio = $muKidsStatesContainer.hasClass('mu-kids-single-state') ? 1 : 2;
88
+ $muKidsStatesContainer.width($muKidsStatesContainer.height() / ratio * 1.25 - fullMargin);
89
+ } else {
90
+ $muKidsStatesContainer.width(0);
91
+ $muKidsBlocks.width('100%');
92
+ }
93
+ }
94
+
60
95
  mumuki.kids = {
61
96
 
62
97
  // ==========
@@ -134,6 +169,8 @@ mumuki.load(function () {
134
169
  contextModalButton.enable();
135
170
  },
136
171
 
172
+ showContext,
173
+
137
174
  // ===========
138
175
  // Private API
139
176
  // ===========
@@ -274,9 +311,6 @@ mumuki.load(function () {
274
311
  mumuki.kids.resultAction.errored = mumuki.kids._showOnCharacterBubble;
275
312
  mumuki.kids.resultAction.pending = mumuki.kids._showOnCharacterBubble;
276
313
 
277
- $('.mu-kids-context').on('hidden.bs.modal', function () {
278
- animateSpeech();
279
- });
280
314
 
281
315
  $(document).ready(() => {
282
316
  // Speech initialization
@@ -292,13 +326,15 @@ mumuki.load(function () {
292
326
 
293
327
  $speechTabs.each(function (i) {
294
328
  var $tab = $($speechTabs[i]);
295
- $tab.click(function () {
296
- $speechTabs.removeClass('active');
297
- $tab.addClass('active');
298
- $texts.hide();
299
- $bubble.children('.' + $tab.data('target')).show();
300
- updateSpeechParagraphs();
301
- })
329
+ if ($tab.data('target')) {
330
+ $tab.click(function () {
331
+ $speechTabs.removeClass('active');
332
+ $tab.addClass('active');
333
+ $texts.hide();
334
+ $bubble.children('.' + $tab.data('target')).show();
335
+ updateSpeechParagraphs();
336
+ })
337
+ }
302
338
  });
303
339
 
304
340
  if (paragraphCount > 1) {
@@ -315,24 +351,25 @@ mumuki.load(function () {
315
351
  });
316
352
 
317
353
  // States initial resizing
318
-
319
354
  mumuki.resize(function () {
320
355
  var margin = 15;
321
356
  var fullMargin = margin * 2;
322
357
 
323
- var $muKidsStatesContainer = $('.mu-kids-states');
324
- var $muKidsStates = $('.mu-kids-state');
325
-
326
- var dimension = $muKidsStatesContainer.height() / 2 * 1.25 - fullMargin;
327
- $muKidsStatesContainer.width(dimension);
358
+ let $muKidsStatesContainer = $('.mu-kids-states');
359
+ let $muKidsStates = $('.mu-kids-state');
360
+ let $muKidsBlocks = $('.mu-kids-blocks');
361
+ let $muKidsExercise = $('.mu-kids-exercise');
362
+ let $muKidsExerciseDescription = $('.mu-kids-exercise-description');
363
+ let $muKidsStateImage = $('.mu-kids-state-image');
328
364
 
329
- var $muKidsExercise = $('.mu-kids-exercise');
330
- var $muKidsExerciseDescription = $('.mu-kids-exercise-description');
365
+ distributeAreas($muKidsStateImage, $muKidsStatesContainer, $muKidsBlocks, fullMargin);
331
366
 
332
- $muKidsExerciseDescription.width($muKidsExercise.width() - $muKidsStatesContainer.width() - margin);
367
+ if (!$muKidsExerciseDescription.hasClass('mu-kids-exercise-description-fixed')) {
368
+ $muKidsExerciseDescription.width($muKidsExercise.width() - $muKidsStatesContainer.width() - margin);
369
+ }
333
370
 
334
371
  $muKidsStates.each((index, state) => mumuki.kids.scaleState($(state), fullMargin));
335
- mumuki.kids.scaleBlocksArea($('.mu-kids-blocks'));
372
+ mumuki.kids.scaleBlocksArea($muKidsBlocks);
336
373
 
337
374
  if (paragraphCount <= 1) clearInterval(nextSpeechBlinking);
338
375
 
@@ -2,6 +2,9 @@ var mumuki = mumuki || {};
2
2
 
3
3
  (function (mumuki) {
4
4
 
5
+ /**
6
+ * Updates the current exercise progress indicator
7
+ * */
5
8
  mumuki.updateProgressBarAndShowModal = function (data) {
6
9
  $('.progress-list-item.active').attr('class', data.class_for_progress_list_item);
7
10
  if(data.guide_finished_by_solution) $('#guide-done').modal();
@@ -0,0 +1,51 @@
1
+ (() => {
2
+
3
+ // ==========================
4
+ // View function for building
5
+ // the results UI
6
+ // ==========================
7
+
8
+ /**
9
+ * @param {string} status
10
+ * @returns {string}
11
+ */
12
+ function iconForStatus(status) {
13
+ switch (status) {
14
+ case "errored": return "fa-minus-circle";
15
+ case "failed": return "fa-times-circle";
16
+ case "passed_with_warnings": return "fa-exclamation-circle";
17
+ case "passed": return "fa-check-circle";
18
+ case "pending": return "fa-circle";
19
+ }
20
+ }
21
+
22
+ /**
23
+ *
24
+ * @param {string} status
25
+ * @returns {string}
26
+ */
27
+ function classForStatus(status) {
28
+ switch (status) {
29
+ case "errored": return "broken";
30
+ case "failed": return "danger";
31
+ case "passed_with_warnings": return "warning";
32
+ case "passed": return "success";
33
+ case "pending": return "muted";
34
+ }
35
+ };
36
+
37
+
38
+ /**
39
+ * @param {string} status
40
+ * @param {boolean} [active]
41
+ * @returns {string}
42
+ */
43
+ function progressListItemClassForStatus(status, active = false) {
44
+ return `progress-list-item text-center ${classForStatus(status)} ${active ? 'active' : ''}`;
45
+ };
46
+
47
+ mumuki.renderers = mumuki.renderers || {};
48
+ mumuki.renderers.classForStatus = classForStatus;
49
+ mumuki.renderers.iconForStatus = iconForStatus;
50
+ mumuki.renderers.progressListItemClassForStatus = progressListItemClassForStatus;
51
+ })();
@@ -1,6 +1,19 @@
1
1
  var mumuki = mumuki || {};
2
2
 
3
3
  (function (mumuki) {
4
+
5
+ // =============
6
+ // UI Components
7
+ // =============
8
+
9
+ function animateTimeoutError(submitButton) {
10
+ let scene = new muvment.Scene($('.submission-result-error-animation'));
11
+ scene.addState(mumuki.errorState('timeout_1').onStart(submitButton.setOriginalContent.bind(submitButton)).onEndSwitch(scene, 'timeout_2'))
12
+ .addState(mumuki.errorState('timeout_2').onEndSwitch(scene, 'timeout_3'))
13
+ .addState(mumuki.errorState('timeout_3').onStart(submitButton.enable.bind(submitButton)))
14
+ .play();
15
+ }
16
+
4
17
  function ResultsBox(submissionsResults) {
5
18
  this.submissionsResultsArea = submissionsResults;
6
19
  this.processingTemplate = $('#processing-template');
@@ -44,31 +57,133 @@ var mumuki = mumuki || {};
44
57
  this.preventClick();
45
58
  }
46
59
  }
60
+ }
61
+
62
+ // ============
63
+ // Content Sync
64
+ // ============
47
65
 
66
+ /**
67
+ * Syncs and returns the content objects of the standard editor form
68
+ *
69
+ * This content object may include keys like {@code content},
70
+ * {@code content_extra} and {@code content_test}
71
+ *
72
+ * @returns {EditorProperty[]}
73
+ */
74
+ function getStandardEditorContents() {
75
+ mumuki.submission._syncContent();
76
+ return $('.new_solution').serializeArray();
48
77
  }
49
78
 
50
- mumuki.load(function () {
51
- var submissionsResults = $('.submission-results');
52
- if (!submissionsResults) return;
79
+ /**
80
+ * Answers a content object with a key for each of the current
81
+ * editor sources.
82
+ *
83
+ * This method will use CustomEditor's sources if availble, or
84
+ * standard editor's content sources otherwise
85
+ */
86
+ function getContent() {
87
+ let content = {};
88
+ let contents;
89
+
90
+ if (mumuki.CustomEditor.hasSources) {
91
+ contents = mumuki.CustomEditor.getContents();
92
+ } else {
93
+ contents = mumuki.submission.getStandardEditorContents();
94
+ }
53
95
 
54
- var resultsBox = new ResultsBox(submissionsResults);
96
+ contents.forEach((it) => {
97
+ content[it.name] = it.value;
98
+ });
99
+
100
+ return content;
101
+ }
55
102
 
56
- var btnSubmit = $('.btn-submit');
57
- var submissionControl = $('.submission_control');
103
+ /**
104
+ * Copies current solution from it native rendering components
105
+ * to the appropriate submission form elements.
106
+ *
107
+ * Both editors and runners with a custom editor that don't register a source should
108
+ * register its own syncer function in order to {@link syncContent} work properly.
109
+ *
110
+ * @see registerContentSyncer
111
+ * @see CustomEditor#addSource
112
+ */
113
+ function _syncContent() {
114
+ if (mumuki.submission._contentSyncer) {
115
+ mumuki.submission._contentSyncer();
116
+ }
117
+ }
58
118
 
59
- var submitButton = new SubmitButton(btnSubmit, submissionControl);
119
+ /**
120
+ * Sets a content syncer, that will be used by {@link _syncContent}
121
+ * in ordet to dump solution into the submission form fields.
122
+ *
123
+ * Each editor should have its own syncer registered - otherwise previous or none may be used
124
+ * causing unpredicatble behaviours - or cleared by passing {@code null}.
125
+ *
126
+ * As a particular case, runners with custom editors that don't add sources using {@link CustomEditor#addSource}
127
+ * should set the {@code #mu-custom-editor-value} value within its syncer.
128
+ *
129
+ * @param {() => void} [syncer] the syncer, or null, if no sync'ing is needed
130
+ */
131
+ function registerContentSyncer(syncer = null) {
132
+ mumuki.submission._contentSyncer = syncer;
133
+ }
60
134
 
61
- var bridge = new mumuki.bridge.Laboratory;
135
+ // ==========
136
+ // Processing
137
+ // ==========
62
138
 
63
- btnSubmit.on('click', function (e) {
64
- e.preventDefault();
139
+ /**
140
+ * Process solution, which consist of making buttons wait, sending to server, rendering results,
141
+ * restoring buttons state.
142
+ *
143
+ * The actual implementation of this method depends on contextual {@link _solutionProcessor}, which can
144
+ * be configured using {@link _registerSolutionProcessor}. Currently there are only two available processors -
145
+ * {@link _kidsSolutionProcessor} and {@link _classicSolutionProcessor} - which are automatically choosen depending
146
+ * on the exercise DOM.
147
+ *
148
+ * @param {Submission} solution
149
+ */
150
+ function processSolution(solution) {
151
+ mumuki.submission._solutionProcessor(solution);
152
+ }
153
+
154
+ /**
155
+ * Configures a callback for processing a solution.
156
+ *
157
+ * This method is called internally by {@link _selectSolutionProcessor}
158
+ * and should normally not be called by runners editor, but is exposed
159
+ * for further non-standard customizations.
160
+ *
161
+ * @param {({solution: object}) => void} processor
162
+ */
163
+ function _registerSolutionProcessor(processor) {
164
+ mumuki.submission._solutionProcessor = processor;
165
+ }
166
+
167
+ /** Processor for kids layouts */
168
+ function _kidsSolutionProcessor(bridge, submitButton) {
169
+ return (solution) => {
170
+ submitButton.wait();
171
+ bridge._submitSolution(solution).always(function (data) {
172
+ submitButton.ready(() => {
173
+ mumuki.kids.restart();
174
+ submitButton.continue();
175
+ });
176
+ mumuki.kids.showResult(data);
177
+ });
178
+ }
179
+ }
180
+
181
+ /** Processor for non-kids layouts */
182
+ function _classicSolutionProcessor(bridge, submitButton, resultsBox) {
183
+ return (solution) => {
65
184
  submitButton.disable();
66
185
  submitButton.setWaitingText();
67
186
  resultsBox.waiting();
68
-
69
- mumuki.editor.syncContent();
70
- var solution = getContent();
71
-
72
187
  bridge._submitSolution(solution).done(function (data) {
73
188
  resultsBox.success(data, submitButton);
74
189
  }).fail(function () {
@@ -77,36 +192,70 @@ var mumuki = mumuki || {};
77
192
  $(document).renderMuComponents();
78
193
  resultsBox.done(data, submitButton);
79
194
  });
80
- });
81
-
82
- submitButton.checkAttemptsLeft();
83
- });
195
+ }
196
+ }
84
197
 
85
- function getEditorsContent() {
86
- return $('.new_solution').serializeArray().concat(mumuki.CustomEditor.getContent())
198
+ /** Selects the most appropriate solution processor */
199
+ function _selectSolutionProcessor(submitButton, $submissionsResults) {
200
+ const bridge = new mumuki.bridge.Laboratory();
201
+ let processor;
202
+ if ($('.mu-kids-exercise').length) {
203
+ processor = _kidsSolutionProcessor(bridge, submitButton);
204
+ } else {
205
+ processor = _classicSolutionProcessor(bridge, submitButton, new ResultsBox($submissionsResults));
206
+ }
207
+ mumuki.submission._registerSolutionProcessor(processor);
87
208
  }
88
209
 
89
- function getContent(){
90
- var content = {};
91
210
 
92
- getEditorsContent().forEach(function(it) {
93
- content[it.name] = it.value;
211
+ // ===========
212
+ // Entry Point
213
+ // ===========
214
+
215
+ mumuki.load(function () {
216
+ var $submissionsResults = $('.submission-results');
217
+ if (!$submissionsResults) return;
218
+
219
+ const $btnSubmit = $('.btn-submit');
220
+ const submitButton = new SubmitButton($btnSubmit, $('.submission_control'));
221
+
222
+ mumuki.submission._selectSolutionProcessor(submitButton, $submissionsResults);
223
+
224
+ submitButton.start(() => {
225
+ var solution = mumuki.submission.getContent();
226
+ mumuki.submission.processSolution(solution);
94
227
  });
95
228
 
96
- return content;
97
- }
229
+ submitButton.checkAttemptsLeft();
230
+ });
98
231
 
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
232
 
233
+ /**
234
+ * This module contains methods for submitting solution in at high level, dealing with network communication,
235
+ * and layout-sensitive UI updates. It is intended to be both used internally by standard editors and by runners
236
+ * custom editors.
237
+ *
238
+ * Runners can choose to bypass this module under kids layouts, and handling all that low-level details. In order
239
+ * to do that {@code .mu-kids-submit-button} selector must be overiden. Customizing submission in classic layout
240
+ * or in a layout-agnostic way can be accomplish by overriding {@code .mu-submit-button}.
241
+ *
242
+ * @see mumuki.kids.showResult
243
+ * @see mumuki.bridge.Laboratory.runTests
244
+ *
245
+ * @module mumuki.submission
246
+ */
107
247
  mumuki.submission = {
108
- animateTimeoutError: animateTimeoutError,
109
- SubmitButton: SubmitButton
248
+ processSolution,
249
+ _registerSolutionProcessor,
250
+ _selectSolutionProcessor,
251
+
252
+ _syncContent,
253
+ registerContentSyncer,
254
+ getStandardEditorContents,
255
+ getContent,
256
+
257
+ animateTimeoutError,
258
+ SubmitButton,
110
259
  };
111
260
 
112
261
  })(mumuki);