mumuki-laboratory 7.10.5 → 7.12.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) 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 +163 -0
  7. data/app/assets/javascripts/mumuki_laboratory/application/kids.js +171 -333
  8. data/app/assets/javascripts/mumuki_laboratory/application/kindergarten.js +159 -0
  9. data/app/assets/javascripts/mumuki_laboratory/application/mu-modal-carrousel.js +63 -0
  10. data/app/assets/javascripts/mumuki_laboratory/application/number-counter.js +18 -0
  11. data/app/assets/javascripts/mumuki_laboratory/application/primary.js +258 -0
  12. data/app/assets/javascripts/mumuki_laboratory/application/profile.js +31 -16
  13. data/app/assets/javascripts/mumuki_laboratory/application/submission.js +1 -0
  14. data/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js +19 -2
  15. data/app/assets/stylesheets/mumuki_laboratory/application/_errors.scss +1 -3
  16. data/app/assets/stylesheets/mumuki_laboratory/application/_modules.scss +3 -1
  17. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_avatar.scss +21 -0
  18. data/app/assets/stylesheets/mumuki_laboratory/application/modules/{_chapter_show.scss → _content_show.scss} +0 -0
  19. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kids_results.scss +117 -0
  20. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kindergarten.scss +335 -12
  21. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_medal.scss +48 -0
  22. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_terms.scss +44 -0
  23. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_user_profile.scss +40 -3
  24. data/app/controllers/application_controller.rb +15 -8
  25. data/app/controllers/book_discussions_controller.rb +4 -0
  26. data/app/controllers/users_controller.rb +8 -2
  27. data/app/helpers/avatar_helper.rb +11 -3
  28. data/app/helpers/contextualization_result_helper.rb +9 -1
  29. data/app/helpers/gamification_helper.rb +5 -0
  30. data/app/helpers/kindergarten_helper.rb +5 -0
  31. data/app/helpers/links_helper.rb +8 -0
  32. data/app/helpers/medal_helper.rb +36 -0
  33. data/app/helpers/open_graph_helper.rb +2 -2
  34. data/app/helpers/organization_list_helper.rb +1 -1
  35. data/app/helpers/overlapped_buttons_helper.rb +1 -1
  36. data/app/helpers/page_title_helper.rb +2 -2
  37. data/app/helpers/profile_helper.rb +9 -1
  38. data/app/views/book/_header.html.erb +17 -0
  39. data/app/views/book/show.html.erb +1 -18
  40. data/app/views/discussions/terms.html.erb +10 -0
  41. data/app/views/exercise_solutions/_contextualization_results_container.html.erb +9 -0
  42. data/app/views/exercise_solutions/_kids_level_up.html.erb +11 -0
  43. data/app/views/exercise_solutions/_kids_results.html.erb +1 -1
  44. data/app/views/exercise_solutions/_results_title.html.erb +5 -0
  45. data/app/views/exercises/show.html.erb +4 -0
  46. data/app/views/invitations/_invitation_form.html.erb +1 -0
  47. data/app/views/layouts/_discussions.html.erb +4 -0
  48. data/app/views/layouts/_error.html.erb +3 -6
  49. data/app/views/layouts/_guide.html.erb +10 -3
  50. data/app/views/layouts/_kindergarten.html.erb +38 -0
  51. data/app/views/layouts/_main.html.erb +3 -1
  52. data/app/views/layouts/_organization_chooser.html.erb +0 -7
  53. data/app/views/layouts/_terms_acceptance_disclaimer.html.erb +6 -0
  54. data/app/views/layouts/application.html.erb +10 -3
  55. data/app/views/layouts/exercise_inputs/layouts/_input_kindergarten.html.erb +27 -27
  56. data/app/views/layouts/modals/_guide_corollary.html.erb +10 -1
  57. data/app/views/layouts/modals/_kids_context.html.erb +1 -1
  58. data/app/views/layouts/modals/_kids_results.html.erb +16 -6
  59. data/app/views/layouts/modals/_kindergarten_context.html.erb +30 -0
  60. data/app/views/layouts/modals/_kindergarten_results.html.erb +36 -0
  61. data/app/views/layouts/modals/_kindergarten_results_aborted.html.erb +27 -0
  62. data/app/views/layouts/modals/_level_up.html.erb +27 -0
  63. data/app/views/users/_avatar_list.html.erb +6 -2
  64. data/app/views/users/_edit_user_form.html.erb +9 -4
  65. data/app/views/users/_term.html.erb +10 -0
  66. data/app/views/users/_user_form.html.erb +17 -3
  67. data/app/views/users/terms.html.erb +18 -0
  68. data/config/routes.rb +2 -0
  69. data/lib/mumuki/laboratory.rb +1 -1
  70. data/lib/mumuki/laboratory/controllers/current_organization.rb +1 -1
  71. data/lib/mumuki/laboratory/controllers/results_rendering.rb +4 -2
  72. data/lib/mumuki/laboratory/locales/en.yml +21 -5
  73. data/lib/mumuki/laboratory/locales/es-CL.yml +10 -4
  74. data/lib/mumuki/laboratory/locales/es.yml +22 -6
  75. data/lib/mumuki/laboratory/locales/pt.yml +21 -5
  76. data/lib/mumuki/laboratory/version.rb +1 -1
  77. data/spec/capybara_helper.rb +99 -0
  78. data/spec/controllers/exercise_solutions_controller_spec.rb +3 -4
  79. data/spec/dummy/db/schema.rb +37 -1
  80. data/spec/dummy/public/medal/outline.svg +1089 -0
  81. data/spec/features/choose_organization_spec.rb +12 -30
  82. data/spec/features/disable_user_flow_spec.rb +3 -5
  83. data/spec/features/disabled_organization_flow_spec.rb +9 -14
  84. data/spec/features/exercise_flow_spec.rb +2 -2
  85. data/spec/features/guide_reset_spec.rb +1 -1
  86. data/spec/features/guides_flow_spec.rb +1 -1
  87. data/spec/features/home_private_flow_spec.rb +1 -3
  88. data/spec/features/home_public_flow_spec.rb +6 -12
  89. data/spec/features/invitations_flow_spec.rb +2 -2
  90. data/spec/features/login_flow_spec.rb +2 -2
  91. data/spec/features/not_found_private_flow_spec.rb +4 -4
  92. data/spec/features/not_found_public_flow_spec.rb +1 -6
  93. data/spec/features/profile_flow_spec.rb +1 -1
  94. data/spec/helpers/page_title_helper_spec.rb +3 -3
  95. data/spec/javascripts/editors-spec.js +23 -0
  96. data/spec/javascripts/gamification-spec.js +58 -0
  97. data/spec/javascripts/submissions-store-spec.js +139 -6
  98. data/spec/spec_helper.rb +2 -0
  99. data/spec/teaspoon_env.rb +24 -6
  100. metadata +137 -98
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14e1de394e31aa3018f8d66df21561874095da64afdfb255a6fcef6189568695
4
- data.tar.gz: 91c9503ff88706237d9e32e0fd1ab22b8f6ba571f47d10e54b01e46ab7a66e35
3
+ metadata.gz: 26c8eb799e96cc5d8e68fc6d5b9ae0e268fcc77fa99156db5051b373b6aee412
4
+ data.tar.gz: 4dde09623edc6d26430bbd6bc86bcd6e85347e73bcd444df00d8f7b47274859c
5
5
  SHA512:
6
- metadata.gz: 177dca452ef3b4a6f64eb117559eac3509ffefdf09c2c7e6fc4a85ecb0500f710d9bfbc15e30756a4288f87eac81c01befadc5d201de9b7ae73a0ca8f8f4b06b
7
- data.tar.gz: 6823c871a44ffd808592b3bfc407081784c0df77ea2062acabb9e2e6b70be87d97cb8d84c47d1de7a1b00abcaf9a14e7c9b446a55a9dfaff74af83714dac72b3
6
+ metadata.gz: efef98352ef78664e8aac7493a70546b824b54e97ed556d18ae1068c532564dcf509f2878e2c8105d9e5a0e318222b3aa0b1894d43fe2d1d48da397137ea6329
7
+ data.tar.gz: fb241340ebe7994e84ab86c08275304c74cd5669bbc2ca3b7c8bcf616d1186b088ecd3da59fe8abb80b65a1f9dfd8538eccbf31cba4f55dbd83f7a2838601911
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,163 @@
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 DummyLevelProgression {
9
+ setExpMessage() {}
10
+
11
+ registerLevelUpAction() {}
12
+
13
+ registerGainedExperienceAction() {}
14
+
15
+ updateLevel() {}
16
+ }
17
+
18
+ class LevelProgression {
19
+ constructor(currentExp) {
20
+ this.currentExp = currentExp;
21
+ this.lastEarnedExp = 0;
22
+ this._levelUpAction = this.defaultLevelUpAction;
23
+ this._gainedExperienceAction = this.defaultGainedExperienceAction;
24
+ }
25
+
26
+ expToLevelUp() {
27
+ return this.baseExpNextLevel() - this.currentExp;
28
+ }
29
+
30
+ baseExpNextLevel() {
31
+ return this.expFor(this.currentLevel() + 1);
32
+ }
33
+
34
+ expFor(level) {
35
+ const ax2 = Formula.QUADRATIC_COEFFICIENT * Math.pow(level, 2);
36
+ const bx = Formula.LINEAR_COEFFICIENT * level;
37
+ const c = Formula.CONSTANT_TERM;
38
+
39
+ return ax2 + bx + c;
40
+ }
41
+
42
+ currentLevel() {
43
+ return this.levelFor(this.currentExp);
44
+ }
45
+
46
+ levelFor(exp) {
47
+ const a = Formula.QUADRATIC_COEFFICIENT;
48
+ const b = Formula.LINEAR_COEFFICIENT;
49
+ const c = Formula.CONSTANT_TERM;
50
+
51
+ return Math.floor((-b + Math.sqrt(Math.pow(b, 2) - 4 * a * (c - exp))) / (2 * a));
52
+ }
53
+
54
+ triggersLevelChange(newExp) {
55
+ return this.levelFor(this.currentExp + newExp) !== this.currentLevel();
56
+ }
57
+
58
+ currentLevelProgress() {
59
+ return (this.currentExp - this.baseExpCurrentLevel()) / (this.baseExpNextLevel() - this.baseExpCurrentLevel());
60
+ }
61
+
62
+ baseExpCurrentLevel() {
63
+ return this.expFor(this.currentLevel());
64
+ }
65
+
66
+ exercisesToNextLevel() {
67
+ return Math.ceil(this.expToLevelUp() / 100);
68
+ }
69
+
70
+ setExpMessage(data) {
71
+ const exp = data.current_exp;
72
+ this.lastEarnedExp = exp - this.currentExp;
73
+
74
+ if (this.lastEarnedExp > 0) {
75
+ this.levelUpActionIfLevelUp(data.level_up_html);
76
+ this._gainedExperienceAction();
77
+
78
+ this.currentExp = exp;
79
+ this.updateLevel();
80
+ }
81
+ }
82
+
83
+ defaultGainedExperienceAction() {
84
+ $('#mu-exp-points').html(this.lastEarnedExp);
85
+ $('#mu-exp-earned-message').removeClass('hidden');
86
+ }
87
+
88
+ defaultLevelUpAction(_levelUpHtml) {
89
+ $('#mu-level-up').modal();
90
+ }
91
+
92
+ registerLevelUpAction(action) {
93
+ this._levelUpAction = action;
94
+ }
95
+
96
+ registerGainedExperienceAction(action) {
97
+ this._gainedExperienceAction = action;
98
+ }
99
+
100
+ levelUpActionIfLevelUp(levelUpHtml) {
101
+ if (this.triggersLevelChange(this.lastEarnedExp)) {
102
+ this._levelUpAction(levelUpHtml);
103
+ }
104
+ }
105
+
106
+ animateExperienceCounter(selector) {
107
+ mumuki.animateNumberCounter(selector, this.lastEarnedExp);
108
+ }
109
+
110
+ updateLevel() {
111
+ const $muLevelProgress = $('#mu-level-progress');
112
+
113
+ $('#mu-solve-more-exercises span').text(this.exercisesToNextLevel());
114
+ $('.mu-level-number').html(this.currentLevel());
115
+ $('.mu-level-tooltip').attr("title", (_, value) => `${value} ${this.currentLevel()}`);
116
+
117
+ if (this.currentLevelProgress() === 0) {
118
+ $muLevelProgress.attr("display", "none");
119
+ }
120
+
121
+ $muLevelProgress.animate(
122
+ {'progress': this.currentLevelProgress() * 250},
123
+ {
124
+ step: function(progress) {
125
+ let pattern = progress + ", 999";
126
+ $(this).attr("stroke-dasharray", pattern);
127
+ },
128
+ duration: 1000
129
+ });
130
+ }
131
+ }
132
+
133
+ function _setUpCurrentLevelProgression() {
134
+ if (_gamificationEnabled()) {
135
+ mumuki.gamification.currentLevelProgression = new LevelProgression(currentExp());
136
+ } else {
137
+ mumuki.gamification.currentLevelProgression = new DummyLevelProgression();
138
+ }
139
+ }
140
+
141
+ function _gamificationEnabled() {
142
+ return $('#mu-current-exp').length;
143
+ }
144
+
145
+ function currentExp() {
146
+ return parseInt($('#mu-current-exp').val());
147
+ }
148
+
149
+ return {
150
+ Formula,
151
+ LevelProgression,
152
+
153
+ _setUpCurrentLevelProgression,
154
+
155
+ /** @type {LevelProgression|DummyLevelProgression} */
156
+ currentLevelProgression: null
157
+ };
158
+ })();
159
+
160
+ mumuki.load(() => {
161
+ mumuki.gamification._setUpCurrentLevelProgression();
162
+ mumuki.gamification.currentLevelProgression.updateLevel();
163
+ });
@@ -1,379 +1,217 @@
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({
24
- backdrop: 'static',
25
- keyboard: false
26
- });
1
+ mumuki.Kids = class {
2
+
3
+ constructor() {
4
+ this.initialize();
5
+ this.showContext();
6
+ $(document).ready(this.onReady.bind(this));
27
7
  }
28
8
 
29
- $kidsContext.on('hidden.bs.modal', function () {
30
- animateSpeech();
31
- });
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.resultsCarrousel = new mumuki.ModalCarrousel('.mu-kids-results-carrousel');
25
+ this.$resultsAbortedModal = $('#kids-results-aborted');
26
+ this.$bubbleCharacterAnimation = $('.mu-kids-character-animation');
27
+ this.$submissionResult = $('.submission-results');
28
+ mumuki.gamification.currentLevelProgression.registerLevelUpAction(this.levelUpAction);
29
+ mumuki.gamification.currentLevelProgression.registerGainedExperienceAction(this.gainedExperienceAction);
30
+ }
32
31
 
33
- showContext();
32
+ gainedExperienceAction() {
33
+ mumuki.gamification.currentLevelProgression.animateExperienceCounter('.mu-kids-results .mu-experience');
34
+ }
34
35
 
35
- function floatFromPx(value) {
36
- return parseFloat(value.substring(0, value.length - 2));
36
+ levelUpAction(levelUpHtml) {
37
+ $('.mu-kids-results-carrousel').append(levelUpHtml);
37
38
  }
38
39
 
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);
40
+ showContext() {
41
+ this.$contextModal.modal({
42
+ backdrop: 'static',
43
+ keyboard: false
44
+ });
46
45
  }
47
46
 
48
- function tabParagraphs(selector) {
49
- return $('.mu-kids-character-speech-bubble > .mu-kids-character-speech-bubble-normal > div' + selector + ' > p');
47
+ showNonAbortedPopup(data, animation_name, open_modal_delay_ms = 0) {
48
+ this.$submissionResult.html(data.html);
49
+ mumuki.presenterCharacter.playAnimation(animation_name, $('.mu-kids-character-result'));
50
+ setTimeout(() => this._openSubmissionResultModal(data), open_modal_delay_ms);
51
+ this.onNonAbortedPopupCall(data);
50
52
  }
51
53
 
52
- function updateSpeechParagraphs() {
53
- $speechParagraphs = tabParagraphs('.' + getSelectedTabName());
54
- resizeSpeechParagraphs(0);
54
+ showAbortedPopup(_data) {
55
+ this.submitButton.disable();
56
+ this.$resultsAbortedModal.modal();
55
57
  }
56
58
 
57
- function getSelectedTabName() {
58
- return $speechTabs.filter(".active").data('target') || $defaultSpeechTabName;
59
+ // ==================
60
+ // == Hook Methods ==
61
+ // ==================
62
+
63
+ _showSuccessPopup() {
64
+ this._mustImplementThisMethod()
59
65
  }
60
66
 
61
- function showParagraph(index) {
62
- $bubble[0].scrollTop = index * paragraphHeight;
63
- currentParagraphIndex = index;
64
- checkArrowsSpeechVisibility();
67
+ _showFailurePopup() {
68
+ this._mustImplementThisMethod()
65
69
  }
66
70
 
67
- function checkArrowsSpeechVisibility() {
68
- setVisibility($prevSpeech, currentParagraphIndex !== 0);
69
- setVisibility($nextSpeech, currentParagraphIndex !== paragraphCount - 1);
71
+ // ====================
72
+ // == Event Callback ==
73
+ // ====================
74
+
75
+ onReady() {
76
+ // SubClasses may override this method
70
77
  }
71
78
 
72
- function setVisibility(element, isVisible) {
73
- isVisible ? element.show() : element.hide();
79
+ onResize() {
80
+ // SubClasses may override this method
74
81
  }
75
82
 
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
- }
83
+ onNonAbortedPopupCall(_data) {
84
+ // SubClasses may override this method
93
85
  }
94
86
 
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);
87
+ onSubmissionResultModalOpen(_data) {
88
+ this.resultsCarrousel.show();
289
89
  }
290
90
 
291
- function showNextParagraph() {
292
- animateSpeech();
293
- showParagraph(currentParagraphIndex + 1);
294
- clearInterval(nextSpeechBlinking);
91
+ // =================
92
+ // == Private API ==
93
+ // =================
94
+
95
+ _openSubmissionResultModal(data) {
96
+ this.$resultsModal.modal({ backdrop: 'static', keyboard: false });
97
+ this.$resultsModal.find('.modal-header').first().html(data.title_html);
98
+ mumuki.gamification.currentLevelProgression.setExpMessage(data);
99
+ this.$resultsModal.find('.modal-footer').first().html(data.button_html);
100
+ $('.mu-close-modal').click(() => this.$resultsModal.modal('hide'));
101
+ this.onSubmissionResultModalOpen(data);
295
102
  }
296
103
 
297
- function animateSpeech() {
298
- mumuki.presenterCharacter.playAnimation('talk', mumuki.kids._getCharacterImage());
104
+ // ==========================
105
+ // == Called by the runner ==
106
+ // ==========================
107
+
108
+ // Displays the exercise results, updating the progress bar
109
+ // firing the modal and running appropriate animations.
110
+ //
111
+ // This method needs to be called by the runner's editor.html extension
112
+ // in order to finish an exercise
113
+ showResult(data) {
114
+ mumuki.progress.updateProgressBarAndShowModal(data);
115
+ if (data.guide_finished_by_solution) return;
116
+ this.resultActions[data.status](data);
299
117
  }
300
118
 
301
- function animateHint() {
302
- mumuki.presenterCharacter.playAnimation('hint', mumuki.kids._getCharacterImage());
119
+ // Restarts the kids exercise.
120
+ //
121
+ // This method may need to be called by the runner's editor.html extension
122
+ // in order to recover from a failed submission
123
+ restart() {
124
+ this._mustImplementThisMethod();
303
125
  }
304
126
 
305
- mumuki.kids.resultAction.passed = mumuki.kids._showOnSuccessPopup;
306
- mumuki.kids.resultAction.passed_with_warnings = mumuki.kids._showOnCharacterBubble;
127
+ // =================================
128
+ // == Called by the assets loader ==
129
+ // =================================
307
130
 
308
- mumuki.kids.resultAction.aborted = mumuki.kids._showOnFailurePopup;
131
+ disableContextModalButton() {
132
+ this.$contextModalButton.setWaiting();
133
+ }
309
134
 
310
- mumuki.kids.resultAction.failed = mumuki.kids._showOnCharacterBubble;
311
- mumuki.kids.resultAction.errored = mumuki.kids._showOnCharacterBubble;
312
- mumuki.kids.resultAction.pending = mumuki.kids._showOnCharacterBubble;
135
+ enableContextModalButton() {
136
+ this.$contextModalButton.enable();
137
+ }
313
138
 
139
+ // ============
140
+ // == Helper ==
141
+ // ============
314
142
 
315
- $(document).ready(() => {
316
- // Speech initialization
317
- if (!$bubble.length) return;
143
+ _mustImplementThisMethod() {
144
+ throw new Error('TODO: implement method')
145
+ }
318
146
 
319
- availableTabs.forEach(function (tabSelector) {
320
- tabParagraphs(tabSelector).contents().unwrap().wrapAll('<p>');
321
- });
147
+ // ============
148
+ // == Scaler ==
149
+ // ============
150
+
151
+ // Sets a function that will be called each
152
+ // time the states need to be resized. The function takes:
153
+ //
154
+ // * $state: a single state area
155
+ // * fullMargin
156
+ // * preferredWidth
157
+ // * preferredHeight
158
+ //
159
+ // Runners must call this method on within the runner's editor.js extension
160
+ registerStateScaler(scaler) {
161
+ this._stateScaler = scaler;
162
+ }
322
163
 
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
- });
164
+ // Sets a function that will be called each
165
+ // time the blocks area needs to be resized. The function takes:
166
+ //
167
+ // * $blocks: the blocks area
168
+ //
169
+ // Runners must call this method on within the runner's editor.js extension
170
+ registerBlocksAreaScaler(scaler) {
171
+ this._blocksAreaScaler = scaler;
172
+ }
339
173
 
340
- if (paragraphCount > 1) {
341
- nextSpeechBlinking = mumuki.setInterval(() => $nextSpeech.fadeTo('slow', 0.1).fadeTo('slow', 1.0), 1000);
342
- }
174
+ // Scales a single state.
175
+ //
176
+ // This method is called by the kids code, but the runner's editor.js extension may need
177
+ // to perform additional calls to it.
178
+ scaleState($state, fullMargin) {
179
+ const preferredWidth = $state.width() - fullMargin * 2;
180
+ const preferredHeight = $state.height() - fullMargin * 2;
181
+ this._stateScaler($state, fullMargin, preferredWidth, preferredHeight);
182
+ }
343
183
 
344
- $nextSpeech.click(showNextParagraph);
345
- $prevSpeech.click(showPrevParagraph);
346
- $description.click(animateSpeech);
184
+ // Scales the blocks area.
185
+ //
186
+ // This method is called by the kids code, but the runner's editor.js extension may need
187
+ // to perform additional calls to it.
188
+ scaleBlocksArea($blocks) {
189
+ this._blocksAreaScaler($blocks);
190
+ }
347
191
 
348
- $hint.click(function () {
349
- animateHint();
350
- this.classList.remove('blink');
351
- });
192
+ _stateScaler($state, fullMargin, preferredWidth, preferredHeight) {
193
+ const $table = $state.find('gs-board > table');
194
+ if (!$table.length) return setTimeout(() => this.scaleState($state, fullMargin));
352
195
 
353
- // States initial resizing
354
- mumuki.resize(function () {
355
- var margin = 15;
356
- var fullMargin = margin * 2;
196
+ console.warn("You are using the default states scaler, which is gobstones-specific. Please register your own scaler in the future");
357
197
 
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');
198
+ $table.css('transform', 'scale(1)');
199
+ const scaleX = preferredWidth / $table.width();
200
+ const scaleY = preferredHeight / $table.height();
201
+ $table.css('transform', 'scale(' + Math.min(scaleX, scaleY) + ')');
202
+ }
364
203
 
365
- distributeAreas($muKidsStateImage, $muKidsStatesContainer, $muKidsBlocks, fullMargin);
204
+ _blocksAreaScaler($blocks) {
205
+ console.warn("You are using the default blocks scaler, which is blockly-specific. Please register your own scaler in the future");
366
206
 
367
- if (!$muKidsExerciseDescription.hasClass('mu-kids-exercise-description-fixed')) {
368
- $muKidsExerciseDescription.width($muKidsExercise.width() - $muKidsStatesContainer.width() - margin);
369
- }
207
+ const $blockArea = $blocks.find('#blocklyDiv');
208
+ const $blockSvg = $blocks.find('.blocklySvg');
370
209
 
371
- $muKidsStates.each((index, state) => mumuki.kids.scaleState($(state), fullMargin));
372
- mumuki.kids.scaleBlocksArea($muKidsBlocks);
210
+ $blockArea.width($blocks.width());
211
+ $blockArea.height($blocks.height());
373
212
 
374
- if (paragraphCount <= 1) clearInterval(nextSpeechBlinking);
213
+ $blockSvg.width($blocks.width());
214
+ $blockSvg.height($blocks.height());
215
+ }
375
216
 
376
- resizeSpeechParagraphs();
377
- });
378
- })
379
- });
217
+ }