mumuki-laboratory 7.7.6 → 7.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -3
  3. data/Rakefile +7 -1
  4. data/app/assets/javascripts/mumuki_laboratory/application/alias-modes.js +1 -1
  5. data/app/assets/javascripts/mumuki_laboratory/application/bridge.js +66 -57
  6. data/app/assets/javascripts/mumuki_laboratory/application/codemirror-builder.js +28 -25
  7. data/app/assets/javascripts/mumuki_laboratory/application/codemirror.js +8 -10
  8. data/app/assets/javascripts/mumuki_laboratory/application/confirmation.js +2 -2
  9. data/app/assets/javascripts/mumuki_laboratory/application/console.js +41 -43
  10. data/app/assets/javascripts/mumuki_laboratory/application/csrf-token.js +9 -12
  11. data/app/assets/javascripts/mumuki_laboratory/application/custom-editor.js +11 -15
  12. data/app/assets/javascripts/mumuki_laboratory/application/discussions.js +1 -3
  13. data/app/assets/javascripts/mumuki_laboratory/application/editors.js +104 -0
  14. data/app/assets/javascripts/mumuki_laboratory/application/elipsis.js +5 -4
  15. data/app/assets/javascripts/mumuki_laboratory/application/exercise.js +32 -0
  16. data/app/assets/javascripts/mumuki_laboratory/application/inputs.js +4 -2
  17. data/app/assets/javascripts/mumuki_laboratory/application/interval.js +2 -4
  18. data/app/assets/javascripts/mumuki_laboratory/application/kids.js +1 -1
  19. data/app/assets/javascripts/mumuki_laboratory/application/load-analytics.js +1 -1
  20. data/app/assets/javascripts/mumuki_laboratory/application/load-error-svg.js +1 -1
  21. data/app/assets/javascripts/mumuki_laboratory/application/messages.js +2 -2
  22. data/app/assets/javascripts/mumuki_laboratory/application/multiple-choice.js +1 -1
  23. data/app/assets/javascripts/mumuki_laboratory/application/multiple-scenarios.js +3 -6
  24. data/app/assets/javascripts/mumuki_laboratory/application/pin.js +3 -5
  25. data/app/assets/javascripts/mumuki_laboratory/application/progress.js +24 -6
  26. data/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js +20 -11
  27. data/app/assets/javascripts/mumuki_laboratory/application/speech-bubble-renderer.js +12 -5
  28. data/app/assets/javascripts/mumuki_laboratory/application/submission.js +19 -101
  29. data/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js +93 -0
  30. data/app/assets/javascripts/mumuki_laboratory/application/sync-mode.js +75 -0
  31. data/app/assets/javascripts/mumuki_laboratory/application/timer.js +5 -6
  32. data/app/assets/javascripts/mumuki_laboratory/application/tooltip.js +1 -1
  33. data/app/assets/javascripts/mumuki_laboratory/application/upload.js +1 -1
  34. data/app/assets/javascripts/mumuki_laboratory/application/user.js +1 -1
  35. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_gs-board.scss +3 -0
  36. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kids.scss +0 -1
  37. data/app/controllers/application_controller.rb +1 -0
  38. data/app/helpers/{locale_helper.rb → globals_helper.rb} +6 -2
  39. data/app/mailers/user_mailer.rb +24 -11
  40. data/app/views/book/show.html.erb +1 -1
  41. data/app/views/exercises/show.html.erb +2 -0
  42. data/app/views/layouts/_main.html.erb +1 -2
  43. data/app/views/layouts/_progress.html.erb +1 -1
  44. data/app/views/layouts/_progress_bar.html.erb +7 -1
  45. data/app/views/layouts/application.html.erb +1 -1
  46. data/lib/mumuki/laboratory/controllers.rb +1 -0
  47. data/lib/mumuki/laboratory/controllers/incognito_mode.rb +28 -0
  48. data/lib/mumuki/laboratory/locales/en.yml +6 -4
  49. data/lib/mumuki/laboratory/locales/es.yml +6 -4
  50. data/lib/mumuki/laboratory/locales/pt.yml +4 -2
  51. data/lib/mumuki/laboratory/version.rb +1 -1
  52. data/spec/dummy/db/schema.rb +2 -1
  53. data/spec/features/chapter_spec.rb +17 -0
  54. data/spec/features/exercise_flow_spec.rb +47 -2
  55. data/spec/features/home_public_flow_spec.rb +16 -0
  56. data/spec/javascripts/editors-spec.js +54 -0
  57. data/spec/javascripts/exercise-spec.js +22 -0
  58. data/spec/javascripts/global-spec.js +6 -0
  59. data/spec/javascripts/spec-helper.js +4 -0
  60. data/spec/javascripts/submissions-store-spec.js +44 -0
  61. data/spec/javascripts/sync-mode-spec.js +15 -0
  62. data/spec/mailers/user_mailer_spec.rb +18 -3
  63. data/spec/teaspoon_env.rb +8 -2
  64. data/vendor/assets/javascripts/codemirror-modes/gobstones.js +2 -3
  65. metadata +19 -5
  66. data/app/helpers/version_helper.rb +0 -5
@@ -1,4 +1,5 @@
1
- ((mumuki)=> {
1
+ mumuki.renderers = mumuki.renderers || {};
2
+ mumuki.renderers.speechBubble = (()=> {
2
3
 
3
4
  function renderSpeechBubbleResultItem(item) {
4
5
  return `
@@ -93,7 +94,13 @@
93
94
  }
94
95
  }
95
96
 
96
- mumuki.renderers = mumuki.renderers || {};
97
- mumuki.renderers.SpeechBubbleRenderer = SpeechBubbleRenderer;
98
- mumuki.renderers.renderSpeechBubbleResultItem = renderSpeechBubbleResultItem;
99
- })(mumuki)
97
+ return {
98
+ SpeechBubbleRenderer,
99
+ renderSpeechBubbleResultItem
100
+ }
101
+ })();
102
+
103
+ /** @deprecated use {@code mumuki.renderers.speechBubble.SpeechBubbleRenderer} instead */
104
+ mumuki.renderers.SpeechBubbleRenderer = mumuki.renderers.speechBubble.SpeechBubbleRenderer;
105
+ /** @deprecated use {@code mumuki.renderers.speechBubble.renderSpeechBubbleResultItem} instead */
106
+ mumuki.renderers.renderSpeechBubbleResultItem = mumuki.renderers.speechBubble.renderSpeechBubbleResultItem;
@@ -1,6 +1,4 @@
1
- var mumuki = mumuki || {};
2
-
3
- (function (mumuki) {
1
+ mumuki.submission = (() => {
4
2
 
5
3
  // =============
6
4
  // UI Components
@@ -14,32 +12,31 @@ var mumuki = mumuki || {};
14
12
  .play();
15
13
  }
16
14
 
17
- function ResultsBox(submissionsResults) {
18
- this.submissionsResultsArea = submissionsResults;
19
- this.processingTemplate = $('#processing-template');
20
- this.submissionsErrorTemplate = $(".submission-result-error");
21
- }
22
-
23
- ResultsBox.prototype = {
24
- waiting: function () {
15
+ class ResultsBox {
16
+ constructor(submissionsResults) {
17
+ this.submissionsResultsArea = submissionsResults;
18
+ this.processingTemplate = $('#processing-template');
19
+ this.submissionsErrorTemplate = $(".submission-result-error");
20
+ }
21
+ waiting() {
25
22
  this.submissionsResultsArea.html(this.processingTemplate.html());
26
23
  this.submissionsErrorTemplate.hide();
27
- },
28
- success: function (data, submitButton) {
24
+ }
25
+ success(data, submitButton) {
29
26
  this.submissionsResultsArea.html(data.html);
30
27
  data.status === 'aborted' ? this.error(submitButton) : submitButton.enable();
31
28
  mumuki.updateProgressBarAndShowModal(data);
32
- },
33
- error: function (submitButton) {
29
+ }
30
+ error(submitButton) {
34
31
  this.submissionsResultsArea.html('');
35
32
  this.submissionsErrorTemplate.show();
36
33
  animateTimeoutError(submitButton);
37
- },
38
- done: function (data, submitButton) {
34
+ }
35
+ done(data, submitButton) {
39
36
  submitButton.updateAttemptsLeft(data);
40
37
  mumuki.pin.scroll();
41
38
  }
42
- };
39
+ }
43
40
 
44
41
  class SubmitButton extends mumuki.Button {
45
42
 
@@ -59,78 +56,6 @@ var mumuki = mumuki || {};
59
56
  }
60
57
  }
61
58
 
62
- // ============
63
- // Content Sync
64
- // ============
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();
77
- }
78
-
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
- }
95
-
96
- contents.forEach((it) => {
97
- content[it.name] = it.value;
98
- });
99
-
100
- return content;
101
- }
102
-
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
- }
118
-
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
- }
134
59
 
135
60
  // ==========
136
61
  // Processing
@@ -212,7 +137,7 @@ var mumuki = mumuki || {};
212
137
  // Entry Point
213
138
  // ===========
214
139
 
215
- mumuki.load(function () {
140
+ mumuki.load(() => {
216
141
  var $submissionsResults = $('.submission-results');
217
142
  if (!$submissionsResults) return;
218
143
 
@@ -222,8 +147,7 @@ var mumuki = mumuki || {};
222
147
  mumuki.submission._selectSolutionProcessor(submitButton, $submissionsResults);
223
148
 
224
149
  submitButton.start(() => {
225
- var solution = mumuki.submission.getContent();
226
- mumuki.submission.processSolution(solution);
150
+ mumuki.submission.processSolution(mumuki.editors.getSubmission());
227
151
  });
228
152
 
229
153
  submitButton.checkAttemptsLeft();
@@ -244,18 +168,12 @@ var mumuki = mumuki || {};
244
168
  *
245
169
  * @module mumuki.submission
246
170
  */
247
- mumuki.submission = {
171
+ return {
248
172
  processSolution,
249
173
  _registerSolutionProcessor,
250
174
  _selectSolutionProcessor,
251
175
 
252
- _syncContent,
253
- registerContentSyncer,
254
- getStandardEditorContents,
255
- getContent,
256
-
257
176
  animateTimeoutError,
258
177
  SubmitButton,
259
178
  };
260
-
261
- })(mumuki);
179
+ })();
@@ -0,0 +1,93 @@
1
+ mumuki.SubmissionsStore = (() => {
2
+ const SubmissionsStore = new class {
3
+ /**
4
+ * Returns the submission's result status for the last submission to
5
+ * the given exercise, or pending, if not present
6
+ *
7
+ * @param {number} exerciseId
8
+ * @returns {SubmissionStatus}
9
+ */
10
+ getLastSubmissionStatus(exerciseId) {
11
+ const submission = this.getLastSubmissionAndResult(exerciseId);
12
+ return submission ? submission.result.status : 'pending';
13
+ }
14
+
15
+ /**
16
+ * Returns the submission and result for the last submission to
17
+ * the given exercise
18
+ *
19
+ * @param {number} exerciseId
20
+ * @returns {SubmissionAndResult}
21
+ */
22
+ getLastSubmissionAndResult(exerciseId) {
23
+ const submissionAndResult = window.localStorage.getItem(this._keyFor(exerciseId));
24
+ if (!submissionAndResult) return null;
25
+ return JSON.parse(submissionAndResult);
26
+ }
27
+
28
+ /**
29
+ * Saves the result for the given exercise
30
+ *
31
+ * @param {number} exerciseId
32
+ * @param {SubmissionAndResult} submissionAndResult
33
+ */
34
+ setSubmissionResultFor(exerciseId, submissionAndResult) {
35
+ window.localStorage.setItem(this._keyFor(exerciseId), this._asString(submissionAndResult));
36
+ }
37
+
38
+ /**
39
+ * Retrieves the last cached, non-aborted result for the given submission of the given exercise
40
+ *
41
+ * @param {number} exerciseId
42
+ * @param {Submission} submission
43
+ * @returns {SubmissionResult} the cached result for this submission
44
+ */
45
+ getSubmissionResultFor(exerciseId, submission) {
46
+ const last = this.getLastSubmissionAndResult(exerciseId);
47
+ if (!last
48
+ || last.result.status === 'aborted'
49
+ || !this.submissionSolutionEquals(last.submission, submission)) {
50
+ return null;
51
+ }
52
+ return last.result;
53
+ }
54
+
55
+ /**
56
+ * Extract the submission's solution content
57
+ *
58
+ * @param {Submission} submission
59
+ * @returns {string}
60
+ */
61
+ submissionSolutionContent(submission) {
62
+ if (submission.solution) {
63
+ return submission.solution.content;
64
+ } else {
65
+ return submission['solution[content]'];
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Compares two solutions to determine if they are equivalent
71
+ * from the point of view of the code evaluation
72
+ *
73
+ * @param {Submission} one
74
+ * @param {Submission} other
75
+ * @returns {boolean}
76
+ */
77
+ submissionSolutionEquals(one, other) {
78
+ return this.submissionSolutionContent(one) === this.submissionSolutionContent(other);
79
+ }
80
+
81
+ // private API
82
+
83
+ _asString(object) {
84
+ return JSON.stringify(object);
85
+ }
86
+
87
+ _keyFor(exerciseId) {
88
+ return `/exercise/${exerciseId}/submission`;
89
+ }
90
+ };
91
+
92
+ return SubmissionsStore;
93
+ })();
@@ -0,0 +1,75 @@
1
+ /** @type {boolean} */
2
+ mumuki.incognitoUser;
3
+ mumuki.syncMode = (() => {
4
+
5
+ /**
6
+ * Syncs progress and solutions
7
+ * from local storage
8
+ */
9
+ class ClientSyncMode {
10
+ syncProgress() {
11
+ mumuki.progress.updateWholeProgressBar($anchor => this._getProgressListItemClass($anchor));
12
+ }
13
+
14
+ syncEditorContent() {
15
+ const lastSubmission = mumuki.SubmissionsStore.getLastSubmissionAndResult(mumuki.exercise.id);
16
+ if (lastSubmission) {
17
+ /** @todo extract core module */
18
+ const content = mumuki.SubmissionsStore.submissionSolutionContent(lastSubmission.submission);
19
+ mumuki.editors.setContent(content);
20
+ }
21
+ }
22
+
23
+ /**
24
+ * @param {JQuery} $anchor
25
+ */
26
+ _getProgressListItemClass($anchor) {
27
+ const exerciseId = $anchor.data('mu-exercise-id');
28
+ const status = mumuki.SubmissionsStore.getLastSubmissionStatus(exerciseId);
29
+ return mumuki.renderers.progressListItemClassForStatus(status, exerciseId == mumuki.exercise.id);
30
+ }
31
+
32
+ }
33
+
34
+ /**
35
+ * Syncs progress and solutions from server.
36
+ *
37
+ * This class does nothing actually, since a server-side behaviour is the default one
38
+ * and no additional actions are needed.
39
+ */
40
+ class ServerSyncMode {
41
+ syncProgress() {
42
+ // nothing
43
+ }
44
+
45
+ syncEditorContent() {
46
+ // nothing
47
+ }
48
+ }
49
+
50
+
51
+ /** Selects the most appropriate sync mode */
52
+ function _selectSyncMode() {
53
+ if (mumuki.incognitoUser) {
54
+ mumuki.syncMode._current = new ClientSyncMode();
55
+ } else {
56
+ mumuki.syncMode._current = new ServerSyncMode();
57
+ }
58
+ }
59
+
60
+ return {
61
+ ServerSyncMode,
62
+ ClientSyncMode,
63
+
64
+ _selectSyncMode,
65
+
66
+ /** @type {ClientSyncMode|ServerSyncMode}*/
67
+ _current: null
68
+ }
69
+ })();
70
+
71
+ mumuki.load(() => {
72
+ mumuki.syncMode._selectSyncMode();
73
+ mumuki.syncMode._current.syncProgress();
74
+ mumuki.syncMode._current.syncEditorContent();
75
+ })
@@ -1,7 +1,5 @@
1
- var mumuki = mumuki || {};
2
-
3
- (function (mumuki) {
4
- mumuki.startTimer = function (endDate) {
1
+ mumuki.startTimer = (() => {
2
+ function startTimer(endDate) {
5
3
  var endTime = new Date(endDate).getTime();
6
4
  var currentTime = Date.now();
7
5
  var diffTime = endTime - currentTime;
@@ -17,5 +15,6 @@ var mumuki = mumuki || {};
17
15
  $('#timer').text(duration.format("HH:mm:ss"));
18
16
  }
19
17
  }, intervalDuration);
20
- };
21
- })(mumuki);
18
+ }
19
+ return startTimer
20
+ })();
@@ -1,3 +1,3 @@
1
- mumuki.load(function () {
1
+ mumuki.load(() => {
2
2
  $('[title]').tooltip();
3
3
  });
@@ -1,4 +1,4 @@
1
- mumuki.load(function() {
1
+ mumuki.load(() => {
2
2
  $('#upload-input').change(function (evt) {
3
3
  var file = evt.target.files[0];
4
4
  if (!file) return;
@@ -1,4 +1,4 @@
1
- mumuki.load(function () {
1
+ mumuki.load(() => {
2
2
  var hash = document.location.hash;
3
3
  if (hash) {
4
4
  $(".nav-tabs a[data-target='" + hash + "']").tab('show');
@@ -4,5 +4,8 @@ gs-board {
4
4
  span {
5
5
  vertical-align: middle;
6
6
  }
7
+ &.gs-board {
8
+ background-color: #FFFFFF;
9
+ }
7
10
  }
8
11
  }
@@ -338,7 +338,6 @@ $kids-speech-tabs-width: 40px;
338
338
  td, th {
339
339
  &.gs-board {
340
340
  border: $kids-speech-border !important;
341
- background-color: #FFFFFF;
342
341
  }
343
342
  &.gbs_lx,
344
343
  &.gbs_lh,
@@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
11
11
  include Mumuki::Laboratory::Controllers::Notifications
12
12
  include Mumuki::Laboratory::Controllers::DynamicErrors
13
13
  include Mumuki::Laboratory::Controllers::EmbeddedMode
14
+ include Mumuki::Laboratory::Controllers::IncognitoMode
14
15
 
15
16
  before_action :set_current_organization!
16
17
  before_action :set_locale!
@@ -1,10 +1,14 @@
1
- module LocaleHelper
2
- def locale_tags
1
+ module GlobalsHelper
2
+ def globals_tags
3
3
  %Q{
4
4
  <script type="text/javascript">
5
5
  window.mumukiLocale = #{raw Organization.current.locale_json};
6
6
  mumuki.locale = '#{Organization.current.locale}';
7
7
  moment.locale('#{Organization.current.locale}');
8
+
9
+ mumuki.incognitoUser = #{current_incognito_user?};
10
+
11
+ mumuki.version = '#{Mumuki::Laboratory::VERSION}';
8
12
  </script>
9
13
  }.html_safe
10
14
  end