mumuki-laboratory 6.4.2 → 6.5.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 +61 -1
- data/app/assets/javascripts/application.js +1 -0
- data/app/assets/javascripts/application/characters.js +26 -0
- data/app/assets/javascripts/application/kids.js +179 -123
- data/app/assets/javascripts/application/load-error-svg.js +3 -20
- data/app/assets/javascripts/application/multiple-scenarios.js +152 -0
- data/app/assets/javascripts/application/popover.js +18 -0
- data/app/assets/javascripts/application/submission.js +5 -10
- data/app/assets/stylesheets/application/_layout.scss +10 -2
- data/app/assets/stylesheets/application/_modules.scss +1 -0
- data/app/assets/stylesheets/application/modules/_kids.scss +11 -4
- data/app/assets/stylesheets/application/modules/popover.scss +9 -0
- data/app/controllers/application_controller.rb +2 -2
- data/app/helpers/application_helper.rb +14 -5
- data/app/helpers/assets_helper.rb +13 -1
- data/app/helpers/links_helper.rb +15 -1
- data/app/helpers/organization_list_helper.rb +1 -1
- data/app/helpers/reset_button_helper.rb +1 -1
- data/app/helpers/version_helper.rb +5 -0
- data/app/views/errors/forbidden.html.erb +1 -1
- data/app/views/errors/internal_server_error.html.erb +1 -1
- data/app/views/errors/not_found.html.erb +1 -1
- data/app/views/exercise_solutions/_kids_results.html.erb +2 -2
- data/app/views/exercises/show.html.erb +2 -1
- data/app/views/layouts/_guide.html.erb +21 -5
- data/app/views/layouts/_kids.html.erb +1 -1
- data/app/views/layouts/_main.html.erb +1 -0
- data/app/views/layouts/_organizations_listing.html.erb +1 -1
- data/app/views/layouts/_submission_result_error_body.html.erb +1 -1
- data/app/views/layouts/exercise_inputs/layouts/_input_kids.html.erb +21 -10
- data/app/views/layouts/modals/_kids_context.html.erb +2 -2
- data/app/views/layouts/modals/_kids_results.html.erb +1 -1
- data/app/views/layouts/modals/_kids_results_aborted.html.erb +1 -1
- data/lib/mumuki/laboratory/controllers/results_rendering.rb +2 -1
- data/lib/mumuki/laboratory/engine.rb +3 -0
- data/lib/mumuki/laboratory/locales/en.yml +2 -0
- data/lib/mumuki/laboratory/locales/es.yml +2 -0
- data/lib/mumuki/laboratory/locales/pt.yml +2 -0
- data/lib/mumuki/laboratory/version.rb +1 -1
- data/public/character/kibi/context.svg +1 -0
- data/public/character/kibi/failure.svg +1 -0
- data/public/character/kibi/jump.svg +1 -0
- data/public/character/kibi/success2_l.svg +1 -0
- data/public/character/kibi/success_l.svg +1 -0
- data/public/character/magnifying_glass/apparition.svg +1 -0
- data/public/character/magnifying_glass/loop.svg +1 -0
- data/public/error/403.svg +1 -0
- data/public/error/404.svg +1 -0
- data/public/error/500.svg +1 -0
- data/public/error/timeout_1.svg +1 -0
- data/public/error/timeout_2.svg +1 -0
- data/public/error/timeout_3.svg +1 -0
- data/spec/controllers/exercise_solutions_controller_spec.rb +2 -1
- data/spec/dummy/db/schema.rb +5 -2
- data/spec/dummy/public/character/animations.json +30 -0
- data/spec/features/exercise_flow_spec.rb +3 -3
- data/spec/features/guides_flow_spec.rb +3 -3
- metadata +52 -17
- data/public/character/kids/kibi_context.svg +0 -11
- data/public/error_403.svg +0 -2
- data/public/error_404.svg +0 -5
- data/public/error_500.svg +0 -2
- data/public/error_timeout_1.svg +0 -2
- data/public/error_timeout_2.svg +0 -1
- data/public/error_timeout_3.svg +0 -1
- data/public/kibi_animated.svg +0 -2
- data/public/kibi_failure.svg +0 -5
- data/public/kibi_success.svg +0 -11
- data/public/kibi_success_dancing.svg +0 -16
- data/public/magnifying_glass_apparition.svg +0 -2
- data/public/magnifying_glass_loop.svg +0 -3
@@ -2,27 +2,10 @@ mumuki.load(function () {
|
|
2
2
|
var error_svgs = ['403', '404', '500', 'timeout_1', 'timeout_2', 'timeout_3'];
|
3
3
|
|
4
4
|
mumuki.errors = mumuki.errors || {};
|
5
|
-
mumuki.characters = mumuki.characters || {};
|
6
5
|
|
7
|
-
error_svgs.forEach(function (
|
8
|
-
addImage(mumuki.errors,
|
6
|
+
error_svgs.forEach(function (svgErrorName) {
|
7
|
+
muvment.animation.addImage(mumuki.errors, svgErrorName, '/error/');
|
9
8
|
});
|
10
9
|
|
11
|
-
|
12
|
-
addImage(mumuki.characters, 'magnifying_glass_apparition', '/');
|
13
|
-
addImage(mumuki.characters, 'magnifying_glass_loop', '/');
|
14
|
-
|
15
|
-
function addImage(object, imageName, urlPrefix) {
|
16
|
-
var url = urlPrefix + imageName + '.svg';
|
17
|
-
if (!object[imageName]) {
|
18
|
-
$.get(url, function (data) {
|
19
|
-
var duration = parseFloat($(data).find('animate').attr('dur') || 0, 10) * 1000;
|
20
|
-
object[imageName] = {
|
21
|
-
url: url,
|
22
|
-
duration: duration
|
23
|
-
};
|
24
|
-
});
|
25
|
-
}
|
26
|
-
}
|
27
|
-
|
10
|
+
mumuki.errorState = (error_name) => mumuki.errors[error_name].asState(error_name);
|
28
11
|
});
|
@@ -0,0 +1,152 @@
|
|
1
|
+
var mumuki = mumuki || {};
|
2
|
+
|
3
|
+
(function (mumuki) {
|
4
|
+
|
5
|
+
const setControlVisibility = function ($control, visible) {
|
6
|
+
visible ? $control.show() : $control.hide();
|
7
|
+
};
|
8
|
+
|
9
|
+
const addClickListener = function($element, listener) {
|
10
|
+
$element.on('click', listener);
|
11
|
+
};
|
12
|
+
|
13
|
+
const getControlElement = function(controlSelector) {
|
14
|
+
return $(`.mu-scenario-control.${controlSelector} i`);
|
15
|
+
};
|
16
|
+
|
17
|
+
const statusIconMap = new Map([
|
18
|
+
['pending', 'fa-circle'],
|
19
|
+
['passed', 'fa-check-circle'],
|
20
|
+
['failed', 'fa-times-circle']
|
21
|
+
]);
|
22
|
+
|
23
|
+
const highlightAnimationClass = 'jump';
|
24
|
+
|
25
|
+
const statusesClasses = Array.from(statusIconMap.keys());
|
26
|
+
const iconsClasses = Array.from(statusIconMap.values());
|
27
|
+
const indicatorsClasses = iconsClasses.concat(statusesClasses);
|
28
|
+
|
29
|
+
const getClassesFor = (status) => [status, statusIconMap.get(status)];
|
30
|
+
|
31
|
+
const wrapScenario = ($scenario) => $scenario.wrap("<div class='mu-scenario'></div>").parent();
|
32
|
+
|
33
|
+
const setIndicatorStatus = function (indicator, status) {
|
34
|
+
const indicatorClassList = indicator.classList;
|
35
|
+
const statusClasses = getClassesFor(status);
|
36
|
+
indicatorClassList.remove(...indicatorsClasses);
|
37
|
+
indicatorClassList.add(...statusClasses);
|
38
|
+
};
|
39
|
+
|
40
|
+
const unhighlightIndicator = function ($indicator) {
|
41
|
+
$indicator.classList.remove(highlightAnimationClass);
|
42
|
+
};
|
43
|
+
|
44
|
+
const highlightIndicator = function ($indicator) {
|
45
|
+
$indicator.classList.add(highlightAnimationClass);
|
46
|
+
};
|
47
|
+
|
48
|
+
const triggerResize = function () {
|
49
|
+
let event = document.createEvent('HTMLEvents');
|
50
|
+
event.initEvent('resize', true, false);
|
51
|
+
document.dispatchEvent(event);
|
52
|
+
};
|
53
|
+
|
54
|
+
class MultipleScenarios {
|
55
|
+
constructor(selectors) {
|
56
|
+
this.indicators = $('.mu-multiple-scenarios .indicators');
|
57
|
+
this.initialize(selectors);
|
58
|
+
}
|
59
|
+
|
60
|
+
initialize (selectors) {
|
61
|
+
this.createScenarios(selectors);
|
62
|
+
this.createControls();
|
63
|
+
this.createIndicators();
|
64
|
+
this.setActiveIndex(0);
|
65
|
+
$('.mu-scenario-control').removeClass('hidden');
|
66
|
+
}
|
67
|
+
|
68
|
+
validScenarioIndex (index) {
|
69
|
+
return index >= 0 && index < this.scenariosCount;
|
70
|
+
}
|
71
|
+
|
72
|
+
createControlListener ($control, changeStep) {
|
73
|
+
addClickListener($control, () => {
|
74
|
+
const newScenarioIndex = this.currentScenarioIndex + changeStep;
|
75
|
+
if (this.validScenarioIndex(newScenarioIndex)) {
|
76
|
+
this.setActiveIndex(newScenarioIndex);
|
77
|
+
}
|
78
|
+
})
|
79
|
+
}
|
80
|
+
|
81
|
+
createControls () {
|
82
|
+
this.previousControl = getControlElement('previous');
|
83
|
+
this.nextControl = getControlElement('next');
|
84
|
+
this.createControlListener(this.previousControl, -1);
|
85
|
+
this.createControlListener(this.nextControl, 1);
|
86
|
+
}
|
87
|
+
|
88
|
+
createScenarios (selectors) {
|
89
|
+
let $scenarios = selectors.map(selector => $(`.mu-multiple-scenarios ${selector}`));
|
90
|
+
this.scenarios = $scenarios.map($scenario => wrapScenario($scenario));
|
91
|
+
this.scenariosCount = this.scenarios[0].length;
|
92
|
+
}
|
93
|
+
|
94
|
+
createIndicators () {
|
95
|
+
for (let index = 0; index < this.scenariosCount; index++) {
|
96
|
+
let $indicator = $('<li class="fa"></li>');
|
97
|
+
this.indicators.append($indicator);
|
98
|
+
let indicatorIndex = index;
|
99
|
+
addClickListener($indicator, () => {
|
100
|
+
this.setActiveIndex(indicatorIndex);
|
101
|
+
});
|
102
|
+
}
|
103
|
+
this.indicatorItems = this.indicators.children('li');
|
104
|
+
this.resetIndicators();
|
105
|
+
}
|
106
|
+
|
107
|
+
setActiveIndex (index) {
|
108
|
+
this.currentScenarioIndex = index;
|
109
|
+
this.setActiveScenario(index);
|
110
|
+
unhighlightIndicator(this.indicatorItems[index]);
|
111
|
+
this.updateControls(index);
|
112
|
+
triggerResize();
|
113
|
+
}
|
114
|
+
|
115
|
+
updateControls (index) {
|
116
|
+
setControlVisibility(this.previousControl, index !== 0);
|
117
|
+
setControlVisibility(this.nextControl, index !== this.scenariosCount - 1);
|
118
|
+
}
|
119
|
+
|
120
|
+
setActiveScenario (index) {
|
121
|
+
[...this.scenarios, this.indicatorItems].forEach(($scenario) => {
|
122
|
+
$scenario.removeClass('active');
|
123
|
+
$scenario[index].classList.add('active');
|
124
|
+
});
|
125
|
+
}
|
126
|
+
|
127
|
+
updateIndicators (testResultsStatuses) {
|
128
|
+
testResultsStatuses.forEach((status, index) => {
|
129
|
+
const $indicator = this.indicatorItems[index];
|
130
|
+
setIndicatorStatus($indicator, status);
|
131
|
+
});
|
132
|
+
this.highlightFailedScenario(testResultsStatuses);
|
133
|
+
}
|
134
|
+
|
135
|
+
highlightFailedScenario (testResultsStatuses) {
|
136
|
+
const currentStatus = testResultsStatuses[this.currentScenarioIndex];
|
137
|
+
const isCurrentFailed = currentStatus === 'failed';
|
138
|
+
const $failedIndicator = this.indicatorItems.filter('.failed')[0];
|
139
|
+
if($failedIndicator && !isCurrentFailed) highlightIndicator($failedIndicator);
|
140
|
+
}
|
141
|
+
|
142
|
+
resetIndicators () {
|
143
|
+
this.indicatorItems.each((_, $indicator) => {
|
144
|
+
setIndicatorStatus($indicator, 'pending');
|
145
|
+
unhighlightIndicator($indicator);
|
146
|
+
});
|
147
|
+
}
|
148
|
+
}
|
149
|
+
|
150
|
+
mumuki.MultipleScenarios = MultipleScenarios;
|
151
|
+
|
152
|
+
}(mumuki));
|
@@ -0,0 +1,18 @@
|
|
1
|
+
mumuki.load(() => {
|
2
|
+
let $popover = $('.mu-popover');
|
3
|
+
|
4
|
+
$('html').click(function(e) {
|
5
|
+
if (!e.target.closest('.popover')) {
|
6
|
+
$popover.popover('hide');
|
7
|
+
}
|
8
|
+
});
|
9
|
+
|
10
|
+
$popover.popover({
|
11
|
+
html: true,
|
12
|
+
trigger: 'manual',
|
13
|
+
container: 'body'
|
14
|
+
}).click(function(e) {
|
15
|
+
$(this).popover('toggle');
|
16
|
+
e.stopPropagation();
|
17
|
+
});
|
18
|
+
});
|
@@ -118,16 +118,11 @@ var mumuki = mumuki || {};
|
|
118
118
|
}
|
119
119
|
|
120
120
|
function animateTimeoutError(submitButton) {
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
setTimeout(function () {
|
127
|
-
image.src = mumuki.errors.error_timeout_3.url;
|
128
|
-
submitButton.enable();
|
129
|
-
}, mumuki.errors.error_timeout_2.duration);
|
130
|
-
}, mumuki.errors.error_timeout_1.duration);
|
121
|
+
let scene = new muvment.Scene($('.submission-result-error-animation'));
|
122
|
+
scene.addState(mumuki.errorState('timeout_1').onStart(submitButton.setSendText.bind(submitButton)).onEndSwitch(scene, 'timeout_2'))
|
123
|
+
.addState(mumuki.errorState('timeout_2').onEndSwitch(scene, 'timeout_3'))
|
124
|
+
.addState(mumuki.errorState('timeout_3').onStart(submitButton.enable.bind(submitButton)))
|
125
|
+
.play();
|
131
126
|
}
|
132
127
|
|
133
128
|
mumuki.submission = {
|
@@ -99,7 +99,7 @@ h1 {
|
|
99
99
|
}
|
100
100
|
}
|
101
101
|
|
102
|
-
.
|
102
|
+
.mu-last-box {
|
103
103
|
margin-bottom: 45px;
|
104
104
|
}
|
105
105
|
|
@@ -140,7 +140,15 @@ hr {
|
|
140
140
|
padding-left: 5px;
|
141
141
|
}
|
142
142
|
|
143
|
-
.mu-
|
143
|
+
.mu-content-toolbar-item {
|
144
144
|
font-size: 60%;
|
145
145
|
vertical-align: middle;
|
146
|
+
.fa {
|
147
|
+
margin: 0;
|
148
|
+
padding: 0;
|
149
|
+
}
|
150
|
+
}
|
151
|
+
|
152
|
+
h3 {
|
153
|
+
margin-top: 20px;
|
146
154
|
}
|
@@ -27,7 +27,8 @@ $kids-speech-tabs-width: 40px;
|
|
27
27
|
}
|
28
28
|
|
29
29
|
.mu-kids-states {
|
30
|
-
|
30
|
+
padding-bottom: 15px;
|
31
|
+
height: $kids-height;
|
31
32
|
}
|
32
33
|
.mu-kids-character {
|
33
34
|
padding: 15px;
|
@@ -39,7 +40,7 @@ $kids-speech-tabs-width: 40px;
|
|
39
40
|
margin-top: 15px;
|
40
41
|
}
|
41
42
|
.mu-kids-state {
|
42
|
-
height:
|
43
|
+
height: 50%;
|
43
44
|
}
|
44
45
|
.mu-kids-character {
|
45
46
|
height: $kids-characters-height;
|
@@ -115,6 +116,9 @@ $kids-speech-tabs-width: 40px;
|
|
115
116
|
ul.results-list li {
|
116
117
|
font-size: 13px
|
117
118
|
}
|
119
|
+
.mu-kids-states strong {
|
120
|
+
font-size: 16px;
|
121
|
+
}
|
118
122
|
}
|
119
123
|
|
120
124
|
@media screen and (max-width: $screen-xs-max) {
|
@@ -128,6 +132,9 @@ $kids-speech-tabs-width: 40px;
|
|
128
132
|
ul.results-list li {
|
129
133
|
font-size: 12px
|
130
134
|
}
|
135
|
+
.mu-kids-states strong {
|
136
|
+
font-size: 14px;
|
137
|
+
}
|
131
138
|
}
|
132
139
|
|
133
140
|
&.passed_with_warnings {
|
@@ -417,8 +424,8 @@ $kids-speech-tabs-width: 40px;
|
|
417
424
|
min-height: 100vh;
|
418
425
|
max-height: 100vh;
|
419
426
|
|
420
|
-
.mu-kids-
|
421
|
-
height:
|
427
|
+
.mu-kids-states {
|
428
|
+
height: 100vh;
|
422
429
|
}
|
423
430
|
.mu-kids-blocks {
|
424
431
|
height: calc(100vh - #{$kids-characters-height} - 30px);
|
@@ -39,7 +39,7 @@ class ApplicationController < ActionController::Base
|
|
39
39
|
|
40
40
|
def should_choose_organization?
|
41
41
|
current_user? &&
|
42
|
-
current_user.
|
42
|
+
current_user.has_student_granted_organizations? &&
|
43
43
|
Mumukit::Platform.implicit_organization?(request)
|
44
44
|
end
|
45
45
|
|
@@ -61,7 +61,7 @@ class ApplicationController < ActionController::Base
|
|
61
61
|
end
|
62
62
|
|
63
63
|
def validate_user_profile!
|
64
|
-
return if !current_user.
|
64
|
+
return if !current_user.has_student_granted_organizations? || current_user.profile_completed?
|
65
65
|
flash.notice = I18n.t :please_fill_profile_data
|
66
66
|
redirect_to user_path
|
67
67
|
end
|
@@ -1,6 +1,11 @@
|
|
1
1
|
module ApplicationHelper
|
2
2
|
include WithStudentPathNavigation
|
3
3
|
|
4
|
+
# html-escapes an string even if it is html-safe
|
5
|
+
def html_rescape(html)
|
6
|
+
html_escape html.to_str
|
7
|
+
end
|
8
|
+
|
4
9
|
def profile_picture
|
5
10
|
image_tag(current_user.image_url, height: 40, class: 'img-circle', onError: "this.onerror = null; this.src = '#{image_url('user_shape.png')}'")
|
6
11
|
end
|
@@ -9,13 +14,17 @@ module ApplicationHelper
|
|
9
14
|
"<div class=\"text-center\">#{super(object, {theme: 'twitter-bootstrap-3'}.merge(options))}</div>".html_safe
|
10
15
|
end
|
11
16
|
|
12
|
-
def
|
17
|
+
def last_box_class(trailing_boxes)
|
18
|
+
trailing_boxes ? '' : 'mu-last-box'
|
19
|
+
end
|
20
|
+
|
21
|
+
def corollary_box(with_corollary, trailing_boxes = false)
|
13
22
|
if with_corollary.corollary.present?
|
14
23
|
%Q{
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
}.html_safe
|
24
|
+
<div class="#{last_box_class trailing_boxes}">
|
25
|
+
<p>#{with_corollary.corollary_html}</p>
|
26
|
+
</div>
|
27
|
+
}.html_safe
|
19
28
|
end
|
20
29
|
end
|
21
30
|
|
@@ -2,11 +2,23 @@ module AssetsHelper
|
|
2
2
|
def assets_include_tags
|
3
3
|
%Q{
|
4
4
|
<meta name="turbolinks-cache-control" content="no-cache">
|
5
|
+
#{laboratory_assets_include_tags}
|
6
|
+
#{organization_assets_include_tags}
|
7
|
+
}.html_safe
|
8
|
+
end
|
9
|
+
|
10
|
+
def laboratory_assets_include_tags
|
11
|
+
%Q{
|
5
12
|
#{stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'}
|
6
13
|
#{javascript_include_tag 'application', 'data-turbolinks-track': 'reload'}
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def organization_assets_include_tags
|
18
|
+
%Q{
|
7
19
|
<link rel="icon" type="image/x-icon" href="#{Organization.current.favicon_url}" data-turbolinks-track="reload">
|
8
20
|
<link rel="stylesheet" type="text/css" href="#{theme_stylesheet_url}" data-turbolinks-track="reload">
|
9
21
|
<script type="text/javascript" src="#{extension_javascript_url}" defer data-turbolinks-track="reload"></script>
|
10
|
-
}
|
22
|
+
}
|
11
23
|
end
|
12
24
|
end
|
data/app/helpers/links_helper.rb
CHANGED
@@ -22,6 +22,20 @@ module LinksHelper
|
|
22
22
|
app.organic_url(Organization.current.name)
|
23
23
|
end
|
24
24
|
|
25
|
+
def teacher_info_button(item)
|
26
|
+
if current_user&.teacher_here? && item.teacher_info.present?
|
27
|
+
%Q{
|
28
|
+
<a
|
29
|
+
class="mu-content-toolbar-item mu-popover"
|
30
|
+
data-toggle="popover"
|
31
|
+
data-html="true"
|
32
|
+
title="#{t :teacher_info}"
|
33
|
+
data-placement="bottom"
|
34
|
+
data-content="#{html_rescape item.teacher_info_html}">#{fixed_fa_icon('info-circle')}</a>
|
35
|
+
}.html_safe
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
25
39
|
def link_to_bibliotheca_guide(guide)
|
26
40
|
edit_link_to_bibliotheca { url_for_bibliotheca_guide(guide) }
|
27
41
|
end
|
@@ -58,7 +72,7 @@ module LinksHelper
|
|
58
72
|
return unless current_user&.writer?
|
59
73
|
|
60
74
|
url = yield
|
61
|
-
link_to fixed_fa_icon(:pencil), url, class: "mu-
|
75
|
+
link_to fixed_fa_icon(:pencil), url, class: "mu-content-toolbar-item", target: "_blank", title: t(:edit)
|
62
76
|
end
|
63
77
|
|
64
78
|
def url_for_bibliotheca_guide(guide)
|
@@ -4,7 +4,7 @@ module ResetButtonHelper
|
|
4
4
|
end
|
5
5
|
|
6
6
|
def restart_guide_link(guide)
|
7
|
-
link_to restart_icon, guide_progress_path(guide), data: {confirm: t(:confirm_restart)}, method: :delete
|
7
|
+
link_to restart_icon, guide_progress_path(guide), class: 'mu-content-toolbar-item', data: {confirm: t(:confirm_restart)}, method: :delete
|
8
8
|
end
|
9
9
|
|
10
10
|
end
|