mumuki-laboratory 9.13.1 → 9.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/mumuki_laboratory/application/codemirror-builder.js +15 -2
  3. data/app/assets/javascripts/mumuki_laboratory/application/codemirror.js +3 -2
  4. data/app/assets/javascripts/mumuki_laboratory/application/console.js +7 -1
  5. data/app/assets/javascripts/mumuki_laboratory/application/discussions.js +13 -17
  6. data/app/assets/javascripts/mumuki_laboratory/application/exercise.js +20 -0
  7. data/app/assets/javascripts/mumuki_laboratory/application/i18n.js +4 -0
  8. data/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js +6 -5
  9. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_breadcrumb.scss +11 -0
  10. data/app/controllers/ajax_controller.rb +5 -0
  11. data/app/controllers/application_controller.rb +7 -0
  12. data/app/controllers/discussions_controller.rb +13 -11
  13. data/app/controllers/exam_authorization_requests_controller.rb +1 -1
  14. data/app/controllers/exercise_solutions_controller.rb +1 -1
  15. data/app/controllers/users_controller.rb +4 -1
  16. data/app/helpers/editor_helper.rb +6 -1
  17. data/app/helpers/exam_registration_helper.rb +1 -1
  18. data/app/views/discussions/_new_message.html.erb +1 -1
  19. data/app/views/exam_authorization_requests/_pending.html.erb +12 -5
  20. data/app/views/exam_registrations/show.html.erb +11 -9
  21. data/app/views/exercises/_read_only.html.erb +27 -35
  22. data/app/views/layouts/application.html.erb +7 -0
  23. data/lib/mumuki/laboratory/locales/en.yml +1 -0
  24. data/lib/mumuki/laboratory/locales/es-CL.yml +7 -4
  25. data/lib/mumuki/laboratory/locales/es.yml +7 -4
  26. data/lib/mumuki/laboratory/locales/pt.yml +1 -0
  27. data/lib/mumuki/laboratory/version.rb +1 -1
  28. data/spec/features/read_only_flow_spec.rb +13 -0
  29. metadata +119 -119
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b47dd283c616c348e2ce6aab4a61cb1e12ec68fa902fcff158e8dc6a7bf63a64
4
- data.tar.gz: 44a250723c61c458d582fe87b62dab762ee846a687c74d9c5e738a9eb5b14f92
3
+ metadata.gz: 67dd086ce64c96505cea38bf9fdf857f304acd8322d91e4c139e0a847e89ce46
4
+ data.tar.gz: 48b771a27f10a159b4b10e38e88cee08f4f032969fbe7a533d188dd4f50cc0b5
5
5
  SHA512:
6
- metadata.gz: 2f39b477c86a46e4eaa33535adfd2adea92fe78f2fdc858c893b6eff24ff16c943b73a3a846045fe6af5ce0b80b6251614c17af13ef29d551ce7a198d2636d94
7
- data.tar.gz: 78e3cf4b67a33b11a3dccb90551ea2382714e5f1ab3b3bdfecdc3cbeabeb45af53178fbd6843fd78859f4e5844afc56656191eba00cffa6431460d8f98858ee7
6
+ metadata.gz: a28dd46787bfb3b9fb50ab7ae7b6d1421e8abb5ec9b0f4f9c20bc4af62da0a5343c6a0973ebad6073af95f66178c49d0f4cccac36f11e1ce6245a23e4527143c
7
+ data.tar.gz: 3ba41f5eee2cdd908ecedbe5694de3738715f99a2c3e6c6e17d0049d2224f74be08fbbcf72b4910a4e4a7c440e05e2b87130466055919ba1b0b704d15ed29688
@@ -27,7 +27,11 @@
27
27
  this.$textarea = $(textarea);
28
28
  }
29
29
 
30
- setupEditor() {
30
+ setupEditor(readonly = false) {
31
+ return readonly ? this._setupReadOnlyEditor() : this._setupCommonEditor();
32
+ }
33
+
34
+ _setupCommonEditor() {
31
35
  this.editor = this.createEditor({
32
36
  lineNumbers: true,
33
37
  extraKeys: {
@@ -61,7 +65,7 @@
61
65
  return this;
62
66
  }
63
67
 
64
- setupReadOnlyEditor() {
68
+ _setupReadOnlyEditor() {
65
69
  this.editor = this.createEditor({
66
70
  readOnly: true,
67
71
  cursorBlinkRate: -1, //Hides the cursor
@@ -71,6 +75,15 @@
71
75
  return this;
72
76
  }
73
77
 
78
+ setupSpellCheckedEditor() {
79
+ this.editor = this.createEditor({
80
+ inputStyle: 'contenteditable',
81
+ spellcheck: true
82
+ });
83
+
84
+ return this;
85
+ }
86
+
74
87
  setupLanguage(language) {
75
88
  var highlightMode = language || this.$textarea.data('editor-language');
76
89
  if (highlightMode === 'dynamic') {
@@ -6,10 +6,11 @@ mumuki.page.editors = [];
6
6
  (() => {
7
7
  function createCodeMirrors() {
8
8
  return $(".editor").map(function (index, textarea) {
9
- var $textarea = $("#solution_content");
9
+ const $textarea = $("#solution_content");
10
+ const readonly = $textarea.data('readonly');
10
11
 
11
12
  return new mumuki.editor.CodeMirrorBuilder(textarea)
12
- .setupEditor()
13
+ .setupEditor(readonly)
13
14
  .setupMinLines($textarea.data('lines'))
14
15
  .setupLanguage()
15
16
  .build();
@@ -87,7 +87,7 @@
87
87
  }
88
88
  get content() {
89
89
  var firstEditor = mumuki.page.editors[0];
90
- if (firstEditor && $("#include_solution").prop("checked"))
90
+ if (firstEditor && this.includeSolution())
91
91
  return firstEditor.getValue();
92
92
  else
93
93
  return '';
@@ -142,6 +142,12 @@
142
142
  get _requestData() {
143
143
  return {content: this.content, query: this.line, cookie: this.cookie};
144
144
  }
145
+ includeSolution() {
146
+ return !this._includeSolutionCheckbox || this._includeSolutionCheckbox.checked;
147
+ }
148
+ get _includeSolutionCheckbox() {
149
+ return $("#include_solution")[0];
150
+ }
145
151
  }
146
152
 
147
153
 
@@ -9,34 +9,28 @@ mumuki.load(() => {
9
9
 
10
10
  function createNewMessageEditor() {
11
11
  var $textarea = $("#discussion-new-message");
12
- var textarea = $textarea[0];
13
- if (!textarea) return;
12
+ var editorContainer = $(".mu-spell-checked-editor")[0];
13
+ if (!editorContainer) return;
14
14
 
15
- return new mumuki.editor.CodeMirrorBuilder(textarea)
16
- .setupSimpleEditor()
15
+ return new mumuki.editor.CodeMirrorBuilder(editorContainer)
16
+ .setupSpellCheckedEditor()
17
17
  .setupMinLines($textarea.data('lines'))
18
+ .setupLanguage()
18
19
  .build();
19
20
  }
20
21
 
21
- function createReadOnlyEditors() {
22
- return $(".read-only-editor").map(function (index, textarea) {
23
- var $textarea = $("#solution_content");
24
-
25
- return new mumuki.editor.CodeMirrorBuilder(textarea)
26
- .setupReadOnlyEditor()
27
- .setupMinLines($textarea.data('lines'))
28
- .setupLanguage()
29
- .build();
30
- });
31
- }
32
-
33
- createReadOnlyEditors();
34
22
  let editor = createNewMessageEditor();
35
23
 
36
24
  var Forum = {
37
25
  toggleButton: function (elements) {
38
26
  elements.toggleClass('d-none');
39
27
  },
28
+ disableButton: function (elements) {
29
+ elements.attr('disabled', true);
30
+ },
31
+ reenableButton: function (elements) {
32
+ elements.attr('disabled', false);
33
+ },
40
34
  token: new mumuki.CsrfToken(),
41
35
  tokenRequest: function (data) {
42
36
  return $.ajax(Forum.token.newRequest(data));
@@ -55,6 +49,7 @@ mumuki.load(() => {
55
49
  Forum.discussionPostAndToggle(url, $upvoteButtons);
56
50
  },
57
51
  discussionResponsible: function (url) {
52
+ Forum.disableButton($responsibleButton);
58
53
  Forum.discussionPostToggleAndRenderToast(url, $responsibleButton);
59
54
  $('.responsible-moderator-badge').toggleClass('d-none');
60
55
  },
@@ -65,6 +60,7 @@ mumuki.load(() => {
65
60
  Forum.discussionPost(url)
66
61
  .done(function (response) {
67
62
  Forum.toggleButton(elem);
63
+ Forum.reenableButton(elem);
68
64
  mumuki.toast.addToast(response);
69
65
  })
70
66
  .fail(function (response) {
@@ -1,3 +1,23 @@
1
+ (() => {
2
+ function solutionChangedSinceLastSubmission() {
3
+ return mumuki.exercise.id &&
4
+ mumuki.SubmissionsStore.getLastSubmissionAndResult(mumuki.exercise.id) &&
5
+ !mumuki.SubmissionsStore.getSubmissionResultFor(mumuki.exercise.id, mumuki.editors.getSubmission());
6
+ }
7
+
8
+ window.addEventListener("beforeunload", (event) => {
9
+ if (solutionChangedSinceLastSubmission()) {
10
+ event.returnValue = 'unsaved_progress';
11
+ } else {
12
+ delete event['returnValue'];
13
+ }
14
+ });
15
+
16
+ window.addEventListener("turbolinks:before-visit", (event) => {
17
+ if (solutionChangedSinceLastSubmission() && !confirm(mumuki.I18n.t('unsaved_progress'))) event.preventDefault();
18
+ });
19
+ })();
20
+
1
21
  /**
2
22
  * @typedef {"input_right" | "input_bottom" | "input_primary" | "input_kindergarten"} Layout
3
23
  * @typedef {{id: number, layout: Layout, settings: any}} Exercise
@@ -9,6 +9,7 @@ mumuki.I18n = (() => {
9
9
  passed_with_warnings: () => "Tu solución funcionó, pero hay cosas que mejorar",
10
10
  pending: () => "Pendiente",
11
11
  skipped: () => "Venís aprendiendo muy bien, por lo que aprobaste este ejercicio",
12
+ unsaved_progress: () => "Tu solución tiene cambios sin guardar, ¿Querés salir de todos modos?",
12
13
  },
13
14
  'es-CL': {
14
15
  aborted: () => "Ups, no pudimos evaluar tu solución",
@@ -18,6 +19,7 @@ mumuki.I18n = (() => {
18
19
  passed_with_warnings: () => "Tu solución funcionó, pero hay cosas que mejorar",
19
20
  pending: () => "Pendiente",
20
21
  skipped: () => "Vienes aprendiendo muy bien, por lo que aprobaste este ejercicio",
22
+ unsaved_progress: () => "Tu solución tiene cambios sin guardar, ¿Quieres salir de todos modos?",
21
23
  },
22
24
  'en': {
23
25
  aborted: () => "Oops, we couldn't evaluate your solution",
@@ -27,6 +29,7 @@ mumuki.I18n = (() => {
27
29
  passed_with_warnings: () => "It worked, but you can do better",
28
30
  pending: () => "Pending",
29
31
  skipped: () => "You are doing very well, so you've passed this exercise",
32
+ unsaved_progress: () => "Your solution has unsaved changes, leave anyways?",
30
33
  },
31
34
  'pt': {
32
35
  aborted: () => "Opa, não pudemos avaliar sua solução",
@@ -36,6 +39,7 @@ mumuki.I18n = (() => {
36
39
  passed_with_warnings: () => "Sua solução funcionou, mas há coisas para melhorar",
37
40
  pending: () => "Pendente",
38
41
  skipped: () => "Você está aprendendo muito bem e passou neste exercício",
42
+ unsaved_progress: () => "Sua solução tem alterações não salvas. Deseja sair mesmo assim?",
39
43
  }
40
44
  }
41
45
 
@@ -27,11 +27,12 @@ mumuki.renderers.results = (() => {
27
27
  */
28
28
  function classForStatus(status) {
29
29
  switch (status) {
30
- case "errored": return "broken";
31
- case "failed": return "danger";
32
- case "passed_with_warnings": return "warning";
33
- case "passed": return "success";
34
- case "pending": return "muted";
30
+ case "errored": return "broken";
31
+ case "failed": return "danger";
32
+ case "manual_evaluation_pending": return "info";
33
+ case "passed_with_warnings": return "warning";
34
+ case "passed": return "success";
35
+ case "pending": return "muted";
35
36
  }
36
37
  }
37
38
 
@@ -38,3 +38,14 @@
38
38
  max-width: 70vw;
39
39
  }
40
40
  }
41
+
42
+ .mu-read-only {
43
+ display: flex;
44
+ flex-direction: column;
45
+ justify-content: center;
46
+ align-content: center;
47
+ background-color: $mu-color-complementary;
48
+ color: white;
49
+ text-align: center;
50
+ padding: 15px;
51
+ }
@@ -1,9 +1,14 @@
1
1
  class AjaxController < ApplicationController
2
2
  before_action :authenticate!
3
+ before_action :validate_organization_enabled!, on: :create
3
4
 
4
5
  private
5
6
 
6
7
  def authenticate!
7
8
  head 403 unless current_user?
8
9
  end
10
+
11
+ def validate_organization_enabled!
12
+ Organization.current.validate_enabled!
13
+ end
9
14
  end
@@ -26,6 +26,7 @@ class ApplicationController < ActionController::Base
26
26
  before_action :authorize_if_private!
27
27
  before_action :validate_user_profile!, if: :current_user?
28
28
  before_action :validate_accepted_role_terms!, if: :current_user?
29
+ before_action :ensure_restore_progress!, if: :current_user?
29
30
 
30
31
  before_action :visit_organization!, if: :current_user?
31
32
 
@@ -164,4 +165,10 @@ class ApplicationController < ActionController::Base
164
165
  def current_access_mode
165
166
  Organization.current.access_mode(current_user)
166
167
  end
168
+
169
+ def ensure_restore_progress!
170
+ if current_access_mode.restore_indicators?(Organization.current.book)
171
+ current_user.restore_organization_progress!(Organization.current)
172
+ end
173
+ end
167
174
  end
@@ -39,17 +39,19 @@ class DiscussionsController < ApplicationController
39
39
  end
40
40
 
41
41
  def responsible
42
- if subject.can_toggle_responsible? current_user
43
- subject.toggle_responsible! current_user
44
-
45
- set_flash_responsible_confirmation!
46
- status = :ok
47
- else
48
- set_flash_responsible_alert!
49
- status = :conflict
50
- end
51
-
52
- render :partial => 'layouts/toast', status: status
42
+ subject.with_pg_lock proc {
43
+ if subject.can_toggle_responsible? current_user
44
+ subject.toggle_responsible! current_user
45
+
46
+ set_flash_responsible_confirmation!
47
+ status = :ok
48
+ else
49
+ set_flash_responsible_alert!
50
+ status = :conflict
51
+ end
52
+
53
+ render :partial => 'layouts/toast', status: status
54
+ }
53
55
  end
54
56
 
55
57
  def create
@@ -31,6 +31,6 @@ class ExamAuthorizationRequestsController < ApplicationController
31
31
 
32
32
  def verify_registration_opened!
33
33
  exam_registration = ExamRegistration.find(authorization_request_params[:exam_registration_id])
34
- raise Mumuki::Domain::GoneError if exam_registration.end_time.past?
34
+ raise Mumuki::Domain::GoneError if exam_registration.ended?
35
35
  end
36
36
  end
@@ -8,7 +8,7 @@ class ExerciseSolutionsController < AjaxController
8
8
 
9
9
  def create
10
10
  assignment = @exercise.try_submit_solution!(current_user, solution_params)
11
- render_results_json assignment, status: assignment.status
11
+ render_results_json assignment, status: assignment.visible_status
12
12
  end
13
13
 
14
14
  private
@@ -29,7 +29,10 @@ class UsersController < ApplicationController
29
29
  end
30
30
 
31
31
  def discussions
32
- @watched_discussions = current_user.watched_discussions_in_organization.scoped_query_by(discussion_filter_params).unread_first
32
+ @watched_discussions = current_user.watched_discussions_in_organization
33
+ .where(exercise: Organization.current.exercises)
34
+ .scoped_query_by(discussion_filter_params)
35
+ .unread_first
33
36
  end
34
37
 
35
38
  def activity
@@ -7,7 +7,12 @@ module EditorHelper
7
7
  end
8
8
 
9
9
  def read_only_editor(content, language, options = {})
10
- editor_options = editor_defaults(language, options, 'read-only-editor')
10
+ editor_options = editor_defaults(language, options.deep_merge(data: { readonly: true }), 'editor')
11
11
  text_area_tag :solution_content, content, editor_options
12
12
  end
13
+
14
+ def spell_checked_editor(name, options = {})
15
+ editor_options = editor_defaults('markdown', options, 'form-control mu-spell-checked-editor')
16
+ text_area_tag name, '', editor_options
17
+ end
13
18
  end
@@ -1,6 +1,6 @@
1
1
  module ExamRegistrationHelper
2
2
  def exam_registration_view
3
- if @registration.end_time.past?
3
+ if @registration.ended?
4
4
  { icon: :times_circle, class: :danger, t: :exam_registration_finished_html }
5
5
  else
6
6
  { icon: :info_circle, class: :info, t: :exam_registration_explanation_html }
@@ -10,7 +10,7 @@
10
10
  <div class="container-fluid">
11
11
  <div class="row">
12
12
  <div class="discussion-new-message-content">
13
- <%= f.editor :content, '', { id: 'discussion-new-message', class: 'form-control', placeholder: t(:message) } %>
13
+ <%= spell_checked_editor 'message[content]', { id: 'discussion-new-message', placeholder: t(:message) } %>
14
14
  </div>
15
15
  <div class="discussion-message-content d-none" id="discussion-new-message-preview"></div>
16
16
  </div>
@@ -1,5 +1,12 @@
1
- <%= t :exam_authorization_pending_explanation_html,
2
- date: l(authorization_request.exam.start_time, format: :long),
3
- end_time: l(authorization_request.exam_registration.end_time, format: :long),
4
- location: Organization.current.time_zone,
5
- edit_path: url_for(authorization_request.exam_registration) %>
1
+ <% if authorization_request.exam_registration.ended? %>
2
+ <%= t :exam_authorization_pending_confirmation_soon_html %>
3
+ <% elsif authorization_request.exam_registration.multiple_options? %>
4
+ <%= t :exam_authorization_pending_change_date_html,
5
+ end_time: l(authorization_request.exam_registration.end_time, format: :long),
6
+ location: Organization.current.time_zone,
7
+ edit_path: url_for(authorization_request.exam_registration) %>
8
+ <% else %>
9
+ <%= t :exam_authorization_pending_done_html,
10
+ end_time: l(authorization_request.exam_registration.end_time, format: :long),
11
+ location: Organization.current.time_zone %>
12
+ <% end %>
@@ -20,17 +20,19 @@
20
20
  <%= t exam_registration_view[:t], date: l(@registration.end_time, format: :long), location: Organization.current.time_zone %>
21
21
  </p>
22
22
  </div>
23
- <% unless @registration.end_time.past? %>
23
+ <% unless @registration.ended? %>
24
24
  <%= form_for @authorization_request, html: {class: 'mu-form'} do |f| %>
25
25
  <%= f.hidden_field :exam_registration_id, value: @registration.id %>
26
- <%= f.label :exam_id, t(:exam_registration_choose_exam) %>
27
- <% @registration.exams.each do |exam| %>
28
- <div class="form-check">
29
- <%= f.radio_button(:exam_id, exam.id, id: exam.id, required: true, class: 'form-check-input mu-read-only-input',
30
- checked: @authorization_request.exam_id == exam.id) %>
31
- <%= label_tag exam.id, "#{l(exam.start_time, format: :long)} #{current_time_zone_html}".html_safe, class: 'form-check-label' %>
32
- </div>
33
- <% end %>
26
+ <div class="mb-4">
27
+ <%= f.label :exam_id, t(:exam_registration_choose_exam) %>
28
+ <% @registration.exams.each do |exam| %>
29
+ <div class="form-check">
30
+ <%= f.radio_button(:exam_id, exam.id, id: exam.id, required: true, class: 'form-check-input mu-read-only-input',
31
+ checked: @authorization_request.exam_id == exam.id) %>
32
+ <%= label_tag exam.id, "#{l(exam.start_time, format: :long)} #{current_time_zone_html}".html_safe, class: 'form-check-label' %>
33
+ </div>
34
+ <% end %>
35
+ </div>
34
36
  <button class="btn btn-complementary"> <%= t :save %> </button>
35
37
  <% end %>
36
38
  <% end %>
@@ -47,22 +47,18 @@
47
47
 
48
48
  <% if should_render_read_only_exercise_tabs?(@discussion) %>
49
49
  <ul class="nav nav-tabs discussion-tabs" role="tablist">
50
-
51
- <% if @discussion.has_submission? %>
52
- <li role="presentation">
53
- <a class="editor-tab nav-link active" data-bs-target="#solution" aria-controls="solution" role="tab" data-bs-toggle="tab">
54
- <%= t :solution %>
55
- </a>
56
- </li>
57
- <li role="presentation">
58
- <a class="editor-tab nav-link" data-bs-target="#results" aria-controls="results" role="tab" data-bs-toggle="tab">
59
- <%= t :results %>
60
- </a>
61
- </li>
62
- <% end %>
63
-
64
50
  <li role="presentation">
65
- <a class="editor-tab nav-link <%= "active" unless @discussion.has_submission? %>" data-bs-target="#content" aria-controls="content" role="tab" data-bs-toggle="tab">
51
+ <a class="editor-tab nav-link active" data-bs-target="#solution" aria-controls="solution" role="tab" data-bs-toggle="tab">
52
+ <%= t :solution %>
53
+ </a>
54
+ </li>
55
+ <li role="presentation">
56
+ <a class="editor-tab nav-link" data-bs-target="#results" aria-controls="results" role="tab" data-bs-toggle="tab">
57
+ <%= t :results %>
58
+ </a>
59
+ </li>
60
+ <li role="presentation">
61
+ <a class="editor-tab nav-link" data-bs-target="#content" aria-controls="content" role="tab" data-bs-toggle="tab">
66
62
  <%= t :description %>
67
63
  </a>
68
64
  </li>
@@ -77,32 +73,28 @@
77
73
  </ul>
78
74
 
79
75
  <div class="tab-content">
80
- <% if @discussion.has_submission? %>
81
- <div role="tabpanel" class="tab-pane active" id="solution">
82
- <div class="mu-tab-body">
83
- <div role="tabpanel" class="tab-pane mu-input-panel fade show active" id="editor">
84
- <div class="mu-read-only-editor">
85
- <%= render_exercise_read_only_editor exercise, @discussion.solution %>
86
- </div>
87
- </div>
76
+ <div role="tabpanel" class="tab-pane mu-input-panel fade show active" id="solution">
77
+ <div class="mu-tab-body">
78
+ <div class="mu-read-only-editor">
79
+ <%= render_exercise_read_only_editor exercise, @discussion.solution %>
88
80
  </div>
89
81
  </div>
82
+ </div>
90
83
 
91
- <div role="tabpanel" class="tab-pane" id="results">
92
- <div class="mu-tab-body">
93
- <%= render layout: 'exercise_solutions/contextualization_results_container', locals: { contextualization: @discussion } do %>
94
- <div class="row">
95
- <div class="col-md-12 submission-results">
96
- <%= render partial: 'exercise_solutions/contextualization_results_body',
97
- locals: { contextualization: @discussion, guide_finished_by_solution: false } %>
98
- </div>
84
+ <div role="tabpanel" class="tab-pane fade" id="results">
85
+ <div class="mu-tab-body">
86
+ <%= render layout: 'exercise_solutions/contextualization_results_container', locals: { contextualization: @discussion } do %>
87
+ <div class="row">
88
+ <div class="col-md-12 submission-results">
89
+ <%= render partial: 'exercise_solutions/contextualization_results_body',
90
+ locals: { contextualization: @discussion, guide_finished_by_solution: false } %>
99
91
  </div>
100
- <% end %>
101
- </div>
92
+ </div>
93
+ <% end %>
102
94
  </div>
103
- <% end %>
95
+ </div>
104
96
 
105
- <div role="tabpanel" class="tab-pane <%= 'active' unless @discussion.has_submission? %>" id="content">
97
+ <div role="tabpanel" class="tab-pane fade" id="content">
106
98
  <div class="mu-tab-body">
107
99
  <div class="exercise-assignment">
108
100
  <%= render partial: 'exercises/exercise_assignment', locals: { exercise: exercise } %>
@@ -1,4 +1,11 @@
1
1
  <%= content_for :navbar do %>
2
+
3
+ <% if current_access_mode.read_only? %>
4
+ <div class="mu-read-only">
5
+ <small><%= t :organization_read_only_legend %></small>
6
+ </div>
7
+ <% end %>
8
+
2
9
  <%= hidden_field_tag("mu-current-exp", UserStats.exp_for(@current_user)) if in_gamified_context? %>
3
10
  <div class="<%= exercise_container_type %> px-0">
4
11
  <nav class="navbar navbar-light navbar-expand-lg mu-navbar">
@@ -275,6 +275,7 @@ en:
275
275
  only_landscape_support: Please, rotate your tablet or cellphone to continue practicing
276
276
  opened: Open
277
277
  opened_count: '%{count} opened'
278
+ organization_read_only_legend: 'You are in reading mode. You will only be able to access the exercises that you did previously and you will not be able to send new solutions'
278
279
  other: Other
279
280
  out_of_attempts: You've run out of attempts. You should proceed to the next exercise!
280
281
  output: Output
@@ -116,13 +116,15 @@ es-CL:
116
116
  internal_server_error: ¡Ups! Algo no anduvo bien
117
117
  not_found: ¡Ups! La página no existe
118
118
  errored: ¡Ups! Tu solución no se puede ejecutar
119
- exam_authorization_pending_explanation_html: Tienes tiempo hasta el <strong>%{end_time}</strong> (hora de %{location}) para inscribirte. Pasada esa fecha, tu solicitud será evaluada y recibirás una confirmación. <br> ¡No olvides revisar las notificaciones!
119
+ exam_authorization_pending_confirmation_soon_html: El período de inscripción a este examen ha finalizado. Tu solicitud será evaluada pronto y recibirás una confirmación. <br> ¡No olvides revisar las notificaciones!
120
+ exam_authorization_pending_change_date_html: Tienes tiempo hasta el <strong>%{end_time}</strong> (hora de %{location}) para cambiar el turno haciendo click <strong><a href="%{edit_path}">aquí</a></strong>. Pasada esa fecha, tu solicitud será evaluada y recibirás una confirmación. <br> ¡No olvides revisar las notificaciones!
121
+ exam_authorization_pending_done_html: Ya te inscribiste a este examen. Luego del <strong>%{end_time}</strong> (hora de %{location}) se evaluará tu solicitud y recibirás una confirmación. <br> ¡No olvides de revisar las notificaciones!
120
122
  exam_authorization_request_approved_html: Tu solicitud fue aprobada, no olvides conectarte el <strong>%{date}</strong> (hora de %{location}). ¡Buena suerte!
121
- exam_authorization_request_created: ¡Tu inscripción al exámen se registró exitosamente!
123
+ exam_authorization_request_created: ¡Tu inscripción al examen se registró exitosamente!
122
124
  exam_authorization_request_rejected: Tu solicitud fue rechazada porque no cumpliste con los requisitos para rendir el examen.
123
- exam_authorization_request_saved: ¡Tu inscripción al exámen se modificó exitosamente!
125
+ exam_authorization_request_saved: ¡Tu inscripción al examen se modificó exitosamente!
124
126
  exam_authorization_request_updated: Hay novedades sobre tu inscripción a %{description}
125
- exam_registration_choose_exam: Seleccioná día y horario en el que te gustaría rendir el exámen
127
+ exam_registration_choose_exam: Seleccioná día y horario en el que te gustaría rendir el examen
126
128
  exam_registration_explanation_html: Tienes tiempo hasta el <strong>%{date}</strong> (hora de %{location}) para inscribirte. Pasada esa fecha, tu solicitud será evaluada y recibirás una confirmación. <br> ¡No olvides revisar las notificaciones!
127
129
  exam_registration_finished_html: La inscripción a este examen finalizó el <strong>%{date}</strong> (hora de %{location})
128
130
  exam_registration_open: ¡Ya puedes inscribirte a %{description}!
@@ -284,6 +286,7 @@ es-CL:
284
286
  one: '1 abierta'
285
287
  other: '%{count} abiertas'
286
288
  organizations: Organizaciones
289
+ organization_read_only_legend: 'Estás en modo lectura. Solo podrás acceder a los ejercicios que realizaste previamente y no podrás enviar nuevas soluciones'
287
290
  other: Otro
288
291
  out_of_attempts: Te quedaste sin intentos. ¡Sigue al próximo ejercicio!
289
292
  output: Salida
@@ -123,13 +123,15 @@ es:
123
123
  internal_server_error: ¡Ups! Algo no anduvo bien
124
124
  not_found: ¡Ups! La página no existe
125
125
  errored: ¡Ups! Tu solución no se puede ejecutar
126
- exam_authorization_pending_explanation_html: Tenés tiempo hasta el <strong>%{end_time}</strong> (hora de %{location}) para cambiar el turno haciendo click <strong><a href="%{edit_path}">acá</a></strong>. Pasada esa fecha, tu solicitud será evaluada y recibirás una confirmación. <br> ¡No te olvides de revisar las notificaciones!
126
+ exam_authorization_pending_confirmation_soon_html: El período de inscripción a este examen ha finalizado. Tu solicitud será evaluada pronto y recibirás una confirmación. <br> ¡No te olvides de revisar las notificaciones!
127
+ exam_authorization_pending_change_date_html: Tenés tiempo hasta el <strong>%{end_time}</strong> (hora de %{location}) para cambiar el turno haciendo click <strong><a href="%{edit_path}">acá</a></strong>. Pasada esa fecha, tu solicitud será evaluada y recibirás una confirmación. <br> ¡No te olvides de revisar las notificaciones!
128
+ exam_authorization_pending_done_html: Ya te inscribiste a este examen. Luego del <strong>%{end_time}</strong> (hora de %{location}) se evaluará tu solicitud y recibirás una confirmación. <br> ¡No te olvides de revisar las notificaciones!
127
129
  exam_authorization_request_approved_html: Tu solicitud fue aprobada, no olvides conectarte el <strong>%{date}</strong> (hora de %{location}). ¡Buena suerte!
128
- exam_authorization_request_created: ¡Tu inscripción al exámen se registró exitosamente!
130
+ exam_authorization_request_created: ¡Tu inscripción al examen se registró exitosamente!
129
131
  exam_authorization_request_rejected: Tu solicitud fue rechazada porque no cumpliste con los requisitos para rendir el examen.
130
- exam_authorization_request_saved: ¡Tu inscripción al exámen se modificó exitosamente!
132
+ exam_authorization_request_saved: ¡Tu inscripción al examen se modificó exitosamente!
131
133
  exam_authorization_request_updated: Hay novedades sobre tu inscripción a %{description}
132
- exam_registration_choose_exam: Seleccioná día y horario en el que te gustaría rendir el exámen
134
+ exam_registration_choose_exam: Seleccioná día y horario en el que te gustaría rendir el examen
133
135
  exam_registration_explanation_html: Tenés tiempo hasta el <strong>%{date}</strong> (hora de %{location}) para inscribirte. Pasada esa fecha, tu solicitud será evaluada y recibirás una confirmación. <br> ¡No te olvides de revisar las notificaciones!
134
136
  exam_registration_finished_html: La inscripción a este examen finalizó el <strong>%{date}</strong> (hora de %{location})
135
137
  exam_registration_open: ¡Ya podés inscribirte a %{description}!
@@ -293,6 +295,7 @@ es:
293
295
  one: '1 abierta'
294
296
  other: '%{count} abiertas'
295
297
  organizations: Organizaciones
298
+ organization_read_only_legend: 'Estás en modo lectura. Solo podrás acceder a los ejercicios que realizaste previamente y no podrás enviar nuevas soluciones'
296
299
  other: Otro
297
300
  out_of_attempts: Te quedaste sin intentos. ¡Seguí al proximo ejercicio!
298
301
  output: Salida
@@ -284,6 +284,7 @@ pt:
284
284
  opened: Aberto
285
285
  opened_count: '%{count} aberto'
286
286
  organizations: Organizações
287
+ organization_read_only_legend: 'Você está no modo de leitura. Você só poderá acessar os exercícios que fez anteriormente e não poderá enviar novas soluções'
287
288
  other: Outro
288
289
  out_of_attempts: Você ficou sem tentativas. Você deve prosseguir para o próximo exercício!
289
290
  output: Sair
@@ -1,5 +1,5 @@
1
1
  module Mumuki
2
2
  module Laboratory
3
- VERSION = '9.13.1'
3
+ VERSION = '9.15.0'
4
4
  end
5
5
  end
@@ -257,6 +257,19 @@ feature 'Read Only Flow' do
257
257
  visit "/exercises/#{exercise112.id}/discussions/new"
258
258
  expect(page).to have_text('You are not allowed to see this content')
259
259
  end
260
+ scenario 'reattach book' do
261
+ expect(book.has_progress_for?(user, organization)).to eq true
262
+ visit "/"
263
+ expect(page).to have_text('Chapters')
264
+ organization.update! book: create(:book_with_full_tree)
265
+ organization.reload
266
+ visit "/"
267
+ expect(page).not_to have_text('Chapters')
268
+ organization.update! book: book
269
+ organization.reload
270
+ visit "/"
271
+ expect(page).to have_text('Chapters')
272
+ end
260
273
  end
261
274
 
262
275
  context 'and user is outsider of organization' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mumuki-laboratory
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.13.1
4
+ version: 9.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Franco Bulgarelli
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-19 00:00:00.000000000 Z
11
+ date: 2021-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 9.13.0
33
+ version: 9.15.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 9.13.0
40
+ version: 9.15.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: mumukit-bridge
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -980,135 +980,135 @@ signing_key:
980
980
  specification_version: 4
981
981
  summary: Code assement web application for the Mumuki Platform.
982
982
  test_files:
983
- - spec/capybara_helper.rb
983
+ - spec/evaluation_helper.rb
984
984
  - spec/api_helper.rb
985
- - spec/features/lessons_flow_spec.rb
986
- - spec/features/login_flow_spec.rb
987
- - spec/features/exercise_flow_spec.rb
988
- - spec/features/immersive_redirection_spec.rb
989
- - spec/features/disabled_organization_flow_spec.rb
990
- - spec/features/discussion_flow_spec.rb
991
- - spec/features/menu_bar_spec.rb
992
- - spec/features/invitations_flow_spec.rb
985
+ - spec/features/exams_flow_spec.rb
993
986
  - spec/features/standard_flow_spec.rb
994
- - spec/features/not_found_public_flow_spec.rb
987
+ - spec/features/guides_flow_spec.rb
988
+ - spec/features/dynamic_exam_spec.rb
989
+ - spec/features/disabled_organization_flow_spec.rb
990
+ - spec/features/user_activity_flow_spec.rb
995
991
  - spec/features/home_public_flow_spec.rb
996
- - spec/features/chapters_flow_spec.rb
992
+ - spec/features/read_only_flow_spec.rb
993
+ - spec/features/home_private_flow_spec.rb
994
+ - spec/features/immersive_redirection_spec.rb
995
+ - spec/features/progressive_tips_spec.rb
996
+ - spec/features/login_flow_spec.rb
997
+ - spec/features/exercise_flow_spec.rb
997
998
  - spec/features/disable_user_flow_spec.rb
998
- - spec/features/links_flow_spec.rb
999
- - spec/features/certificate_programs_flow_spec.rb
1000
- - spec/features/exams_flow_spec.rb
1001
- - spec/features/user_activity_flow_spec.rb
1002
- - spec/features/guide_reset_spec.rb
1003
- - spec/features/dynamic_exam_spec.rb
999
+ - spec/features/menu_bar_spec.rb
1004
1000
  - spec/features/not_found_private_flow_spec.rb
1005
- - spec/features/guides_flow_spec.rb
1006
- - spec/features/profile_flow_spec.rb
1001
+ - spec/features/guide_reset_spec.rb
1007
1002
  - spec/features/notifications_flow_spec.rb
1008
- - spec/features/terms_flow_spec.rb
1009
- - spec/features/home_private_flow_spec.rb
1010
- - spec/features/read_only_flow_spec.rb
1011
- - spec/features/runner_assets_spec.rb
1012
- - spec/features/topic_flow_spec.rb
1003
+ - spec/features/lessons_flow_spec.rb
1004
+ - spec/features/chapters_flow_spec.rb
1005
+ - spec/features/profile_flow_spec.rb
1013
1006
  - spec/features/complements_flow_spec.rb
1014
- - spec/features/progressive_tips_spec.rb
1015
- - spec/helpers/breadcrumbs_helper_spec.rb
1016
- - spec/helpers/certificate_helper_spec.rb
1017
- - spec/helpers/avatar_helper_spec.rb
1018
- - spec/helpers/with_navigation_spec.rb
1019
- - spec/helpers/authors_helper_spec.rb
1020
- - spec/helpers/with_choices_spec.rb
1021
- - spec/helpers/icons_helper_spec.rb
1022
- - spec/helpers/email_helper_spec.rb
1023
- - spec/helpers/exercise_input_helper_spec.rb
1024
- - spec/helpers/user_activity_helper_spec.rb
1025
- - spec/helpers/test_results_rendering_spec.rb
1026
- - spec/helpers/application_helper_spec.rb
1027
- - spec/helpers/page_title_helper_spec.rb
1028
- - spec/evaluation_helper.rb
1029
- - spec/dummy/Rakefile
1030
- - spec/dummy/package.json
1031
- - spec/dummy/bin/setup
1032
- - spec/dummy/bin/bundle
1033
- - spec/dummy/bin/yarn
1034
- - spec/dummy/bin/rake
1035
- - spec/dummy/bin/rails
1036
- - spec/dummy/bin/update
1037
- - spec/dummy/public/error/timeout_2.svg
1038
- - spec/dummy/public/error/timeout_3.svg
1039
- - spec/dummy/public/error/timeout_1.svg
1040
- - spec/dummy/public/error/500.svg
1041
- - spec/dummy/public/error/403.svg
1042
- - spec/dummy/public/error/401.svg
1043
- - spec/dummy/public/error/410.svg
1044
- - spec/dummy/public/error/404.svg
1045
- - spec/dummy/public/character/magnifying_glass/loop.svg
1046
- - spec/dummy/public/character/magnifying_glass/apparition.svg
1047
- - spec/dummy/public/character/kibi/success_l.svg
1048
- - spec/dummy/public/character/kibi/failure.svg
1049
- - spec/dummy/public/character/kibi/jump.svg
1050
- - spec/dummy/public/character/kibi/passed_with_warnings.svg
1051
- - spec/dummy/public/character/kibi/success2_l.svg
1052
- - spec/dummy/public/character/kibi/context.svg
1053
- - spec/dummy/public/character/animations.json
1054
- - spec/dummy/public/medal/outline.svg
1055
- - spec/dummy/db/schema.rb
1056
- - spec/dummy/db/seeds.rb
1057
- - spec/dummy/config.ru
1058
- - spec/dummy/config/secrets.yml
1059
- - spec/dummy/config/puma.rb
1060
- - spec/dummy/config/boot.rb
1061
- - spec/dummy/config/locales/en.yml
1062
- - spec/dummy/config/environment.rb
1063
- - spec/dummy/config/initializers/assets.rb
1064
- - spec/dummy/config/initializers/wrap_parameters.rb
1065
- - spec/dummy/config/initializers/cookies_serializer.rb
1066
- - spec/dummy/config/initializers/filter_parameter_logging.rb
1067
- - spec/dummy/config/database.yml
1068
- - spec/dummy/config/routes.rb
1069
- - spec/dummy/config/spring.rb
1070
- - spec/dummy/config/rabbit.yml
1071
- - spec/dummy/config/environments/development.rb
1072
- - spec/dummy/config/environments/test.rb
1073
- - spec/dummy/config/application.rb
1074
- - spec/dummy/config/cable.yml
1075
- - spec/teaspoon_env.rb
1076
- - spec/controllers/exercise_solutions_controller_spec.rb
1077
- - spec/controllers/exam_authorization_requests_controller_spec.rb
1078
- - spec/controllers/discussions_controller_spec.rb
1079
- - spec/controllers/guide_progress_controller_spec.rb
1080
- - spec/controllers/users_controller_spec.rb
1081
- - spec/controllers/courses_api_controller_spec.rb
1082
- - spec/controllers/chapters_controller_spec.rb
1083
- - spec/controllers/users_api_controller_spec.rb
1084
- - spec/controllers/exam_registrations_controller_spec.rb
1085
- - spec/controllers/students_api_controller_spec.rb
1086
- - spec/controllers/confirmations_controller_spec.rb
1087
- - spec/controllers/api_clients_controller.rb
1088
- - spec/controllers/certificates_controller_spec.rb
1089
- - spec/controllers/invitations_controller_spec.rb
1090
- - spec/controllers/discussions_messages_controller_spec.rb
1091
- - spec/controllers/messages_controller_spec.rb
1092
- - spec/controllers/organizations_api_controller_spec.rb
1093
- - spec/javascripts/elipsis-spec.js
1094
- - spec/javascripts/bridge-spec.js
1007
+ - spec/features/not_found_public_flow_spec.rb
1008
+ - spec/features/certificate_programs_flow_spec.rb
1009
+ - spec/features/topic_flow_spec.rb
1010
+ - spec/features/links_flow_spec.rb
1011
+ - spec/features/runner_assets_spec.rb
1012
+ - spec/features/invitations_flow_spec.rb
1013
+ - spec/features/discussion_flow_spec.rb
1014
+ - spec/features/terms_flow_spec.rb
1015
+ - spec/capybara_helper.rb
1016
+ - spec/javascripts/gamification-spec.js
1017
+ - spec/javascripts/upload-spec.js
1018
+ - spec/javascripts/timer-spec.js
1095
1019
  - spec/javascripts/timeout-spec.js
1020
+ - spec/javascripts/elipsis-spec.js
1021
+ - spec/javascripts/editors-spec.js
1022
+ - spec/javascripts/speech-bubble-renderer-spec.js
1096
1023
  - spec/javascripts/results-renderers-spec.js
1024
+ - spec/javascripts/sync-mode-spec.js
1025
+ - spec/javascripts/i18n-spec.js
1097
1026
  - spec/javascripts/submissions-store-spec.js
1098
1027
  - spec/javascripts/kids-button-spec.js
1099
- - spec/javascripts/speech-bubble-renderer-spec.js
1100
- - spec/javascripts/sync-mode-spec.js
1028
+ - spec/javascripts/exercise-spec.js
1029
+ - spec/javascripts/global-spec.js
1030
+ - spec/javascripts/bridge-spec.js
1101
1031
  - spec/javascripts/events-spec.js
1102
- - spec/javascripts/editors-spec.js
1103
1032
  - spec/javascripts/spec-helper.js
1104
- - spec/javascripts/gamification-spec.js
1105
- - spec/javascripts/i18n-spec.js
1106
- - spec/javascripts/upload-spec.js
1107
- - spec/javascripts/timer-spec.js
1108
- - spec/javascripts/global-spec.js
1109
- - spec/javascripts/exercise-spec.js
1110
1033
  - spec/javascripts/csrf-token-spec.js
1111
1034
  - spec/login_helper.rb
1112
1035
  - spec/spec_helper.rb
1113
- - spec/mailers/previews/user_mailer_preview.rb
1036
+ - spec/controllers/exam_authorization_requests_controller_spec.rb
1037
+ - spec/controllers/api_clients_controller.rb
1038
+ - spec/controllers/exercise_solutions_controller_spec.rb
1039
+ - spec/controllers/discussions_controller_spec.rb
1040
+ - spec/controllers/exam_registrations_controller_spec.rb
1041
+ - spec/controllers/organizations_api_controller_spec.rb
1042
+ - spec/controllers/certificates_controller_spec.rb
1043
+ - spec/controllers/discussions_messages_controller_spec.rb
1044
+ - spec/controllers/users_controller_spec.rb
1045
+ - spec/controllers/invitations_controller_spec.rb
1046
+ - spec/controllers/guide_progress_controller_spec.rb
1047
+ - spec/controllers/users_api_controller_spec.rb
1048
+ - spec/controllers/students_api_controller_spec.rb
1049
+ - spec/controllers/confirmations_controller_spec.rb
1050
+ - spec/controllers/messages_controller_spec.rb
1051
+ - spec/controllers/chapters_controller_spec.rb
1052
+ - spec/controllers/courses_api_controller_spec.rb
1114
1053
  - spec/mailers/user_mailer_spec.rb
1054
+ - spec/mailers/previews/user_mailer_preview.rb
1055
+ - spec/teaspoon_env.rb
1056
+ - spec/dummy/bin/update
1057
+ - spec/dummy/bin/yarn
1058
+ - spec/dummy/bin/rake
1059
+ - spec/dummy/bin/bundle
1060
+ - spec/dummy/bin/setup
1061
+ - spec/dummy/bin/rails
1062
+ - spec/dummy/config/cable.yml
1063
+ - spec/dummy/config/boot.rb
1064
+ - spec/dummy/config/secrets.yml
1065
+ - spec/dummy/config/routes.rb
1066
+ - spec/dummy/config/environments/test.rb
1067
+ - spec/dummy/config/environments/development.rb
1068
+ - spec/dummy/config/locales/en.yml
1069
+ - spec/dummy/config/initializers/cookies_serializer.rb
1070
+ - spec/dummy/config/initializers/wrap_parameters.rb
1071
+ - spec/dummy/config/initializers/assets.rb
1072
+ - spec/dummy/config/initializers/filter_parameter_logging.rb
1073
+ - spec/dummy/config/application.rb
1074
+ - spec/dummy/config/spring.rb
1075
+ - spec/dummy/config/database.yml
1076
+ - spec/dummy/config/environment.rb
1077
+ - spec/dummy/config/rabbit.yml
1078
+ - spec/dummy/config/puma.rb
1079
+ - spec/dummy/Rakefile
1080
+ - spec/dummy/db/schema.rb
1081
+ - spec/dummy/db/seeds.rb
1082
+ - spec/dummy/public/character/kibi/success_l.svg
1083
+ - spec/dummy/public/character/kibi/failure.svg
1084
+ - spec/dummy/public/character/kibi/context.svg
1085
+ - spec/dummy/public/character/kibi/jump.svg
1086
+ - spec/dummy/public/character/kibi/success2_l.svg
1087
+ - spec/dummy/public/character/kibi/passed_with_warnings.svg
1088
+ - spec/dummy/public/character/animations.json
1089
+ - spec/dummy/public/character/magnifying_glass/loop.svg
1090
+ - spec/dummy/public/character/magnifying_glass/apparition.svg
1091
+ - spec/dummy/public/medal/outline.svg
1092
+ - spec/dummy/public/error/timeout_2.svg
1093
+ - spec/dummy/public/error/410.svg
1094
+ - spec/dummy/public/error/timeout_3.svg
1095
+ - spec/dummy/public/error/500.svg
1096
+ - spec/dummy/public/error/403.svg
1097
+ - spec/dummy/public/error/timeout_1.svg
1098
+ - spec/dummy/public/error/404.svg
1099
+ - spec/dummy/public/error/401.svg
1100
+ - spec/dummy/config.ru
1101
+ - spec/dummy/package.json
1102
+ - spec/helpers/with_choices_spec.rb
1103
+ - spec/helpers/email_helper_spec.rb
1104
+ - spec/helpers/test_results_rendering_spec.rb
1105
+ - spec/helpers/authors_helper_spec.rb
1106
+ - spec/helpers/with_navigation_spec.rb
1107
+ - spec/helpers/certificate_helper_spec.rb
1108
+ - spec/helpers/page_title_helper_spec.rb
1109
+ - spec/helpers/user_activity_helper_spec.rb
1110
+ - spec/helpers/application_helper_spec.rb
1111
+ - spec/helpers/breadcrumbs_helper_spec.rb
1112
+ - spec/helpers/icons_helper_spec.rb
1113
+ - spec/helpers/exercise_input_helper_spec.rb
1114
+ - spec/helpers/avatar_helper_spec.rb