mumuki-laboratory 9.14.1 → 9.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/mumuki_laboratory/application/codemirror-builder.js +6 -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/csrf-token.js +5 -5
  6. data/app/assets/javascripts/mumuki_laboratory/application/discussions.js +9 -13
  7. data/app/assets/javascripts/mumuki_laboratory/application/exercise.js +35 -0
  8. data/app/assets/javascripts/mumuki_laboratory/application/i18n.js +4 -0
  9. data/app/controllers/ajax_controller.rb +1 -1
  10. data/app/controllers/application_controller.rb +2 -2
  11. data/app/controllers/exam_authorization_requests_controller.rb +15 -14
  12. data/app/controllers/exam_registrations_controller.rb +1 -1
  13. data/app/helpers/discussions_helper.rb +4 -1
  14. data/app/helpers/editor_helper.rb +2 -2
  15. data/app/helpers/exams_helper.rb +33 -0
  16. data/app/helpers/exercise_input_helper.rb +0 -4
  17. data/app/helpers/menu_bar_helper.rb +5 -4
  18. data/app/helpers/notifications_helper.rb +1 -1
  19. data/app/helpers/time_helper.rb +12 -0
  20. data/app/helpers/time_zone_helper.rb +5 -1
  21. data/app/helpers/user_discussions_helper.rb +7 -7
  22. data/app/views/book/show.html.erb +4 -2
  23. data/app/views/discussions/_actions.html.erb +1 -0
  24. data/app/views/discussions/_basic_actions.html.erb +20 -0
  25. data/app/views/discussions/_description_message.html.erb +1 -1
  26. data/app/views/discussions/_message.html.erb +6 -2
  27. data/app/views/discussions/new.html.erb +7 -1
  28. data/app/views/discussions/show.html.erb +1 -18
  29. data/app/views/exam_registrations/show.html.erb +1 -1
  30. data/app/views/exercises/_read_only.html.erb +56 -70
  31. data/app/views/exercises/show.html.erb +1 -0
  32. data/app/views/layouts/_discussions_list.html.erb +1 -1
  33. data/app/views/layouts/_messages.html.erb +1 -1
  34. data/app/views/users/_user_form.html.erb +1 -1
  35. data/app/views/users/messages.html.erb +1 -1
  36. data/app/views/users/notifications.html.erb +1 -1
  37. data/lib/mumuki/laboratory/locales/en.yml +6 -0
  38. data/lib/mumuki/laboratory/locales/es-CL.yml +6 -0
  39. data/lib/mumuki/laboratory/locales/es.yml +6 -0
  40. data/lib/mumuki/laboratory/locales/pt.yml +6 -0
  41. data/lib/mumuki/laboratory/version.rb +1 -1
  42. data/spec/controllers/exam_authorization_requests_controller_spec.rb +16 -2
  43. data/spec/dummy/db/schema.rb +2 -1
  44. data/spec/features/not_found_private_flow_spec.rb +1 -1
  45. metadata +113 -109
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc6cd361b66c4bf555f6c76814b671fb3dd477554858bf597a069524bad986de
4
- data.tar.gz: 456af32a6deea453adc9b107351cb7fdf08f7f2139127850bc7b169813f002b7
3
+ metadata.gz: '0617268be6df9b74ff993247e4876c84492970ce49b6967db2aabb9ede077b5b'
4
+ data.tar.gz: 7c8387588bafd36583f3304f081401d91824fe8db75f023edefd4518d3c4e3f9
5
5
  SHA512:
6
- metadata.gz: f96e1aa34781e87da4108b64c4bec13ef5dd071755250eb9ce3a1cd484808f2f9939df6d95fd87c2cd401990cc39df6462569ba65f5dddb78c2888efa9e89126
7
- data.tar.gz: 9c4a276ad0b94d856d26cfc8761167de630ee6cb75abedaadec6802b1f6a01c4cabb3c9270069e1c6d6a5a4bc4fe39545f0bcb834b8b1beefbe940f71dfcd56c
6
+ metadata.gz: 9aa1867aaa19195882d1eab24537353237354417dff78f134fce70790475d005b7e42fc7212a5420e41a6aa2d051ab6e107e8ea824363782406ffc98a435dc28
7
+ data.tar.gz: 05127705df408c4f031c4dd9c9b03a8685034fcc68b97ddd27d70aaaa87a85598c5dafebb7000fede11b3c8f09b9324b711a07d4362ca8518538d2912d118e6c
@@ -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
@@ -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
 
@@ -1,12 +1,12 @@
1
1
  mumuki.CsrfToken = (() => {
2
2
  class CsrfToken {
3
- constructor() {
4
- this.value = $('meta[name="csrf-token"]').attr('content');
3
+ get token() {
4
+ return $('meta[name="csrf-token"]').attr('content');
5
5
  }
6
+
6
7
  newRequest(data) {
7
- var self = this;
8
- data.beforeSend = function (xhr) {
9
- xhr.setRequestHeader('X-CSRF-Token', self.value);
8
+ data.beforeSend = (xhr) => {
9
+ xhr.setRequestHeader('X-CSRF-Token', this.token);
10
10
  };
11
11
  return data;
12
12
  }
@@ -19,25 +19,18 @@ mumuki.load(() => {
19
19
  .build();
20
20
  }
21
21
 
22
- function createReadOnlyEditors() {
23
- return $(".read-only-editor").map(function (index, textarea) {
24
- var $textarea = $("#solution_content");
25
-
26
- return new mumuki.editor.CodeMirrorBuilder(textarea)
27
- .setupReadOnlyEditor()
28
- .setupMinLines($textarea.data('lines'))
29
- .setupLanguage()
30
- .build();
31
- });
32
- }
33
-
34
- createReadOnlyEditors();
35
22
  let editor = createNewMessageEditor();
36
23
 
37
24
  var Forum = {
38
25
  toggleButton: function (elements) {
39
26
  elements.toggleClass('d-none');
40
27
  },
28
+ disableButton: function (elements) {
29
+ elements.attr('disabled', true);
30
+ },
31
+ reenableButton: function (elements) {
32
+ elements.attr('disabled', false);
33
+ },
41
34
  token: new mumuki.CsrfToken(),
42
35
  tokenRequest: function (data) {
43
36
  return $.ajax(Forum.token.newRequest(data));
@@ -56,6 +49,7 @@ mumuki.load(() => {
56
49
  Forum.discussionPostAndToggle(url, $upvoteButtons);
57
50
  },
58
51
  discussionResponsible: function (url) {
52
+ Forum.disableButton($responsibleButton);
59
53
  Forum.discussionPostToggleAndRenderToast(url, $responsibleButton);
60
54
  $('.responsible-moderator-badge').toggleClass('d-none');
61
55
  },
@@ -66,6 +60,7 @@ mumuki.load(() => {
66
60
  Forum.discussionPost(url)
67
61
  .done(function (response) {
68
62
  Forum.toggleButton(elem);
63
+ Forum.reenableButton(elem);
69
64
  mumuki.toast.addToast(response);
70
65
  })
71
66
  .fail(function (response) {
@@ -75,6 +70,7 @@ mumuki.load(() => {
75
70
  discussionMessageToggleApprove: function (url, elem) {
76
71
  Forum.discussionPost(url).done(function () {
77
72
  elem.toggleClass("selected");
73
+ elem.attr('data-bs-original-title', '');
78
74
  });
79
75
  },
80
76
  discussionMessageToggleNotActuallyAQuestion: function (url, elem) {
@@ -1,3 +1,31 @@
1
+ (() => {
2
+ function shouldSkipChangesCheck() {
3
+ return mumuki.isKidsExercise() || mumuki.exercise.isReadOnly;
4
+ }
5
+
6
+ function solutionChangedSinceLastSubmission() {
7
+ return mumuki.exercise.id &&
8
+ mumuki.SubmissionsStore.getLastSubmissionAndResult(mumuki.exercise.id) &&
9
+ !mumuki.SubmissionsStore.getSubmissionResultFor(mumuki.exercise.id, mumuki.editors.getSubmission());
10
+ }
11
+
12
+ function shouldWarnOfChanges() {
13
+ return !shouldSkipChangesCheck() && solutionChangedSinceLastSubmission();
14
+ }
15
+
16
+ window.addEventListener("beforeunload", (event) => {
17
+ if (shouldWarnOfChanges()) {
18
+ event.returnValue = 'unsaved_progress';
19
+ } else {
20
+ delete event['returnValue'];
21
+ }
22
+ });
23
+
24
+ window.addEventListener("turbolinks:before-visit", (event) => {
25
+ if (shouldWarnOfChanges() && !confirm(mumuki.I18n.t('unsaved_progress'))) event.preventDefault();
26
+ });
27
+ })();
28
+
1
29
  /**
2
30
  * @typedef {"input_right" | "input_bottom" | "input_primary" | "input_kindergarten"} Layout
3
31
  * @typedef {{id: number, layout: Layout, settings: any}} Exercise
@@ -46,6 +74,13 @@ mumuki.exercise = {
46
74
  return this._current;
47
75
  },
48
76
 
77
+ /**
78
+ * @type {Boolean?}
79
+ */
80
+ get isReadOnly() {
81
+ return $('#mu-exercise-read-only').val() === 'true';
82
+ },
83
+
49
84
  /**
50
85
  * Set global current exercise information
51
86
  */
@@ -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
 
@@ -9,6 +9,6 @@ class AjaxController < ApplicationController
9
9
  end
10
10
 
11
11
  def validate_organization_enabled!
12
- Organization.current.validate_enabled!
12
+ Organization.current.validate_enabled! unless current_user&.teacher_here?
13
13
  end
14
14
  end
@@ -19,11 +19,11 @@ class ApplicationController < ActionController::Base
19
19
  before_action :set_time_zone!
20
20
 
21
21
  before_action :ensure_user_enabled!, if: :current_user?
22
-
23
22
  before_action :redirect_to_proper_context!, if: :immersive_context_wrong?
24
- before_action :validate_active_organization!
25
23
 
26
24
  before_action :authorize_if_private!
25
+ before_action :validate_active_organization!
26
+
27
27
  before_action :validate_user_profile!, if: :current_user?
28
28
  before_action :validate_accepted_role_terms!, if: :current_user?
29
29
  before_action :ensure_restore_progress!, if: :current_user?
@@ -1,36 +1,37 @@
1
1
  class ExamAuthorizationRequestsController < ApplicationController
2
2
 
3
- before_action :verify_registration_opened!, on: [:create, :update]
3
+ before_action :set_registration!
4
+ before_action :set_exam!
5
+ before_action :verify_registration_opened!
4
6
 
5
7
  def create
6
- authorization_request = ExamAuthorizationRequest.find_or_create_by! create_authorization_request_params do |it|
7
- it.assign_attributes authorization_request_params
8
- end
9
- current_user.read_notification! authorization_request.exam_registration
8
+ authorization_request = @registration.request_authorization! current_user, @exam
9
+ current_user.read_notification! @registration
10
10
  flash.notice = I18n.t :exam_authorization_request_created
11
11
  redirect_to root_path
12
12
  end
13
13
 
14
14
  def update
15
- ExamAuthorizationRequest.update params[:id], authorization_request_params
15
+ @registration.update_authorization_request_by_id! params[:id], @exam
16
16
  flash.notice = I18n.t :exam_authorization_request_saved
17
17
  redirect_to root_path
18
18
  end
19
19
 
20
20
  private
21
21
 
22
- def create_authorization_request_params
23
- authorization_request_params.slice :exam_registration_id, :user, :organization
22
+ def authorization_request_params
23
+ params.require(:exam_authorization_request).permit(:exam_id, :exam_registration_id)
24
24
  end
25
25
 
26
- def authorization_request_params
27
- params
28
- .require(:exam_authorization_request).permit(:exam_id, :exam_registration_id)
29
- .merge(user: current_user, organization: Organization.current)
26
+ def set_registration!
27
+ @registration = Organization.current.exam_registrations.find(authorization_request_params[:exam_registration_id])
28
+ end
29
+
30
+ def set_exam!
31
+ @exam = @registration.exams.find(authorization_request_params[:exam_id])
30
32
  end
31
33
 
32
34
  def verify_registration_opened!
33
- exam_registration = ExamRegistration.find(authorization_request_params[:exam_registration_id])
34
- raise Mumuki::Domain::GoneError if exam_registration.ended?
35
+ raise Mumuki::Domain::GoneError if @registration.ended?
35
36
  end
36
37
  end
@@ -1,6 +1,6 @@
1
1
  class ExamRegistrationsController < ApplicationController
2
2
  def show
3
- @registration = ExamRegistration.find(params[:id])
3
+ @registration = Organization.current.exam_registrations.find(params[:id])
4
4
  @authorization_request = @registration.authorization_request_for(current_user)
5
5
  end
6
6
  end
@@ -184,7 +184,10 @@ module DiscussionsHelper
184
184
  end
185
185
 
186
186
  def discussion_info(discussion)
187
- "#{t(:time_since, time: time_ago_in_words(discussion.created_at))} · #{t(:reply_count, count: discussion.visible_messages.size)}"
187
+ <<~HTML.html_safe
188
+ <span>#{friendly_time(discussion.created_at, :time_since)}</span>
189
+ <span> · #{t(:reply_count, count: discussion.visible_messages.size)}</span>
190
+ HTML
188
191
  end
189
192
 
190
193
  def discussion_filter_params_without_page
@@ -7,8 +7,8 @@ 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')
11
- text_area_tag :solution_content, content, editor_options
10
+ editor_options = editor_defaults(language, options.deep_merge(data: { readonly: true }), 'editor')
11
+ text_area_tag 'solution[content]', content, editor_options
12
12
  end
13
13
 
14
14
  def spell_checked_editor(name, options = {})
@@ -0,0 +1,33 @@
1
+ module ExamsHelper
2
+ def exam_information_for(user, exam)
3
+ %Q{
4
+ #{course_information_for(user, exam)}
5
+ #{date_information_for(exam)}
6
+ #{duration_information_for(exam)}
7
+ }.html_safe
8
+ end
9
+
10
+ private
11
+
12
+ def course_information_for(user, exam)
13
+ if user.teacher_here?
14
+ "#{fa_icon('graduation-cap', class: 'fa-fw',
15
+ text: "<strong>#{t :course}:</strong> #{exam.course.canonical_code}".html_safe)}
16
+ <br>"
17
+ end
18
+ end
19
+
20
+ def date_information_for(exam)
21
+ "#{fa_icon('calendar-alt', class: 'fa-fw',
22
+ text: "<strong>#{t :date_and_time}:</strong> #{local_time_without_time_zone(exam.start_time)} - #{local_time(exam.end_time)}".html_safe)}
23
+ <br>"
24
+ end
25
+
26
+ def duration_information_for(exam)
27
+ if exam.duration?
28
+ "#{fa_icon(:stopwatch, class: 'fa-fw',
29
+ text: "<strong>#{t :available_time}:</strong> #{t :time_in_minutes, time: exam.duration}".html_safe)}
30
+ <br>"
31
+ end
32
+ end
33
+ end
@@ -35,10 +35,6 @@ module ExerciseInputHelper
35
35
  should_render_exercise_tabs?(exercise) { exercise.has_messages_for? user }
36
36
  end
37
37
 
38
- def should_render_read_only_exercise_tabs?(discussion)
39
- should_render_exercise_tabs?(discussion.exercise) { discussion.has_submission? }
40
- end
41
-
42
38
  def should_render_message_input?(exercise, organization = Organization.current)
43
39
  exercise.is_a?(Problem) && !exercise.hidden? && organization.raise_hand_enabled?
44
40
  end
@@ -38,12 +38,13 @@ module MenuBarHelper
38
38
  li_tag menu_item('sign-out-alt', :sign_out, logout_path(origin: url_for, organization: Organization.current))
39
39
  end
40
40
 
41
- def menu_item(icon, name, url, css_class = nil, **translation_params)
42
- menu_text_item(icon, t(name, translation_params), url, css_class)
41
+ def menu_item(icon, name, url, css_class = nil, translation_params = {}, options = {})
42
+ menu_text_item(icon, t(name, translation_params), url, css_class, **options)
43
43
  end
44
44
 
45
- def menu_text_item(icon, text, url, css_class = nil)
46
- link_to fixed_fa_icon(icon, text: text), url, role: 'menuitem', tabindex: '-1', class: "dropdown-item #{css_class}"
45
+ def menu_text_item(icon, text, url, css_class = nil, **options)
46
+ link_options = { role: 'menuitem', tabindex: '-1', class: "dropdown-item #{css_class}" }.merge(options)
47
+ link_to fixed_fa_icon(icon, text: text), url, link_options
47
48
  end
48
49
 
49
50
  def any_menu_bar_links?
@@ -4,7 +4,7 @@ module NotificationsHelper
4
4
  end
5
5
 
6
6
  def notification_preview_item(icon, name, url, **translation_params)
7
- menu_item icon, name, url, 'mu-notification-preview', **translation_params
7
+ menu_item icon, name, url, 'mu-notification-preview', translation_params
8
8
  end
9
9
 
10
10
  def notification_text_preview_item(icon, text, url)
@@ -0,0 +1,12 @@
1
+ module TimeHelper
2
+ def friendly_time(time, t_key = nil)
3
+ friendly_time = time_ago_in_words time
4
+ friendly_time_t = t_key ? t(t_key, time: friendly_time) : friendly_time
5
+
6
+ <<~HTML.html_safe
7
+ <time title="#{time}">
8
+ #{friendly_time_t}
9
+ </time>
10
+ HTML
11
+ end
12
+ end
@@ -1,5 +1,9 @@
1
1
  module TimeZoneHelper
2
2
  def local_time(time, time_zone = Time.zone.name)
3
- "#{l(time.in_time_zone(time_zone), format: :long)} (#{time_zone})"
3
+ "#{local_time_without_time_zone(time, time_zone)} (#{time_zone})"
4
+ end
5
+
6
+ def local_time_without_time_zone(time, time_zone = Time.zone.name)
7
+ l(time.in_time_zone(time_zone), format: :long)
4
8
  end
5
9
  end
@@ -1,6 +1,6 @@
1
1
  module UserDiscussionsHelper
2
2
  def user_discussions_table_title(discussion, user, last_read)
3
- %Q{
3
+ <<~HTML.html_safe
4
4
  <tr></tr>
5
5
  <thead>
6
6
  <tr>
@@ -9,30 +9,30 @@ module UserDiscussionsHelper
9
9
  </td>
10
10
  </tr>
11
11
  </thead>
12
- }.html_safe
12
+ HTML
13
13
  end
14
14
 
15
15
  def user_discussions_table_header
16
- %Q{
16
+ <<~HTML.html_safe
17
17
  <tr class="fw-bold">
18
18
  <td></td>
19
19
  <td>#{t(:exercise)}</td>
20
20
  <td>#{t(:discussion_created_by)}</td>
21
21
  <td>#{t(:last_message)}</td>
22
22
  </tr>
23
- }.html_safe
23
+ HTML
24
24
  end
25
25
 
26
26
  def user_discussions_table_item(discussion, user)
27
- %Q{
27
+ <<~HTML.html_safe
28
28
  <tr>
29
29
  <td class="text-center">
30
30
  #{icon_for_read(discussion.read_by?(user))}
31
31
  </td>
32
32
  <td>#{link_to discussion.item.name, item_discussion_path(discussion)}</td>
33
33
  <td>#{discussion_user_name discussion.initiator}</td>
34
- <td>#{t(:time_since, time: time_ago_in_words(discussion.last_message_date))}</td>
34
+ <td>#{friendly_time(discussion.last_message_date, :time_since)}</td>
35
35
  </tr>
36
- }.html_safe
36
+ HTML
37
37
  end
38
38
  end
@@ -45,8 +45,10 @@
45
45
  <h2><%= t(:exams) %></h2>
46
46
  <% @exams.each_with_index do |it, index| %>
47
47
  <div class="chapter">
48
- <h3> <%= index + 1 %>. <%= link_to_path_element it, mode: :plain %></h3>
49
-
48
+ <h3><%= link_to_path_element it, mode: :plain %></h3>
49
+ <div class="fs-7">
50
+ <%= exam_information_for(current_user, it) %>
51
+ </div>
50
52
  <div class="text-box">
51
53
  <%= it.guide.description_teaser_html %>
52
54
  </div>
@@ -0,0 +1 @@
1
+ <%= render partial: 'discussions/basic_actions' %>
@@ -0,0 +1,20 @@
1
+ <% if @discussion.can_toggle_responsible?(current_user) %>
2
+ <div class="discussion-responsible me-1">
3
+ <%= btn_toggle responsible_icon, not_responsible_icon, @discussion.any_responsible?, class: 'btn-sm',
4
+ onclick: "mumuki.Forum.discussionResponsible('#{responsible_discussion_url(@discussion)}')" %>
5
+ </div>
6
+ <% end %>
7
+
8
+ <% if @discussion.subscribable? %>
9
+ <div class="discussion-subscription me-1">
10
+ <%= btn_toggle subscription_icon, unsubscription_icon, current_user.subscribed_to?(@discussion), class: 'btn-sm',
11
+ onclick: "mumuki.Forum.discussionSubscription('#{subscription_discussion_url(@discussion)}')" %>
12
+ </div>
13
+ <% end %>
14
+
15
+ <% if @discussion.solved? %>
16
+ <div class="discussion-upvote">
17
+ <%= btn_toggle upvote_icon, undo_upvote_icon, current_user.upvoted?(@discussion), class: 'btn-sm',
18
+ onclick: "mumuki.Forum.discussionUpvote('#{upvote_discussion_url(@discussion)}')" %>
19
+ </div>
20
+ <% end %>
@@ -4,7 +4,7 @@
4
4
  <div class="discussion-message-bubble-title">
5
5
  <%= linked_discussion_user_name(discussion.initiator) %>
6
6
  <span class="message-date">
7
- <%= t(:time_since, time: time_ago_in_words(discussion.created_at)) %>
7
+ <%= friendly_time(discussion.created_at, :time_since) %>
8
8
  </span>
9
9
  </div>
10
10
  </div>
@@ -7,12 +7,16 @@
7
7
  <span class="moderator-badge"><%= t(:moderation) %></span>
8
8
  <% end %>
9
9
  <span class="message-date">
10
- <%= t(:time_since, time: time_ago_in_words(message.created_at)) %>
10
+ <%= friendly_time(message.created_at, :time_since) %>
11
11
  </span>
12
12
  <span class="actions">
13
13
  <% if message.authorized?(current_user) && !message.deleted? %>
14
14
  <% if current_user&.moderator_here? %>
15
15
  <a class="discussion-message-approved <%= 'selected' if message.approved? %>"
16
+ <% if message.approved? && current_user&.forum_supervisor_here? %>
17
+ data-bs-toggle="tooltip" data-bs-placement="left"
18
+ title="<%= t :approved_by, validator: message.approved_by.name, date: local_time(message.approved_at) %>"
19
+ <% end %>
16
20
  onclick="mumuki.Forum.discussionMessageToggleApprove('<%= approve_discussion_message_url(@discussion, message) %>', $(this))">
17
21
  <%= fa_icon(:check, class: 'fa-lg') %>
18
22
  </a>
@@ -47,7 +51,7 @@
47
51
  <% if current_user&.moderator_here? %>
48
52
  <hr>
49
53
  <%= t :deleted_by, deleter: message.deleted_by.name %>
50
- <%= t(:time_since, time: time_ago_in_words(message.deleted_at)) %>.
54
+ <%= friendly_time(message.deleted_at, :time_since) %>
51
55
  <a href='<%= "#deletedMessage#{message.id}" %>' data-bs-toggle="collapse">
52
56
  <%= t :show_message %>
53
57
  </a>
@@ -22,12 +22,18 @@
22
22
  <div class="container-fluid">
23
23
  <div class="row">
24
24
  <div class="discussion-new-message-content">
25
- <%= f.editor :description, '', {id: 'discussion-new-message', class: 'form-control', placeholder: t(:discussion_description_placeholder)} %>
25
+ <%= spell_checked_editor 'discussion[description]',
26
+ { id: 'discussion-new-message', placeholder: t(:discussion_description_placeholder) } %>
26
27
  </div>
27
28
  </div>
28
29
  </div>
29
30
  </div>
30
31
  </div>
32
+ <% if faqs_enabled_here? %>
33
+ <div class="fs-7">
34
+ <%= fa_icon('exclamation-triangle', text: t(:only_forum_questions_on_forum, link: link_to_faqs).html_safe) %>
35
+ </div>
36
+ <% end %>
31
37
  <%= f.submit t(:publish_discussion), class: 'btn btn-complementary w-100 discussion-new-message-button' %>
32
38
  <% end %>
33
39
  <% end %>
@@ -15,24 +15,7 @@
15
15
  <h3 class="flex-grow-1 me-3"><%= t :messages %></h3>
16
16
  <% if current_user && @discussion.persisted? && current_access_mode.show_discussion_element? %>
17
17
  <span class="d-flex">
18
- <% if @discussion.can_toggle_responsible?(current_user) %>
19
- <div class="discussion-responsible me-1">
20
- <%= btn_toggle responsible_icon, not_responsible_icon, @discussion.any_responsible?, class: 'btn-sm',
21
- onclick: "mumuki.Forum.discussionResponsible('#{responsible_discussion_url(@discussion)}')" %>
22
- </div>
23
- <% end %>
24
- <% if @discussion.subscribable? %>
25
- <div class="discussion-subscription me-1">
26
- <%= btn_toggle subscription_icon, unsubscription_icon, current_user.subscribed_to?(@discussion), class: 'btn-sm',
27
- onclick: "mumuki.Forum.discussionSubscription('#{subscription_discussion_url(@discussion)}')" %>
28
- </div>
29
- <% end %>
30
- <% if @discussion.solved? %>
31
- <div class="discussion-upvote">
32
- <%= btn_toggle upvote_icon, undo_upvote_icon, current_user.upvoted?(@discussion), class: 'btn-sm',
33
- onclick: "mumuki.Forum.discussionUpvote('#{upvote_discussion_url(@discussion)}')" %>
34
- </div>
35
- <% end %>
18
+ <%= render partial: 'discussions/actions' %>
36
19
  </span>
37
20
  <% end %>
38
21
  </div>
@@ -25,7 +25,7 @@
25
25
  <%= f.hidden_field :exam_registration_id, value: @registration.id %>
26
26
  <div class="mb-4">
27
27
  <%= f.label :exam_id, t(:exam_registration_choose_exam) %>
28
- <% @registration.exams.each do |exam| %>
28
+ <% @registration.available_exams.each do |exam| %>
29
29
  <div class="form-check">
30
30
  <%= f.radio_button(:exam_id, exam.id, id: exam.id, required: true, class: 'form-check-input mu-read-only-input',
31
31
  checked: @authorization_request.exam_id == exam.id) %>