mumuki-laboratory 8.3.0 → 9.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/mumuki_laboratory/application/codemirror.js +1 -1
  3. data/app/assets/javascripts/mumuki_laboratory/application/upload.js +69 -14
  4. data/app/assets/stylesheets/mumuki_laboratory/application.scss +1 -0
  5. data/app/assets/stylesheets/mumuki_laboratory/application/_codemirror-themes.scss +1 -0
  6. data/app/assets/stylesheets/mumuki_laboratory/application/_layout.scss +3 -0
  7. data/app/assets/stylesheets/mumuki_laboratory/application/_modules.scss +1 -0
  8. data/app/assets/stylesheets/mumuki_laboratory/application/codemirror-themes/_mu-light.scss +3 -0
  9. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_content_show.scss +15 -2
  10. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_discussion.scss +31 -8
  11. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_medal.scss +1 -1
  12. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_user_menu.scss +35 -0
  13. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_user_profile.scss +11 -0
  14. data/app/controllers/api/base_controller.rb +0 -1
  15. data/app/controllers/api/courses_controller.rb +1 -1
  16. data/app/controllers/api/organizations_controller.rb +5 -2
  17. data/app/controllers/api/roles_controller.rb +4 -0
  18. data/app/controllers/api/users_controller.rb +6 -1
  19. data/app/controllers/application_controller.rb +1 -1
  20. data/app/controllers/concerns/with_authorization.rb +1 -16
  21. data/app/controllers/concerns/with_user_params.rb +4 -0
  22. data/app/controllers/discussions_messages_controller.rb +0 -1
  23. data/app/controllers/exam_authorization_requests_controller.rb +26 -0
  24. data/app/controllers/exam_registrations_controller.rb +6 -0
  25. data/app/controllers/users_controller.rb +8 -5
  26. data/app/helpers/application_helper.rb +4 -0
  27. data/app/helpers/breadcrumbs_helper.rb +4 -0
  28. data/app/helpers/content_view_helper.rb +19 -0
  29. data/app/helpers/exercise_input_helper.rb +8 -17
  30. data/app/helpers/icons_helper.rb +3 -11
  31. data/app/helpers/links_helper.rb +2 -2
  32. data/app/helpers/menu_bar_helper.rb +3 -3
  33. data/app/helpers/overlapped_buttons_helper.rb +10 -6
  34. data/app/helpers/progress_bar_helper.rb +2 -2
  35. data/app/helpers/user_menu_helper.rb +18 -0
  36. data/app/views/chapters/show.html.erb +17 -16
  37. data/app/views/complements/show.html.erb +1 -1
  38. data/app/views/discussions/_message.html.erb +7 -7
  39. data/app/views/exam_authorization_requests/show.html.erb +17 -0
  40. data/app/views/exam_registrations/show.html.erb +37 -0
  41. data/app/views/exams/show.html.erb +1 -1
  42. data/app/views/{layouts → exercises}/_exercise_skipped.html.erb +0 -0
  43. data/app/views/exercises/_exercise_title_icons.html.erb +4 -0
  44. data/app/views/exercises/show.html.erb +5 -8
  45. data/app/views/{layouts → guides}/_guide.html.erb +3 -3
  46. data/app/views/guides/_guide_container.html.erb +24 -0
  47. data/app/views/{layouts → guides}/_guide_title_icons.html.erb +1 -3
  48. data/app/views/layouts/_progress_bar.html.erb +9 -7
  49. data/app/views/layouts/_progress_listing.html.erb +5 -5
  50. data/app/views/layouts/_user_menu.html.erb +21 -0
  51. data/app/views/layouts/application.html.erb +1 -6
  52. data/app/views/layouts/exercise_inputs/editors/_upload.html.erb +11 -2
  53. data/app/views/lessons/show.html.erb +1 -1
  54. data/app/views/notifications/_discussion.html.erb +1 -0
  55. data/app/views/notifications/_dropdown.html.erb +13 -0
  56. data/app/views/notifications/_exam_authorization_request.html.erb +1 -0
  57. data/app/views/notifications/_exam_registration.html.erb +1 -0
  58. data/app/views/notifications/_message.html.erb +1 -0
  59. data/app/views/users/_user_form.html.erb +10 -8
  60. data/app/views/users/discussions.html.erb +28 -0
  61. data/app/views/users/edit.html.erb +1 -1
  62. data/app/views/users/messages.html.erb +27 -0
  63. data/app/views/users/show.html.erb +4 -51
  64. data/app/views/users/terms.html.erb +2 -2
  65. data/config/routes.rb +6 -0
  66. data/lib/mumuki/laboratory/controllers/notifications.rb +3 -22
  67. data/lib/mumuki/laboratory/locales/en.yml +35 -18
  68. data/lib/mumuki/laboratory/locales/es-CL.yml +26 -9
  69. data/lib/mumuki/laboratory/locales/es.yml +31 -15
  70. data/lib/mumuki/laboratory/locales/pt.yml +28 -11
  71. data/lib/mumuki/laboratory/version.rb +1 -1
  72. data/spec/controllers/exam_authorization_requests_controller_spec.rb +40 -0
  73. data/spec/controllers/exam_registrations_controller_spec.rb +19 -0
  74. data/spec/controllers/organizations_api_controller_spec.rb +16 -9
  75. data/spec/dummy/db/schema.rb +24 -0
  76. data/spec/features/exercise_flow_spec.rb +3 -3
  77. data/spec/features/login_flow_spec.rb +1 -1
  78. data/spec/features/menu_bar_spec.rb +24 -24
  79. data/spec/features/notifications_flow_spec.rb +46 -0
  80. data/spec/features/profile_flow_spec.rb +6 -9
  81. data/spec/features/terms_flow_spec.rb +30 -0
  82. data/spec/javascripts/bridge-spec.js +2 -2
  83. data/spec/javascripts/csrf-token-spec.js +2 -2
  84. data/spec/javascripts/editors-spec.js +7 -9
  85. data/spec/javascripts/elipsis-spec.js +4 -4
  86. data/spec/javascripts/events-spec.js +7 -7
  87. data/spec/javascripts/exercise-spec.js +7 -8
  88. data/spec/javascripts/global-spec.js +3 -3
  89. data/spec/javascripts/i18n-spec.js +23 -20
  90. data/spec/javascripts/kids-button-spec.js +6 -8
  91. data/spec/javascripts/results-renderers-spec.js +5 -5
  92. data/spec/javascripts/speech-bubble-renderer-spec.js +2 -3
  93. data/spec/javascripts/submissions-store-spec.js +14 -14
  94. data/spec/javascripts/sync-mode-spec.js +3 -3
  95. data/spec/javascripts/timeout-spec.js +2 -2
  96. data/spec/javascripts/timer-spec.js +2 -2
  97. data/spec/javascripts/upload-spec.js +80 -0
  98. metadata +129 -103
  99. data/app/views/layouts/_guide_container.html.erb +0 -28
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ feature 'Notifications Flow', organization_workspace: :test do
4
+ let!(:chapter) { create(:chapter) }
5
+ let(:organization) { Organization.current }
6
+ let(:user) { create(:user) }
7
+
8
+ before { reindex_current_organization! }
9
+ before { set_current_user!(user) }
10
+
11
+ def notifications_bell
12
+ find '.badge-notifications'
13
+ end
14
+
15
+ def find_notification_number(number)
16
+ find('#notificationsDropdown').click
17
+ find "#notificationsPanel li:nth-child(#{number}) a"
18
+ end
19
+
20
+ context 'user with notifications' do
21
+ let(:exam_registration) { create(:exam_registration, description: 'Mid term exam 2020') }
22
+ let(:exam_authorization_request) { create(:exam_authorization_request, exam_registration: exam_registration, user: user) }
23
+
24
+ let!(:notifications) do
25
+ [exam_registration, exam_authorization_request].map { |target| create(:notification, user: user, target: target ) }
26
+ end
27
+
28
+ before { visit '/' }
29
+
30
+ scenario 'displays count on navigation bar' do
31
+ expect(notifications_bell).to have_text('2')
32
+ end
33
+
34
+ scenario 'navigates to target on notification click' do
35
+ find_notification_number(2).click
36
+ expect(page).to have_text 'Registration to Mid term exam 2020'
37
+ expect(page).to have_text 'Choose date and time to attend to the exam'
38
+ end
39
+
40
+ scenario 'removes notification after target is processed' do
41
+ # Notification for exam authorization request is considered read after user click
42
+ find_notification_number(1).click
43
+ expect(notifications_bell).to have_text('1')
44
+ end
45
+ end
46
+ end
@@ -95,34 +95,31 @@ feature 'Profile Flow', organization_workspace: :test do
95
95
  visit "/user/edit"
96
96
  fill_in('user_first_name', with: 'first_name')
97
97
 
98
- # Match :first is used because there are two buttons: mobile and desktop.
99
98
  click_on(button_options)
100
- expect(page).to have_text('Your data was updated successfuly')
101
- expect(page).to have_text('Profile')
99
+ expect(page).to have_text('Your data was updated successfully')
100
+ expect(page).to have_text('My profile')
102
101
  end
103
102
  end
104
103
  end
105
104
 
106
105
  context 'with no messages' do
107
-
108
- scenario 'visit messages tab' do
109
- visit "/user#messages"
106
+ scenario 'visit messages' do
107
+ visit "/user/messages"
110
108
 
111
109
  expect(page).to have_text('It seems you don\'t have any messages yet!')
112
110
  end
113
111
  end
114
112
 
115
113
  context 'with messages' do
116
- scenario 'visit messages tab' do
114
+ scenario 'visit messages' do
117
115
  Organization.find_by_name('test').switch!
118
116
  problem.submit_solution! user, {content: 'something'}
119
117
  Message.import_from_resource_h! message
120
- visit "/user#messages"
118
+ visit "/user/messages"
121
119
 
122
120
  expect(page).to_not have_text('It seems you don\'t have any messages yet!')
123
121
  expect(page).to have_text(problem.name)
124
122
  end
125
123
  end
126
124
  end
127
-
128
125
  end
@@ -153,5 +153,35 @@ feature 'Terms Flow', organization_workspace: :test do
153
153
  end
154
154
  end
155
155
 
156
+ context 'with incognito mode' do
157
+ before { test_organization.update! incognito_mode_enabled: true }
158
+
159
+ describe 'visit user terms path' do
160
+ let(:terms_path) { '/user/terms' }
161
+ let(:expected_terms) { general_terms_scopes }
162
+
163
+ it_behaves_like 'has expected terms'
164
+ end
165
+
166
+ scenario 'visit any other path' do
167
+ visit '/'
168
+
169
+ expect(page).to have_text('Start Practicing')
170
+ end
171
+
172
+ context 'visit forum' do
173
+ let(:terms_path) { '/discussions/terms' }
174
+
175
+ context 'with enabled forum' do
176
+ before { test_organization.update! forum_enabled: true }
177
+
178
+ scenario 'visit forum' do
179
+ visit '/discussions/terms'
180
+ expect(page).to have_text('You may have mistyped the address or the page may have moved')
181
+ end
182
+ end
183
+ end
184
+ end
185
+
156
186
  end
157
187
 
@@ -1,5 +1,5 @@
1
1
  describe('bridge', () => {
2
2
  it('can create bridge', () => {
3
3
  expect(new mumuki.bridge.Laboratory()).not.toBe(null);
4
- })
5
- })
4
+ });
5
+ });
@@ -1,7 +1,7 @@
1
1
  describe('csrf token', () => {
2
2
  it('can create token', () => {
3
3
  expect(new mumuki.CsrfToken()).not.toBe(null);
4
- })
5
- })
4
+ });
5
+ });
6
6
 
7
7
 
@@ -1,11 +1,10 @@
1
1
  describe('editors', () => {
2
-
3
2
  beforeEach(() => {
4
- mumuki.CustomEditor.clearSources()
5
- })
3
+ mumuki.CustomEditor.clearSources();
4
+ });
6
5
 
7
6
  it('has initially no sources', () => {
8
- expect(mumuki.CustomEditor.hasSources).toBe(false)
7
+ expect(mumuki.CustomEditor.hasSources).toBe(false);
9
8
  });
10
9
 
11
10
  it('can add a custom source', () => {
@@ -26,7 +25,7 @@ describe('editors', () => {
26
25
  <div class="field form-group editor-code">
27
26
  <textarea class="form-control editor" name="solution[content]" id="solution_content">the standard solution</textarea>
28
27
  </div>
29
- </form>`)
28
+ </form>`);
30
29
 
31
30
  mumuki.editors.addCustomSource({
32
31
  getContent() {
@@ -43,11 +42,10 @@ describe('editors', () => {
43
42
  <div class="field form-group editor-code">
44
43
  <textarea class="form-control editor" name="solution[content]" id="solution_content">the solution</textarea>
45
44
  </div>
46
- </form>`)
45
+ </form>`);
47
46
  expect(mumuki.editors.getSubmission()).toEqual({"solution[content]":"the solution"});
48
47
  });
49
48
 
50
-
51
49
  it('reads the form if no sources and exercise is multifile', () => {
52
50
  $('body').html(`
53
51
  <form role="form" class="new_solution">
@@ -63,7 +61,7 @@ describe('editors', () => {
63
61
  name="solution[content[receta.css]]"
64
62
  id="solution_content[receta.css]">some css</textarea>
65
63
  </div>
66
- </form>`)
64
+ </form>`);
67
65
  expect(mumuki.editors.getSubmission()).toEqual({
68
66
  "solution[content[index.html]]": "some html",
69
67
  "solution[content[receta.css]]": "some css"
@@ -74,4 +72,4 @@ describe('editors', () => {
74
72
  $('body').html(``);
75
73
  expect(mumuki.editors.getSubmission()).toEqual({});
76
74
  });
77
- })
75
+ });
@@ -1,7 +1,7 @@
1
1
  describe('elipsis', () => {
2
2
  it('does nothing when no elipsis', () => {
3
3
  expect(mumuki.elipsis('hello')).toEqual('hello');
4
- })
4
+ });
5
5
 
6
6
  it('replaces student elipsis', () => {
7
7
  expect(mumuki.elipsis(`function longitud(unString) {
@@ -10,7 +10,7 @@ describe('elipsis', () => {
10
10
  /*@elipsis-for-student&gt;*/
11
11
  }`)).toEqual(`function longitud(unString) {
12
12
  /* ... */
13
- }`)
13
+ }`);
14
14
  });
15
15
 
16
16
  it('replaces student hidden', () => {
@@ -20,6 +20,6 @@ describe('elipsis', () => {
20
20
  /*@hidden-for-student&gt;*/
21
21
  }`)).toEqual(`function longitud(unString) {
22
22
  /**/
23
- }`)
23
+ }`);
24
24
  });
25
- })
25
+ });
@@ -6,18 +6,18 @@ describe('events', () => {
6
6
  it('is not called when it is not fired', () => {
7
7
  mumuki.events.on('foo', (e) => {
8
8
  fail(`should not be called, but got ${JSON.stringify(e)}`);
9
- })
10
- })
9
+ });
10
+ });
11
11
 
12
12
  it('is not called when it is fired but not enabled', () => {
13
13
  let fired = false;
14
14
  mumuki.events.on('foo', (e) => {
15
15
  fail(`should not be called, but got ${JSON.stringify(e)}`);
16
16
  fired = true;
17
- })
17
+ });
18
18
  mumuki.events.fire('foo', 42);
19
19
  expect(fired).toBe(false);
20
- })
20
+ });
21
21
 
22
22
  it('is called when it is fired and enabled', () => {
23
23
  let fired = false;
@@ -25,9 +25,9 @@ describe('events', () => {
25
25
  mumuki.events.on('foo', (event) => {
26
26
  expect(event).toEqual(42);
27
27
  fired = true;
28
- })
28
+ });
29
29
 
30
30
  mumuki.events.fire('foo', 42);
31
31
  expect(fired).toBe(true);
32
- })
33
- })
32
+ });
33
+ });
@@ -1,10 +1,9 @@
1
1
  describe('exercise', () => {
2
-
3
2
  it('current exercise information is available when present', () => {
4
3
  $('body').html(`
5
4
  <input type="hidden" name="mu-exercise-id" id="mu-exercise-id" value="3361" />
6
5
  <input type="hidden" name="mu-exercise-layout" id="mu-exercise-layout" value="input_right" />
7
- <input type="hidden" name="mu-exercise-settings" id="mu-exercise-settings" value="{}" />`)
6
+ <input type="hidden" name="mu-exercise-settings" id="mu-exercise-settings" value="{}" />`);
8
7
 
9
8
  mumuki.exercise.load();
10
9
 
@@ -12,13 +11,13 @@ describe('exercise', () => {
12
11
  expect(mumuki.exercise.layout).toBe('input_right');
13
12
  expect(mumuki.exercise.settings).toEqual({});
14
13
  expect(mumuki.exercise.current).not.toBe(null);
15
- })
14
+ });
16
15
 
17
16
  it('current exercise information is available when present and settings are not empty', () => {
18
17
  $('body').html(`
19
18
  <input type="hidden" name="mu-exercise-id" id="mu-exercise-id" value="3361" />
20
19
  <input type="hidden" name="mu-exercise-layout" id="mu-exercise-layout" value="input_right" />
21
- <input type="hidden" name="mu-exercise-settings" id="mu-exercise-settings" value="{&quot;game_framework&quot;:true}" />`)
20
+ <input type="hidden" name="mu-exercise-settings" id="mu-exercise-settings" value="{&quot;game_framework&quot;:true}" />`);
22
21
 
23
22
  mumuki.exercise.load();
24
23
 
@@ -26,10 +25,10 @@ describe('exercise', () => {
26
25
  expect(mumuki.exercise.layout).toBe('input_right');
27
26
  expect(mumuki.exercise.settings.game_framework).toBe(true);
28
27
  expect(mumuki.exercise.current).not.toBe(null);
29
- })
28
+ });
30
29
 
31
30
  it('current exercise information is available when not present', () => {
32
- $('body').html(``)
31
+ $('body').html(``);
33
32
 
34
33
  mumuki.exercise.load();
35
34
 
@@ -37,5 +36,5 @@ describe('exercise', () => {
37
36
  expect(mumuki.exercise.layout).toBe(null);
38
37
  expect(mumuki.exercise.settings).toBe(null);
39
38
  expect(mumuki.exercise.current).toBe(null);
40
- })
41
- })
39
+ });
40
+ });
@@ -1,6 +1,6 @@
1
1
  describe("global loading", () => {
2
2
  it("produces no global loading errors", () => {
3
3
  const error = window['__globalLoadingError__'];
4
- expect(!error).toBe(true, `Expected no global loading errors but got ${error && error.message}`)
5
- })
6
- })
4
+ expect(!error).toBe(true, `Expected no global loading errors but got ${error && error.message}`);
5
+ });
6
+ });
@@ -1,29 +1,30 @@
1
1
  describe('I18n', () => {
2
-
3
2
  describe('t / translate', () => {
4
-
5
3
  it('accept english translations', () => {
6
4
  mumuki.locale = 'en';
7
5
  expect(mumuki.I18n.translate('passed')).toBe('Everything is in order! Your solution passed all our tests!');
8
- })
6
+ });
7
+
9
8
  it('accept spanish translations', () => {
10
9
  mumuki.locale = 'es';
11
10
  expect(mumuki.I18n.translate('passed_with_warnings')).toBe('Tu solución funcionó, pero hay cosas que mejorar');
12
- })
11
+ });
12
+
13
13
  it('accept chilean translations', () => {
14
14
  mumuki.locale = 'es-CL';
15
15
  expect(mumuki.I18n.translate('failed')).toBe('Tu solución no pasó las pruebas');
16
- })
16
+ });
17
+
17
18
  it('accept portuguese translations', () => {
18
19
  mumuki.locale = 'pt';
19
20
  expect(mumuki.I18n.translate('errored')).toBe('Opa! Sua solução não pode ser executada');
20
- })
21
+ });
22
+
21
23
  it('fails when translation missing', () => {
22
24
  mumuki.locale = 'en';
23
25
  expect(mumuki.I18n.translate('foo')).toBe('Translation missing: en, `foo`');
24
- })
25
-
26
- })
26
+ });
27
+ });
27
28
 
28
29
  describe('register', () => {
29
30
  beforeEach(() => {
@@ -33,21 +34,23 @@ describe('I18n', () => {
33
34
  greet: (data) => `Hi ${data.name}`,
34
35
  errored: "D'Oh!"
35
36
  }
36
- })
37
- })
37
+ });
38
+ });
39
+
38
40
  it('overrides existing translation key', () => {
39
41
  expect(mumuki.I18n.translate('errored')).toBe("D'Oh!");
40
- })
42
+ });
43
+
41
44
  it('add missing translation key', () => {
42
45
  expect(mumuki.I18n.translate('greet', {name: 'Jane'})).toBe('Hi Jane');
43
- })
46
+ });
47
+
44
48
  it('keep non overriding translation key', () => {
45
49
  expect(mumuki.I18n.translate('passed')).toBe('Everything is in order! Your solution passed all our tests!');
46
- })
47
- })
50
+ });
51
+ });
48
52
 
49
53
  describe('with some prefix', () => {
50
-
51
54
  fixture.set(`
52
55
  <div class="mu-kindergarten" data-i18n-prefix="testing">
53
56
  <button class="mu-kids-button">Click me<button>
@@ -61,19 +64,19 @@ describe('I18n', () => {
61
64
  en: {
62
65
  testing_failed: 'Ops! Execution failed',
63
66
  }
64
- })
67
+ });
65
68
  });
66
69
 
67
70
  it('Using prefix with existing translation key', () => {
68
71
  expect(mumuki.I18n.t('passed')).toBe('Everything is in order! Your solution passed all our tests!');
69
72
  });
73
+
70
74
  it('Use prefix with none existing translation key but default key exists', () => {
71
75
  expect(mumuki.I18n.t('failed')).toBe('Ops! Execution failed');
72
76
  });
77
+
73
78
  it('No key found', () => {
74
79
  expect(mumuki.I18n.t('foo')).toBe('Translation missing: en, `foo`');
75
- })
76
-
80
+ });
77
81
  });
78
-
79
82
  });
@@ -1,5 +1,4 @@
1
1
  describe('KidsButton', () => {
2
-
3
2
  let button;
4
3
 
5
4
  fixture.set(`
@@ -12,25 +11,24 @@ describe('KidsButton', () => {
12
11
  beforeEach(() => {
13
12
  mumuki.kids = new mumuki.Kids();
14
13
  button = new mumuki.submission.KidsSubmitButton($('.mu-kids-button'));
15
- })
14
+ });
16
15
 
17
16
  it('can create button', () => {
18
17
  expect(button).not.toBe(null);
19
- })
18
+ });
20
19
 
21
20
  it('overlay is hidden by default', () => {
22
21
  expect(mumuki.kids.$overlay.css('display')).toBe('none');
23
- })
22
+ });
24
23
 
25
24
  it('call showOverlay on wait', () => {
26
25
  button.wait();
27
26
  expect(mumuki.kids.$overlay.css('display')).toBe('block');
28
- })
27
+ });
29
28
 
30
29
  it('call hideOverlay on continue', () => {
31
30
  button.wait();
32
31
  button.continue();
33
32
  expect(mumuki.kids.$overlay.css('display')).toBe('none');
34
- })
35
-
36
- })
33
+ });
34
+ });