mumuki-laboratory 7.7.6 → 7.10.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 +83 -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/events.js +51 -0
- data/app/assets/javascripts/mumuki_laboratory/application/exercise.js +68 -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/profile.js +71 -0
- 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.scss +1 -1
- data/app/assets/stylesheets/mumuki_laboratory/application/_modules.scss +19 -17
- data/app/assets/stylesheets/mumuki_laboratory/application/modules/_avatar.scss +41 -0
- data/app/assets/stylesheets/mumuki_laboratory/application/modules/_gs-board.scss +3 -0
- data/app/assets/stylesheets/mumuki_laboratory/application/modules/{guide-corollary.scss → _guide_corollary.scss} +0 -0
- data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kids.scss +1 -2
- data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kindergarten.scss +2 -1
- data/app/assets/stylesheets/mumuki_laboratory/application/modules/{popover.scss → _popover.scss} +0 -0
- data/app/assets/stylesheets/mumuki_laboratory/application/modules/_user_profile.scss +36 -0
- data/app/controllers/application_controller.rb +2 -1
- data/app/controllers/users_controller.rb +5 -1
- data/app/helpers/application_helper.rb +6 -4
- data/app/helpers/avatar_helper.rb +9 -0
- data/app/helpers/discussions_helper.rb +2 -2
- data/app/helpers/{locale_helper.rb → globals_helper.rb} +6 -2
- data/app/helpers/profile_helper.rb +5 -0
- data/app/mailers/user_mailer.rb +24 -11
- data/app/views/book/show.html.erb +1 -1
- data/app/views/exercises/show.html.erb +3 -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/_runner_assets.html.erb +1 -2
- data/app/views/layouts/application.html.erb +2 -2
- data/app/views/layouts/modals/_avatar_picker.html.erb +16 -0
- data/app/views/users/_avatar_list.html.erb +11 -0
- data/app/views/users/_edit_user_form.html.erb +22 -0
- data/app/views/users/_user_form.html.erb +21 -8
- data/app/views/users/edit.html.erb +5 -0
- data/app/views/users/show.html.erb +0 -4
- data/config/routes.rb +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/datetime.es.yml +14 -14
- data/lib/mumuki/laboratory/locales/en.yml +12 -4
- data/lib/mumuki/laboratory/locales/es.yml +12 -4
- data/lib/mumuki/laboratory/locales/pt.yml +10 -2
- data/lib/mumuki/laboratory/version.rb +1 -1
- data/spec/dummy/db/schema.rb +13 -1
- data/spec/features/chapter_spec.rb +17 -0
- data/spec/features/exercise_flow_spec.rb +54 -6
- data/spec/features/home_public_flow_spec.rb +16 -0
- data/spec/helpers/avatar_helper_spec.rb +26 -0
- data/spec/javascripts/editors-spec.js +54 -0
- data/spec/javascripts/events-spec.js +33 -0
- data/spec/javascripts/exercise-spec.js +41 -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 +23 -3
- data/spec/teaspoon_env.rb +8 -2
- data/vendor/assets/javascripts/codemirror-modes/gobstones.js +3 -3
- metadata +38 -11
- data/app/assets/stylesheets/mumuki_laboratory/application/modules/_follow_us.scss +0 -16
- data/app/helpers/version_helper.rb +0 -5
@@ -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
|
+
})();
|
@@ -1,26 +1,28 @@
|
|
1
|
-
@import "modules/
|
2
|
-
@import "modules/exercise_assignment";
|
3
|
-
@import "modules/exercise_results";
|
4
|
-
@import "modules/modal";
|
5
|
-
@import "modules/upload";
|
6
|
-
@import "modules/console";
|
7
|
-
@import "modules/progress_listing";
|
1
|
+
@import "modules/avatar";
|
8
2
|
@import "modules/book_header";
|
9
|
-
@import "modules/
|
10
|
-
@import "modules/
|
11
|
-
@import "modules/overlap";
|
12
|
-
@import "modules/editor";
|
13
|
-
@import "modules/organization_chooser";
|
14
|
-
@import "modules/guide-corollary";
|
3
|
+
@import "modules/breadcrumb";
|
4
|
+
@import "modules/chapter_show";
|
15
5
|
@import "modules/checkboxes";
|
6
|
+
@import "modules/console";
|
7
|
+
@import "modules/datepicker";
|
8
|
+
@import "modules/discussion";
|
16
9
|
@import "modules/dropdown";
|
10
|
+
@import "modules/editor";
|
11
|
+
@import "modules/exercise_assignment";
|
12
|
+
@import "modules/exercise_results";
|
17
13
|
@import "modules/flash";
|
18
|
-
@import "modules/highlight";
|
19
|
-
@import "modules/breadcrumb";
|
20
14
|
@import "modules/gs-board";
|
15
|
+
@import "modules/guide_corollary";
|
16
|
+
@import "modules/highlight";
|
21
17
|
@import "modules/kids";
|
22
18
|
@import "modules/kindergarten";
|
23
19
|
@import "modules/kids_results";
|
24
|
-
@import "modules/
|
20
|
+
@import "modules/modal";
|
21
|
+
@import "modules/organization_chooser";
|
22
|
+
@import "modules/overlap";
|
25
23
|
@import "modules/popover";
|
26
|
-
@import "modules/
|
24
|
+
@import "modules/progress_bar";
|
25
|
+
@import "modules/progress_listing";
|
26
|
+
@import "modules/timer";
|
27
|
+
@import "modules/upload";
|
28
|
+
@import "modules/user_profile";
|
@@ -0,0 +1,41 @@
|
|
1
|
+
.mu-user-avatar {
|
2
|
+
width: 250px;
|
3
|
+
height: 250px;
|
4
|
+
|
5
|
+
border: 2px solid $mu-color-dark-separator;
|
6
|
+
border-radius: 50%;
|
7
|
+
|
8
|
+
padding: 2px;
|
9
|
+
}
|
10
|
+
|
11
|
+
.mu-edit-avatar {
|
12
|
+
position: relative;
|
13
|
+
top: 100px;
|
14
|
+
right: 15px;
|
15
|
+
}
|
16
|
+
|
17
|
+
.mu-avatar-list {
|
18
|
+
display: flex;
|
19
|
+
align-items: center;
|
20
|
+
justify-content: space-evenly;
|
21
|
+
flex-wrap: wrap;
|
22
|
+
|
23
|
+
padding: 15px;
|
24
|
+
}
|
25
|
+
|
26
|
+
.mu-avatar-item {
|
27
|
+
width: 150px;
|
28
|
+
height: 150px;
|
29
|
+
|
30
|
+
border: 2px solid $mu-color-dark-separator;
|
31
|
+
|
32
|
+
padding: 2px;
|
33
|
+
margin: 10px;
|
34
|
+
|
35
|
+
cursor: pointer;
|
36
|
+
|
37
|
+
&:hover {
|
38
|
+
box-shadow: -3px 3px 10px -3px $mu-color-primary;
|
39
|
+
transform: scale(1.025);
|
40
|
+
}
|
41
|
+
}
|
File without changes
|
@@ -96,7 +96,7 @@ $kids-speech-tabs-width: 40px;
|
|
96
96
|
}
|
97
97
|
|
98
98
|
.mu-kids-character-animation {
|
99
|
-
width:
|
99
|
+
width: $kids-characters-height;
|
100
100
|
}
|
101
101
|
|
102
102
|
.mu-kids-character {
|
@@ -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,
|
@@ -8,7 +8,7 @@
|
|
8
8
|
.mu-kids-exercise-workspace {
|
9
9
|
display: flex;
|
10
10
|
flex-flow: row;
|
11
|
-
height: 100
|
11
|
+
height: calc(100% - #{$kids-characters-height});
|
12
12
|
width: 100%;
|
13
13
|
}
|
14
14
|
|
@@ -41,6 +41,7 @@
|
|
41
41
|
|
42
42
|
.mu-kids-state.mu-state-initial {
|
43
43
|
height: 100%;
|
44
|
+
width: 100%;
|
44
45
|
}
|
45
46
|
|
46
47
|
.mu-kids-state.mu-state-final {
|
data/app/assets/stylesheets/mumuki_laboratory/application/modules/{popover.scss → _popover.scss}
RENAMED
File without changes
|
@@ -0,0 +1,36 @@
|
|
1
|
+
.mu-user-header {
|
2
|
+
padding: 10px 0px;
|
3
|
+
|
4
|
+
display: flex;
|
5
|
+
align-items: center;
|
6
|
+
}
|
7
|
+
|
8
|
+
.mu-profile-actions {
|
9
|
+
vertical-align: middle;
|
10
|
+
margin-left: auto;
|
11
|
+
|
12
|
+
.btn {
|
13
|
+
width: 150px;
|
14
|
+
height: 55px;
|
15
|
+
margin-left: 15px;
|
16
|
+
transition: background-color 0.3s, border-color 0.3s;
|
17
|
+
|
18
|
+
&[disabled], &[disabled]:hover {
|
19
|
+
background-color: lighten($mu-color-complementary, 15%);
|
20
|
+
border-color: lighten($mu-color-complementary, 15%);
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
.mu-profile-info {
|
26
|
+
font-size: 20px;
|
27
|
+
margin-top: 5px;
|
28
|
+
div {
|
29
|
+
margin-bottom: 15px;
|
30
|
+
.italic {
|
31
|
+
font-style: italic;
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
|
36
|
+
}
|
@@ -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!
|
@@ -92,7 +93,7 @@ class ApplicationController < ActionController::Base
|
|
92
93
|
def validate_user_profile!
|
93
94
|
unless current_user.profile_completed?
|
94
95
|
flash.notice = I18n.t :please_fill_profile_data
|
95
|
-
redirect_to
|
96
|
+
redirect_to edit_user_path
|
96
97
|
end
|
97
98
|
end
|
98
99
|
|
@@ -11,7 +11,7 @@ class UsersController < ApplicationController
|
|
11
11
|
|
12
12
|
def update
|
13
13
|
current_user.update_and_notify! user_params
|
14
|
-
redirect_to
|
14
|
+
redirect_to user_path, notice: I18n.t(:user_data_updated)
|
15
15
|
end
|
16
16
|
|
17
17
|
def unsubscribe
|
@@ -21,6 +21,10 @@ class UsersController < ApplicationController
|
|
21
21
|
redirect_to root_path, notice: t(:unsubscribed_successfully)
|
22
22
|
end
|
23
23
|
|
24
|
+
def permissible_params
|
25
|
+
super << :avatar_id
|
26
|
+
end
|
27
|
+
|
24
28
|
private
|
25
29
|
|
26
30
|
def validate_user_profile!
|
@@ -6,12 +6,14 @@ module ApplicationHelper
|
|
6
6
|
html_escape html.to_str
|
7
7
|
end
|
8
8
|
|
9
|
-
def
|
10
|
-
|
9
|
+
def profile_picture_for(user, **options)
|
10
|
+
options.merge!(height: 40, onError: "this.onerror = null; this.src = '#{image_url(user.placeholder_image_url)}'")
|
11
|
+
avatar_image(user.profile_picture, options)
|
11
12
|
end
|
12
13
|
|
13
|
-
def
|
14
|
-
|
14
|
+
def avatar_image(avatar_url, **options)
|
15
|
+
options.merge!(class: "img-circle #{options[:class]}")
|
16
|
+
image_tag(image_url(avatar_url), options)
|
15
17
|
end
|
16
18
|
|
17
19
|
def paginate(object, options = {})
|