mumuki-laboratory 7.10.4 → 7.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -7
  3. data/Rakefile +9 -2
  4. data/app/assets/javascripts/mumuki_laboratory/application/bridge.js +26 -5
  5. data/app/assets/javascripts/mumuki_laboratory/application/characters.js +3 -1
  6. data/app/assets/javascripts/mumuki_laboratory/application/gamification.js +85 -0
  7. data/app/assets/javascripts/mumuki_laboratory/application/kids.js +160 -334
  8. data/app/assets/javascripts/mumuki_laboratory/application/kindergarten.js +200 -0
  9. data/app/assets/javascripts/mumuki_laboratory/application/primary.js +257 -0
  10. data/app/assets/javascripts/mumuki_laboratory/application/profile.js +31 -16
  11. data/app/assets/javascripts/mumuki_laboratory/application/submission.js +1 -0
  12. data/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js +19 -2
  13. data/app/assets/stylesheets/mumuki_laboratory/application/_errors.scss +2 -4
  14. data/app/assets/stylesheets/mumuki_laboratory/application/_modules.scss +3 -1
  15. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_avatar.scss +21 -0
  16. data/app/assets/stylesheets/mumuki_laboratory/application/modules/{_chapter_show.scss → _content_show.scss} +0 -0
  17. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kindergarten.scss +421 -12
  18. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_medal.scss +48 -0
  19. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_terms.scss +44 -0
  20. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_user_profile.scss +9 -0
  21. data/app/controllers/application_controller.rb +15 -8
  22. data/app/controllers/book_discussions_controller.rb +4 -0
  23. data/app/controllers/users_controller.rb +8 -2
  24. data/app/helpers/application_helper.rb +2 -2
  25. data/app/helpers/avatar_helper.rb +11 -3
  26. data/app/helpers/contextualization_result_helper.rb +9 -1
  27. data/app/helpers/kindergarten_helper.rb +5 -0
  28. data/app/helpers/links_helper.rb +8 -0
  29. data/app/helpers/medal_helper.rb +36 -0
  30. data/app/helpers/open_graph_helper.rb +2 -2
  31. data/app/helpers/organization_list_helper.rb +1 -1
  32. data/app/helpers/overlapped_buttons_helper.rb +1 -1
  33. data/app/helpers/page_title_helper.rb +2 -2
  34. data/app/helpers/profile_helper.rb +9 -1
  35. data/app/views/book/_header.html.erb +17 -0
  36. data/app/views/book/show.html.erb +1 -18
  37. data/app/views/discussions/terms.html.erb +10 -0
  38. data/app/views/exercise_solutions/_kids_results.html.erb +1 -1
  39. data/app/views/exercises/show.html.erb +1 -0
  40. data/app/views/invitations/_invitation_form.html.erb +1 -0
  41. data/app/views/layouts/_discussions.html.erb +5 -1
  42. data/app/views/layouts/_error.html.erb +3 -6
  43. data/app/views/layouts/_guide.html.erb +10 -3
  44. data/app/views/layouts/_kindergarten.html.erb +38 -0
  45. data/app/views/layouts/_main.html.erb +3 -1
  46. data/app/views/layouts/_organization_chooser.html.erb +0 -7
  47. data/app/views/layouts/_terms_acceptance_disclaimer.html.erb +6 -0
  48. data/app/views/layouts/application.html.erb +3 -2
  49. data/app/views/layouts/exercise_inputs/layouts/_input_kindergarten.html.erb +27 -27
  50. data/app/views/layouts/modals/_guide_corollary.html.erb +9 -0
  51. data/app/views/layouts/modals/_kindergarten_context.html.erb +30 -0
  52. data/app/views/layouts/modals/_kindergarten_results.html.erb +23 -0
  53. data/app/views/layouts/modals/_kindergarten_results_aborted.html.erb +27 -0
  54. data/app/views/users/_avatar_list.html.erb +6 -2
  55. data/app/views/users/_edit_user_form.html.erb +9 -4
  56. data/app/views/users/_term.html.erb +10 -0
  57. data/app/views/users/_user_form.html.erb +5 -1
  58. data/app/views/users/terms.html.erb +18 -0
  59. data/config/routes.rb +2 -0
  60. data/lib/mumuki/laboratory.rb +1 -1
  61. data/lib/mumuki/laboratory/controllers/current_organization.rb +1 -1
  62. data/lib/mumuki/laboratory/controllers/results_rendering.rb +3 -2
  63. data/lib/mumuki/laboratory/locales/en.yml +16 -5
  64. data/lib/mumuki/laboratory/locales/es-CL.yml +5 -4
  65. data/lib/mumuki/laboratory/locales/es.yml +17 -6
  66. data/lib/mumuki/laboratory/locales/pt.yml +16 -5
  67. data/lib/mumuki/laboratory/version.rb +1 -1
  68. data/spec/capybara_helper.rb +99 -0
  69. data/spec/controllers/exercise_solutions_controller_spec.rb +3 -4
  70. data/spec/dummy/db/schema.rb +37 -1
  71. data/spec/dummy/public/medal/outline.svg +1089 -0
  72. data/spec/features/choose_organization_spec.rb +12 -30
  73. data/spec/features/disable_user_flow_spec.rb +3 -5
  74. data/spec/features/disabled_organization_flow_spec.rb +9 -14
  75. data/spec/features/exercise_flow_spec.rb +2 -2
  76. data/spec/features/guide_reset_spec.rb +1 -1
  77. data/spec/features/guides_flow_spec.rb +1 -1
  78. data/spec/features/home_private_flow_spec.rb +1 -3
  79. data/spec/features/home_public_flow_spec.rb +6 -12
  80. data/spec/features/invitations_flow_spec.rb +2 -2
  81. data/spec/features/login_flow_spec.rb +2 -2
  82. data/spec/features/not_found_private_flow_spec.rb +4 -4
  83. data/spec/features/not_found_public_flow_spec.rb +1 -6
  84. data/spec/features/profile_flow_spec.rb +1 -1
  85. data/spec/helpers/page_title_helper_spec.rb +3 -3
  86. data/spec/javascripts/editors-spec.js +23 -0
  87. data/spec/javascripts/gamification-spec.js +58 -0
  88. data/spec/javascripts/submissions-store-spec.js +139 -6
  89. data/spec/spec_helper.rb +2 -0
  90. data/spec/teaspoon_env.rb +24 -6
  91. metadata +41 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9ec0735be65d4978b877d62bc5542277c1ddff2cf9d1bff48454ee4d874f1fb
4
- data.tar.gz: 53e5b0019c91b60f0d2e7a07523941640f0b46118d74a04f4035a37efaafaaf1
3
+ metadata.gz: e025a36f6aa947e6a39b8372e2a0f257d473143a9c5bef32575885fcf375312b
4
+ data.tar.gz: 81428a5522e48781ed29cde5b8f2c219f3388a8d36e1ae466f91ee412e69b246
5
5
  SHA512:
6
- metadata.gz: 50ccfb5349f8734c2158041465158ecf5fb3665334ad3c5ba75fcea1f11ecaf52774c5a8d010e014df621ab47115101bd7a9a2c4e222396164c9f479f001d546
7
- data.tar.gz: 67262d351678e0106d736a585798f9133917e10285c885fea4a9cfa8b02ddd0f19c15fb36c47659f5548d91796279a93f29e93d37ae80b07902f70825117ccd6
6
+ metadata.gz: 374aec02893b3e31880b6bb3bbaf1031e0c2b567ee1b25b8d03bb7a30bda810aa72f5ff9974d70cab6637cede23a8e734e95ec5e9a1357d14772300e64a0fc6f
7
+ data.tar.gz: 70637e35aa57f98661f78f787a15e181ed4737d4b1d3080adcf06e90f4564e5cec9b642055106cccf6ae29fbaa90b1695367c7ac424c89b225166f80ab007557
data/README.md CHANGED
@@ -1,6 +1,5 @@
1
- [![Build Status](https://travis-ci.org/mumuki/mumuki-laboratory.svg?branch=master)](https://travis-ci.org/mumuki/mumuki-laboratory)
1
+ [![Build Status](https://travis-ci.com/mumuki/mumuki-laboratory.svg?branch=master)](https://travis-ci.com/mumuki/mumuki-laboratory)
2
2
  [![Code Climate](https://codeclimate.com/github/mumuki/mumuki-laboratory/badges/gpa.svg)](https://codeclimate.com/github/mumuki/mumuki-laboratory)
3
- [![Test Coverage](https://codeclimate.com/github/mumuki/mumuki-laboratory/badges/coverage.svg)](https://codeclimate.com/github/mumuki/mumuki-laboratory)
4
3
  [![Issue Count](https://codeclimate.com/github/mumuki/mumuki-laboratory/badges/issue_count.svg)](https://codeclimate.com/github/mumuki/mumuki-laboratory)
5
4
 
6
5
  <img width="60%" src="https://raw.githubusercontent.com/mumuki/mumuki-laboratory/master/laboratory-screenshot.png"></img>
@@ -132,16 +131,35 @@ rails s
132
131
  ## Running tests
133
132
 
134
133
  ```bash
135
- bundle exec rspec
134
+ # Run all tests
135
+ bundle exec rake
136
+
137
+ # Run only web tests (i.e. Capybara and Teaspoon)
138
+ bundle exec rake spec:web
139
+ ```
140
+
141
+ ## Running Capybara tests with Selenium
142
+
143
+ The Capybara config of this project supports running tests on Firefox, Chrome and Safari via Selenium. The [`webdrivers`](https://github.com/titusfortner/webdrivers) gem automatically installs (and updates) all the necessary Selenium webdrivers.
144
+
145
+ By default, Capybara tests will run with the default dummy-driver (Rack test). If you want to run on a real browser, you should set `MUMUKI_SELENIUM_DRIVER` variable to `firefox`, `chrome` or `safari`. Also, a Rake task to run just the Capybara tests is available.
146
+
147
+ Some examples:
148
+
149
+ ```bash
150
+ # Run web tests, using Firefox
151
+ MUMUKI_SELENIUM_DRIVER=firefox bundle exec rake spec:web
152
+
153
+ # Run Capybara tests on Chrome
154
+ MUMUKI_SELENIUM_DRIVER=chrome bundle exec rake spec:web:capybara
136
155
  ```
137
156
 
138
157
  ## Running JS tests
139
158
 
140
- > You need first to download [geckodriver](https://github.com/mozilla/geckodriver/releases/download/v0.27.0/geckodriver-v0.27.0-linux64.tar.gz), uncrompress
141
- > it and add it to your path
159
+ The [`webdrivers`](https://github.com/titusfortner/webdrivers) gem also works with Teaspoon, no need to install anything manually. By default tests run on Firefox, but this behavior can be changed by setting `MUMUKI_SELENIUM_DRIVER` (see section above).
142
160
 
143
161
  ```bash
144
- MOZ_HEADLESS=1 bundle exec rake teaspoon
162
+ bundle exec rake spec:web:teaspoon
145
163
  ```
146
164
 
147
165
  ## Running `eslint`
@@ -278,7 +296,8 @@ which are granted to be safe and stable.
278
296
  "status": "passed|passed_with_warnings|failed",
279
297
  "guide_finished_by_solution": "boolean",
280
298
  "html": "string",
281
- "remaining_attempts_html": "string" ,
299
+ "remaining_attempts_html": "string",
300
+ "current_exp": "integer",
282
301
  "title_html": "string", // kids-only
283
302
  "button_html": "string", // kids-only
284
303
  "expectations": [ // kids-only
data/Rakefile CHANGED
@@ -25,7 +25,7 @@ require 'rspec/core'
25
25
  require 'rspec/core/rake_task'
26
26
 
27
27
  desc "Run all specs in spec directory (excluding plugin specs)"
28
- RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
28
+ RSpec::Core::RakeTask.new
29
29
 
30
30
  desc "Force development environment, required by javascript specs"
31
31
  task :development do
@@ -33,8 +33,15 @@ task :development do
33
33
  ENV['RAILS_ENV'] = 'development'
34
34
  end
35
35
 
36
+ RSpec::Core::RakeTask.new('spec:web:capybara') do |t|
37
+ t.pattern = 'spec/features/*_spec.rb'
38
+ end
39
+
36
40
  desc "Run the javascript specs"
37
- task teaspoon: [:development, "app:teaspoon"]
41
+ task 'spec:web:teaspoon': [:development, "app:teaspoon"]
42
+
43
+ desc "Run all the web-related tests"
44
+ task 'spec:web': ['spec:web:capybara', 'spec:web:teaspoon']
38
45
 
39
46
  task default: :spec
40
47
 
@@ -22,11 +22,32 @@
22
22
  */
23
23
 
24
24
  /**
25
- * @typedef {{
26
- * "solution[content]"?:string,
27
- * solution?: Solution,
28
- * client_result?: SubmissionClientResult
29
- * }} Submission
25
+ * Contents of a submission expressed in the form of params
26
+ * generated and accepted by laboratory HTTP Controllers
27
+ *
28
+ * @typedef {{"solution[content]":string}} ClassicContents
29
+ */
30
+
31
+ /**
32
+ * Contents of a submission expressed as an object
33
+ * that are created programatically
34
+ *
35
+ * @typedef {{solution: Solution}} ProgramaticContents
36
+ */
37
+
38
+ /**
39
+ * Contents of a multifile submission expressed as dictionary of keys in the form
40
+ * `solution[content[the-filename-goes-here]]`
41
+ *
42
+ * @typedef {object} MutifileContents
43
+ */
44
+
45
+ /**
46
+ * @typedef {ClassicContents|ProgramaticContents|MutifileContents} Contents
47
+ */
48
+
49
+ /**
50
+ * @typedef {Contents & {client_result?: SubmissionClientResult}} Submission
30
51
  */
31
52
 
32
53
  /**
@@ -9,8 +9,10 @@ mumuki.load(() => {
9
9
  });
10
10
 
11
11
  function placeKidsAnimations() {
12
+ placeAnimation('.mu-kids-character-result-aborted', 'failure');
12
13
  placeAnimation('.mu-kids-character-animation', 'blink');
13
14
  placeAnimation('.mu-kids-character-context', 'context');
15
+ placeAnimation('.mu-kids-character-result', 'blink');
14
16
  }
15
17
 
16
18
  function placeAnimation(selector, clip) {
@@ -20,7 +22,7 @@ mumuki.load(() => {
20
22
 
21
23
  function atRandom(array) {
22
24
  return array[Math.floor(Math.random() * array.length)];
23
- }
25
+ }
24
26
 
25
27
  mumuki.characters = characters;
26
28
  });
@@ -0,0 +1,85 @@
1
+ mumuki.gamification = (() => {
2
+ class Formula {
3
+ static get QUADRATIC_COEFFICIENT() { return 25; }
4
+ static get LINEAR_COEFFICIENT() { return 100; }
5
+ static get CONSTANT_TERM() { return -125; }
6
+ }
7
+
8
+ class LevelProgression {
9
+ constructor(currentExp) {
10
+ this.currentExp = currentExp;
11
+ }
12
+
13
+ expToLevelUp() {
14
+ return this.baseExpNextLevel() - this.currentExp;
15
+ }
16
+
17
+ baseExpNextLevel() {
18
+ return this.expFor(this.currentLevel() + 1);
19
+ }
20
+
21
+ expFor(level) {
22
+ const ax2 = Formula.QUADRATIC_COEFFICIENT * Math.pow(level, 2);
23
+ const bx = Formula.LINEAR_COEFFICIENT * level;
24
+ const c = Formula.CONSTANT_TERM;
25
+
26
+ return ax2 + bx + c;
27
+ }
28
+
29
+ currentLevel() {
30
+ return this.levelFor(this.currentExp);
31
+ }
32
+
33
+ levelFor(exp) {
34
+ const a = Formula.QUADRATIC_COEFFICIENT;
35
+ const b = Formula.LINEAR_COEFFICIENT;
36
+ const c = Formula.CONSTANT_TERM;
37
+
38
+ return Math.floor((-b + Math.sqrt(Math.pow(b, 2) - 4 * a * (c - exp))) / (2 * a));
39
+ }
40
+
41
+ triggersLevelChange(exp) {
42
+ return this.levelFor(exp) !== this.currentLevel();
43
+ }
44
+
45
+ currentLevelProgress() {
46
+ return (this.currentExp - this.baseExpCurrentLevel()) / (this.baseExpNextLevel() - this.baseExpCurrentLevel());
47
+ }
48
+
49
+ baseExpCurrentLevel() {
50
+ return this.expFor(this.currentLevel());
51
+ }
52
+
53
+ setExpMessage(exp) {
54
+ let expGained = exp - this.currentExp;
55
+
56
+ if (expGained > 0) {
57
+ this.currentExp = exp;
58
+ $('#mu-exp-points').html(expGained);
59
+ $('#mu-level-number').html(this.currentLevel());
60
+ }
61
+ }
62
+ }
63
+
64
+ function _setUpCurrentLevelProgression() {
65
+ mumuki.gamification._currentLevelProgression = new LevelProgression(currentExp());
66
+ }
67
+
68
+ function currentExp() {
69
+ return $('#mu-current-exp').val();
70
+ }
71
+
72
+ return {
73
+ Formula,
74
+ LevelProgression,
75
+
76
+ _setUpCurrentLevelProgression,
77
+
78
+ /** @type {LevelProgression} */
79
+ _currentLevelProgression: null
80
+ };
81
+ })();
82
+
83
+ mumuki.load(() => {
84
+ mumuki.gamification._setUpCurrentLevelProgression();
85
+ });
@@ -1,379 +1,205 @@
1
- mumuki.load(() => {
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({
1
+ mumuki.Kids = class {
2
+
3
+ constructor() {
4
+ this.initialize();
5
+ this.showContext();
6
+ $(document).ready(this.onReady.bind(this));
7
+ }
8
+
9
+ // ================
10
+ // == Public API ==
11
+ // ================
12
+
13
+ initialize() {
14
+ this.submitButton = new mumuki.submission.SubmitButton($('#kids-btn-retry'), $('.submission_control'));
15
+ this.resultActions = {};
16
+ this.$states = $('.mu-kids-states');
17
+ this.$state = $('.mu-kids-state');
18
+ this.$blocks = $('.mu-kids-blocks');
19
+ this.$exercise = $('.mu-kids-exercise');
20
+ this.$exerciseDescription = $('.mu-kids-exercise-description');
21
+ this.$stateImage = $('.mu-kids-state-image');
22
+ this.$contextModal = $('#mu-kids-context');
23
+ this.$resultsModal = $('#kids-results');
24
+ this.$resultsAbortedModal = $('#kids-results-aborted');
25
+ this.$bubbleCharacterAnimation = $('.mu-kids-character-animation');
26
+ this.$submissionResult = $('.submission-results');
27
+ }
28
+
29
+ showContext() {
30
+ this.$contextModal.modal({
24
31
  backdrop: 'static',
25
32
  keyboard: false
26
33
  });
27
34
  }
28
35
 
29
- $kidsContext.on('hidden.bs.modal', function () {
30
- animateSpeech();
31
- });
32
-
33
- showContext();
34
-
35
- function floatFromPx(value) {
36
- return parseFloat(value.substring(0, value.length - 2));
36
+ showNonAbortedPopup(data, animation_name, open_modal_delay_ms = 0) {
37
+ this.$submissionResult.html(data.html);
38
+ mumuki.presenterCharacter.playAnimation(animation_name, $('.mu-kids-character-result'));
39
+ setTimeout(() => this._openSubmissionResultModal(data), open_modal_delay_ms);
40
+ this.onNonAbortedPopupCall(data);
37
41
  }
38
42
 
39
- function resizeSpeechParagraphs(paragraphIndex) {
40
- var previousParagraphCount = paragraphCount;
41
- scrollHeight = $bubble[0].scrollHeight;
42
- paragraphHeight = floatFromPx($speechParagraphs.css('line-height')) * paragraphsLines;
43
- paragraphCount = Math.ceil(scrollHeight / paragraphHeight);
44
- var newParagraphIndex = Math.floor((paragraphCount / previousParagraphCount) * currentParagraphIndex);
45
- showParagraph(paragraphIndex || newParagraphIndex);
43
+ showAbortedPopup(_data) {
44
+ this.submitButton.disable();
45
+ this.$resultsAbortedModal.modal();
46
46
  }
47
47
 
48
- function tabParagraphs(selector) {
49
- return $('.mu-kids-character-speech-bubble > .mu-kids-character-speech-bubble-normal > div' + selector + ' > p');
50
- }
48
+ // ==================
49
+ // == Hook Methods ==
50
+ // ==================
51
51
 
52
- function updateSpeechParagraphs() {
53
- $speechParagraphs = tabParagraphs('.' + getSelectedTabName());
54
- resizeSpeechParagraphs(0);
52
+ _showSuccessPopup() {
53
+ this._mustImplementThisMethod()
55
54
  }
56
55
 
57
- function getSelectedTabName() {
58
- return $speechTabs.filter(".active").data('target') || $defaultSpeechTabName;
56
+ _showFailurePopup() {
57
+ this._mustImplementThisMethod()
59
58
  }
60
59
 
61
- function showParagraph(index) {
62
- $bubble[0].scrollTop = index * paragraphHeight;
63
- currentParagraphIndex = index;
64
- checkArrowsSpeechVisibility();
65
- }
60
+ // ====================
61
+ // == Event Callback ==
62
+ // ====================
66
63
 
67
- function checkArrowsSpeechVisibility() {
68
- setVisibility($prevSpeech, currentParagraphIndex !== 0);
69
- setVisibility($nextSpeech, currentParagraphIndex !== paragraphCount - 1);
64
+ onReady() {
65
+ // SubClasses may override this method
70
66
  }
71
67
 
72
- function setVisibility(element, isVisible) {
73
- isVisible ? element.show() : element.hide();
68
+ onResize() {
69
+ // SubClasses may override this method
74
70
  }
75
71
 
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
- }
72
+ onNonAbortedPopupCall(_data) {
73
+ // SubClasses may override this method
93
74
  }
94
75
 
95
- mumuki.kids = {
96
-
97
- // ==========
98
- // Public API
99
- // ==========
100
-
101
- // Sets a function that will be called each
102
- // time the states need to be resized. The function takes:
103
- //
104
- // * $state: a single state area
105
- // * fullMargin
106
- // * preferredWidth
107
- // * preferredHeight
108
- //
109
- // Runners must call this method on within the runner's editor.js extension
110
- registerStateScaler: function (scaler) {
111
- this._stateScaler = scaler;
112
- },
113
-
114
- // Sets a function that will be called each
115
- // time the blocks area needs to be resized. The function takes:
116
- //
117
- // * $blocks: the blocks area
118
- //
119
- // Runners must call this method on within the runner's editor.js extension
120
- registerBlocksAreaScaler: function (scaler) {
121
- this._blocksAreaScaler = scaler;
122
- },
123
-
124
- // Scales a single state.
125
- //
126
- // This method is called by the kids code, but the runner's editor.js extension may need
127
- // to perform additional calls to it.
128
- scaleState: function ($state, fullMargin) {
129
- const preferredWidth = $state.width() - fullMargin * 2;
130
- const preferredHeight = $state.height() - fullMargin * 2;
131
- this._stateScaler($state, fullMargin, preferredWidth, preferredHeight);
132
- },
133
-
134
- // Scales the blocks area.
135
- //
136
- // This method is called by the kids code, but the runner's editor.js extension may need
137
- // to perform additional calls to it.
138
- scaleBlocksArea: function ($blocks) {
139
- this._blocksAreaScaler($blocks);
140
- },
141
-
142
- // Displays the kids results, updating the progress bar
143
- // firing the modal and running appropriate animations.
144
- //
145
- // This method needs to be called by the runner's editor.html extension
146
- // in order to finish an exercise
147
- showResult: function (data) {
148
- mumuki.updateProgressBarAndShowModal(data);
149
- if (data.guide_finished_by_solution) return;
150
- mumuki.kids.resultAction[data.status](data);
151
- },
152
-
153
- // Restarts the kids exercise.
154
- //
155
- // This method may need to be called by the runner's editor.html extension
156
- // in order to recover from a failed submission
157
- restart: function () {
158
- mumuki.kids._hideMessageOnCharacterBubble();
159
- var $bubble = mumuki.kids._getCharacterBubble();
160
- Object.keys(mumuki.kids.resultAction).forEach($bubble.removeClass.bind($bubble));
161
- mumuki.presenterCharacter.playAnimation('talk', mumuki.kids._getCharacterImage());
162
- },
163
-
164
- disableContextModalButton: function () {
165
- contextModalButton.setWaiting();
166
- },
167
-
168
- enableContextModalButton: function () {
169
- contextModalButton.enable();
170
- },
171
-
172
- showContext,
173
-
174
- // ===========
175
- // Private API
176
- // ===========
177
-
178
- _updateSubmissionResult: function (html) {
179
- return $('.submission-results').html(html);
180
- },
181
-
182
- _getResultsModal: function () {
183
- return $('#kids-results');
184
- },
185
-
186
- _getResultsAbortedModal: function () {
187
- return $('#kids-results-aborted');
188
- },
189
-
190
- _getCharacterImage: function () {
191
- return $('.mu-kids-character > img');
192
- },
193
-
194
- _getCharacterBubble: function () {
195
- return $('.mu-kids-character-speech-bubble');
196
- },
197
-
198
- _getOverlay: function () {
199
- return $('.mu-kids-overlay');
200
- },
201
-
202
- _hideMessageOnCharacterBubble: function () {
203
- var $bubble = mumuki.kids._getCharacterBubble();
204
- $bubble.find('.mu-kids-character-speech-bubble-tabs').show();
205
- $bubble.find('.mu-kids-character-speech-bubble-normal').show();
206
- $bubble.find('.mu-kids-character-speech-bubble-failed').hide();
207
- $bubble.find('.mu-kids-discussion-link').remove();
208
- Object.keys(mumuki.kids.resultAction).forEach($bubble.removeClass.bind($bubble));
209
- mumuki.kids._getOverlay().hide()
210
- },
211
-
212
- _showMessageOnCharacterBubble: function (data) {
213
- const renderer = new mumuki.renderers.SpeechBubbleRenderer(mumuki.kids._getCharacterBubble());
214
- renderer.setDiscussionsLinkHtml(discussionsLinkHtml);
215
- renderer.setResponseData(data);
216
- renderer.render();
217
- mumuki.kids._getOverlay().show();
218
- },
219
-
220
- _showOnSuccessPopup: function (data) {
221
- mumuki.kids._updateSubmissionResult(data.html);
222
- mumuki.presenterCharacter.playAnimation('success_l', mumuki.kids._getCharacterImage());
223
- mumuki.kids._showMessageOnCharacterBubble(data);
224
- mumuki.presenterCharacter.playAnimation('success2_l', $('.mu-kids-character-success'));
225
- setTimeout(function () {
226
- var $resultsKidsModal = mumuki.kids._getResultsModal();
227
- if ($resultsKidsModal) {
228
- $resultsKidsModal.modal({
229
- backdrop: 'static',
230
- keyboard: false
231
- });
232
- $resultsKidsModal.find('.modal-header').first().html(data.title_html);
233
- $resultsKidsModal.find('.modal-footer').first().html(data.button_html);
234
- mumuki.kids._showCorollaryCharacter();
235
- $('.mu-close-modal').click(() => mumuki.kids._getResultsModal().modal('hide'));
236
- }
237
- }, 1000 * 4);
238
- },
239
-
240
- _showOnFailurePopup: function () {
241
- mumuki.kids.submitButton.disable();
242
- mumuki.kids._getResultsAbortedModal().modal();
243
- mumuki.submission.animateTimeoutError(mumuki.kids.submitButton);
244
- },
245
-
246
- _showOnCharacterBubble: function (data) {
247
- mumuki.presenterCharacter.playAnimation('failure', mumuki.kids._getCharacterImage());
248
- mumuki.kids._showMessageOnCharacterBubble(data);
249
- },
250
-
251
- _showCorollaryCharacter: function () {
252
- mumuki.characters.magnifying_glass.playAnimation('show', $('.mu-kids-corollary-animation'));
253
- },
254
-
255
- _stateScaler: function ($state, fullMargin, preferredWidth, preferredHeight) {
256
- var $table = $state.find('gs-board > table');
257
- if (!$table.length) return setTimeout(() => this.scaleState($state, fullMargin));
258
-
259
- console.warn("You are using the default states scaler, which is gobstones-specific. Please register your own scaler in the future");
260
-
261
- $table.css('transform', 'scale(1)');
262
- var scaleX = preferredWidth / $table.width();
263
- var scaleY = preferredHeight / $table.height();
264
- $table.css('transform', 'scale(' + Math.min(scaleX, scaleY) + ')');
265
- },
266
-
267
- _blocksAreaScaler: function ($blocks) {
268
- console.warn("You are using the default blocks scaler, which is blockly-specific. Please register your own scaler in the future");
269
-
270
- var $blockArea = $blocks.find('#blocklyDiv');
271
- var $blockSvg = $blocks.find('.blocklySvg');
272
-
273
- $blockArea.width($blocks.width());
274
- $blockArea.height($blocks.height());
275
-
276
- $blockSvg.width($blocks.width());
277
- $blockSvg.height($blocks.height());
278
- },
279
-
280
- resultAction: {}
281
-
282
- };
283
-
284
- mumuki.kids.submitButton = new mumuki.submission.SubmitButton($('#kids-btn-retry'), $('.submission_control'));
285
-
286
- function showPrevParagraph() {
287
- animateSpeech();
288
- showParagraph(currentParagraphIndex - 1);
76
+ onSubmissionResultModalOpen(_data) {
77
+ // SubClasses may override this method
289
78
  }
290
79
 
291
- function showNextParagraph() {
292
- animateSpeech();
293
- showParagraph(currentParagraphIndex + 1);
294
- clearInterval(nextSpeechBlinking);
80
+ // =================
81
+ // == Private API ==
82
+ // =================
83
+
84
+ _openSubmissionResultModal(data) {
85
+ this.$resultsModal.modal({ backdrop: 'static', keyboard: false })
86
+ this.$resultsModal.find('.modal-header').first().html(data.title_html)
87
+ this.$resultsModal.find('.modal-footer').first().html(data.button_html)
88
+ $('.mu-close-modal').click(() => this.$resultsModal.modal('hide'));
89
+ this.onSubmissionResultModalOpen(data);
295
90
  }
296
91
 
297
- function animateSpeech() {
298
- mumuki.presenterCharacter.playAnimation('talk', mumuki.kids._getCharacterImage());
92
+ // ==========================
93
+ // == Called by the runner ==
94
+ // ==========================
95
+
96
+ // Displays the exercise results, updating the progress bar
97
+ // firing the modal and running appropriate animations.
98
+ //
99
+ // This method needs to be called by the runner's editor.html extension
100
+ // in order to finish an exercise
101
+ showResult(data) {
102
+ mumuki.progress.updateProgressBarAndShowModal(data);
103
+ if (data.guide_finished_by_solution) return;
104
+ this.resultActions[data.status](data);
299
105
  }
300
106
 
301
- function animateHint() {
302
- mumuki.presenterCharacter.playAnimation('hint', mumuki.kids._getCharacterImage());
107
+ // Restarts the kids exercise.
108
+ //
109
+ // This method may need to be called by the runner's editor.html extension
110
+ // in order to recover from a failed submission
111
+ restart() {
112
+ this._mustImplementThisMethod();
303
113
  }
304
114
 
305
- mumuki.kids.resultAction.passed = mumuki.kids._showOnSuccessPopup;
306
- mumuki.kids.resultAction.passed_with_warnings = mumuki.kids._showOnCharacterBubble;
115
+ // =================================
116
+ // == Called by the assets loader ==
117
+ // =================================
307
118
 
308
- mumuki.kids.resultAction.aborted = mumuki.kids._showOnFailurePopup;
119
+ disableContextModalButton() {
120
+ this.$contextModalButton.setWaiting();
121
+ }
309
122
 
310
- mumuki.kids.resultAction.failed = mumuki.kids._showOnCharacterBubble;
311
- mumuki.kids.resultAction.errored = mumuki.kids._showOnCharacterBubble;
312
- mumuki.kids.resultAction.pending = mumuki.kids._showOnCharacterBubble;
123
+ enableContextModalButton() {
124
+ this.$contextModalButton.enable();
125
+ }
313
126
 
127
+ // ============
128
+ // == Helper ==
129
+ // ============
314
130
 
315
- $(document).ready(() => {
316
- // Speech initialization
317
- if (!$bubble.length) return;
131
+ _mustImplementThisMethod() {
132
+ throw new Error('TODO: implement method')
133
+ }
318
134
 
319
- availableTabs.forEach(function (tabSelector) {
320
- tabParagraphs(tabSelector).contents().unwrap().wrapAll('<p>');
321
- });
135
+ // ============
136
+ // == Scaler ==
137
+ // ============
138
+
139
+ // Sets a function that will be called each
140
+ // time the states need to be resized. The function takes:
141
+ //
142
+ // * $state: a single state area
143
+ // * fullMargin
144
+ // * preferredWidth
145
+ // * preferredHeight
146
+ //
147
+ // Runners must call this method on within the runner's editor.js extension
148
+ registerStateScaler(scaler) {
149
+ this._stateScaler = scaler;
150
+ }
322
151
 
323
- updateSpeechParagraphs();
324
-
325
- resizeSpeechParagraphs();
326
-
327
- $speechTabs.each(function (i) {
328
- var $tab = $($speechTabs[i]);
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
- }
338
- });
152
+ // Sets a function that will be called each
153
+ // time the blocks area needs to be resized. The function takes:
154
+ //
155
+ // * $blocks: the blocks area
156
+ //
157
+ // Runners must call this method on within the runner's editor.js extension
158
+ registerBlocksAreaScaler(scaler) {
159
+ this._blocksAreaScaler = scaler;
160
+ }
339
161
 
340
- if (paragraphCount > 1) {
341
- nextSpeechBlinking = mumuki.setInterval(() => $nextSpeech.fadeTo('slow', 0.1).fadeTo('slow', 1.0), 1000);
342
- }
162
+ // Scales a single state.
163
+ //
164
+ // This method is called by the kids code, but the runner's editor.js extension may need
165
+ // to perform additional calls to it.
166
+ scaleState($state, fullMargin) {
167
+ const preferredWidth = $state.width() - fullMargin * 2;
168
+ const preferredHeight = $state.height() - fullMargin * 2;
169
+ this._stateScaler($state, fullMargin, preferredWidth, preferredHeight);
170
+ }
343
171
 
344
- $nextSpeech.click(showNextParagraph);
345
- $prevSpeech.click(showPrevParagraph);
346
- $description.click(animateSpeech);
172
+ // Scales the blocks area.
173
+ //
174
+ // This method is called by the kids code, but the runner's editor.js extension may need
175
+ // to perform additional calls to it.
176
+ scaleBlocksArea($blocks) {
177
+ this._blocksAreaScaler($blocks);
178
+ }
347
179
 
348
- $hint.click(function () {
349
- animateHint();
350
- this.classList.remove('blink');
351
- });
180
+ _stateScaler($state, fullMargin, preferredWidth, preferredHeight) {
181
+ const $table = $state.find('gs-board > table');
182
+ if (!$table.length) return setTimeout(() => this.scaleState($state, fullMargin));
352
183
 
353
- // States initial resizing
354
- mumuki.resize(function () {
355
- var margin = 15;
356
- var fullMargin = margin * 2;
184
+ console.warn("You are using the default states scaler, which is gobstones-specific. Please register your own scaler in the future");
357
185
 
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');
186
+ $table.css('transform', 'scale(1)');
187
+ const scaleX = preferredWidth / $table.width();
188
+ const scaleY = preferredHeight / $table.height();
189
+ $table.css('transform', 'scale(' + Math.min(scaleX, scaleY) + ')');
190
+ }
364
191
 
365
- distributeAreas($muKidsStateImage, $muKidsStatesContainer, $muKidsBlocks, fullMargin);
192
+ _blocksAreaScaler($blocks) {
193
+ console.warn("You are using the default blocks scaler, which is blockly-specific. Please register your own scaler in the future");
366
194
 
367
- if (!$muKidsExerciseDescription.hasClass('mu-kids-exercise-description-fixed')) {
368
- $muKidsExerciseDescription.width($muKidsExercise.width() - $muKidsStatesContainer.width() - margin);
369
- }
195
+ const $blockArea = $blocks.find('#blocklyDiv');
196
+ const $blockSvg = $blocks.find('.blocklySvg');
370
197
 
371
- $muKidsStates.each((index, state) => mumuki.kids.scaleState($(state), fullMargin));
372
- mumuki.kids.scaleBlocksArea($muKidsBlocks);
198
+ $blockArea.width($blocks.width());
199
+ $blockArea.height($blocks.height());
373
200
 
374
- if (paragraphCount <= 1) clearInterval(nextSpeechBlinking);
201
+ $blockSvg.width($blocks.width());
202
+ $blockSvg.height($blocks.height());
203
+ }
375
204
 
376
- resizeSpeechParagraphs();
377
- });
378
- })
379
- });
205
+ }