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.
- checksums.yaml +4 -4
- data/README.md +13 -3
- data/Rakefile +7 -1
- data/app/assets/javascripts/mumuki_laboratory/application/alias-modes.js +1 -1
- data/app/assets/javascripts/mumuki_laboratory/application/bridge.js +66 -57
- data/app/assets/javascripts/mumuki_laboratory/application/codemirror-builder.js +28 -25
- data/app/assets/javascripts/mumuki_laboratory/application/codemirror.js +8 -10
- data/app/assets/javascripts/mumuki_laboratory/application/confirmation.js +2 -2
- data/app/assets/javascripts/mumuki_laboratory/application/console.js +41 -43
- data/app/assets/javascripts/mumuki_laboratory/application/csrf-token.js +9 -12
- data/app/assets/javascripts/mumuki_laboratory/application/custom-editor.js +11 -15
- data/app/assets/javascripts/mumuki_laboratory/application/discussions.js +1 -3
- data/app/assets/javascripts/mumuki_laboratory/application/editors.js +104 -0
- data/app/assets/javascripts/mumuki_laboratory/application/elipsis.js +5 -4
- data/app/assets/javascripts/mumuki_laboratory/application/exercise.js +32 -0
- data/app/assets/javascripts/mumuki_laboratory/application/inputs.js +4 -2
- data/app/assets/javascripts/mumuki_laboratory/application/interval.js +2 -4
- data/app/assets/javascripts/mumuki_laboratory/application/kids.js +1 -1
- data/app/assets/javascripts/mumuki_laboratory/application/load-analytics.js +1 -1
- data/app/assets/javascripts/mumuki_laboratory/application/load-error-svg.js +1 -1
- data/app/assets/javascripts/mumuki_laboratory/application/messages.js +2 -2
- data/app/assets/javascripts/mumuki_laboratory/application/multiple-choice.js +1 -1
- data/app/assets/javascripts/mumuki_laboratory/application/multiple-scenarios.js +3 -6
- data/app/assets/javascripts/mumuki_laboratory/application/pin.js +3 -5
- data/app/assets/javascripts/mumuki_laboratory/application/progress.js +24 -6
- data/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js +20 -11
- data/app/assets/javascripts/mumuki_laboratory/application/speech-bubble-renderer.js +12 -5
- data/app/assets/javascripts/mumuki_laboratory/application/submission.js +19 -101
- data/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js +93 -0
- data/app/assets/javascripts/mumuki_laboratory/application/sync-mode.js +75 -0
- data/app/assets/javascripts/mumuki_laboratory/application/timer.js +5 -6
- data/app/assets/javascripts/mumuki_laboratory/application/tooltip.js +1 -1
- data/app/assets/javascripts/mumuki_laboratory/application/upload.js +1 -1
- data/app/assets/javascripts/mumuki_laboratory/application/user.js +1 -1
- data/app/assets/stylesheets/mumuki_laboratory/application/modules/_gs-board.scss +3 -0
- data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kids.scss +0 -1
- data/app/controllers/application_controller.rb +1 -0
- data/app/helpers/{locale_helper.rb → globals_helper.rb} +6 -2
- data/app/mailers/user_mailer.rb +24 -11
- data/app/views/book/show.html.erb +1 -1
- data/app/views/exercises/show.html.erb +2 -0
- data/app/views/layouts/_main.html.erb +1 -2
- data/app/views/layouts/_progress.html.erb +1 -1
- data/app/views/layouts/_progress_bar.html.erb +7 -1
- data/app/views/layouts/application.html.erb +1 -1
- data/lib/mumuki/laboratory/controllers.rb +1 -0
- data/lib/mumuki/laboratory/controllers/incognito_mode.rb +28 -0
- data/lib/mumuki/laboratory/locales/en.yml +6 -4
- data/lib/mumuki/laboratory/locales/es.yml +6 -4
- data/lib/mumuki/laboratory/locales/pt.yml +4 -2
- data/lib/mumuki/laboratory/version.rb +1 -1
- data/spec/dummy/db/schema.rb +2 -1
- data/spec/features/chapter_spec.rb +17 -0
- data/spec/features/exercise_flow_spec.rb +47 -2
- data/spec/features/home_public_flow_spec.rb +16 -0
- data/spec/javascripts/editors-spec.js +54 -0
- data/spec/javascripts/exercise-spec.js +22 -0
- data/spec/javascripts/global-spec.js +6 -0
- data/spec/javascripts/spec-helper.js +4 -0
- data/spec/javascripts/submissions-store-spec.js +44 -0
- data/spec/javascripts/sync-mode-spec.js +15 -0
- data/spec/mailers/user_mailer_spec.rb +18 -3
- data/spec/teaspoon_env.rb +8 -2
- data/vendor/assets/javascripts/codemirror-modes/gobstones.js +2 -3
- metadata +19 -5
- data/app/helpers/version_helper.rb +0 -5
@@ -1,4 +1,5 @@
|
|
1
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
}
|
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
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
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
|
29
|
+
}
|
30
|
+
error(submitButton) {
|
34
31
|
this.submissionsResultsArea.html('');
|
35
32
|
this.submissionsErrorTemplate.show();
|
36
33
|
animateTimeoutError(submitButton);
|
37
|
-
}
|
38
|
-
done
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
18
|
+
}
|
19
|
+
return startTimer
|
20
|
+
})();
|
@@ -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
|
2
|
-
def
|
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
|