mumuki-laboratory 9.3.0 → 9.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/mumuki_laboratory/application/discussions.js +15 -0
  3. data/app/assets/javascripts/mumuki_laboratory/application/faqs.js +6 -6
  4. data/app/assets/javascripts/mumuki_laboratory/application/toast.js +12 -1
  5. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_discussion.scss +0 -25
  6. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_faqs.scss +4 -3
  7. data/app/controllers/discussions_controller.rb +29 -2
  8. data/app/controllers/exam_authorization_requests_controller.rb +7 -1
  9. data/app/helpers/discussions_helper.rb +26 -2
  10. data/app/views/discussions/_description_message.html.erb +1 -1
  11. data/app/views/discussions/_message.html.erb +1 -1
  12. data/app/views/discussions/_new_message.html.erb +1 -1
  13. data/app/views/discussions/new.html.erb +1 -1
  14. data/app/views/discussions/show.html.erb +6 -0
  15. data/app/views/exam_authorization_requests/_approved.html.erb +1 -1
  16. data/app/views/exam_authorization_requests/_pending.html.erb +2 -2
  17. data/app/views/exam_registrations/show.html.erb +3 -2
  18. data/app/views/exercises/_read_only.html.erb +5 -6
  19. data/app/views/faqs/index.html.erb +1 -1
  20. data/app/views/layouts/_discussions.html.erb +3 -3
  21. data/app/views/layouts/_main.html.erb +4 -10
  22. data/app/views/layouts/_toast.html.erb +7 -0
  23. data/app/views/users/activity.html.erb +8 -6
  24. data/app/views/users/exam_authorizations.html.erb +1 -1
  25. data/config/routes.rb +2 -0
  26. data/lib/mumuki/laboratory/locales/en.yml +13 -5
  27. data/lib/mumuki/laboratory/locales/es-CL.yml +12 -3
  28. data/lib/mumuki/laboratory/locales/es.yml +12 -4
  29. data/lib/mumuki/laboratory/locales/pt.yml +12 -4
  30. data/lib/mumuki/laboratory/version.rb +1 -1
  31. data/lib/tasks/exams.rake +15 -0
  32. data/spec/controllers/discussions_controller_spec.rb +42 -0
  33. data/spec/dummy/db/schema.rb +3 -3
  34. data/spec/features/discussion_flow_spec.rb +38 -14
  35. data/spec/features/user_activity_flow_spec.rb +30 -10
  36. data/spec/helpers/with_navigation_spec.rb +22 -0
  37. metadata +114 -112
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7fcf8057854fbb65bd0ddb801770c0d7a9de320735217bb50ac239d3c303651d
4
- data.tar.gz: 6466a0bfca548fce3eb105daa98b9f9790616ad0009928efd7c46719fc9526d7
3
+ metadata.gz: 37091ade7a9c0bad80f16a5276b2748692a49cc11708613036dacd1042b9cdc2
4
+ data.tar.gz: 949f97b3a7e9e920f47cb1748a66d0f6951f4d45a2021cacad43e4a1122b4a8f
5
5
  SHA512:
6
- metadata.gz: fe7ff6eb305b615a33cea3d90be11d06b58acbeb7e6e6add7049bf632055d676e6a9cc475c88649a8341ca8038fe7d8b6ce7b8616ceef1a6d3094bdffba75422
7
- data.tar.gz: d7fba0d3d8a4e3223efc8987649a482794be90958d5e6b618b4bce519e72b72b7e42cddaecf8f4f151ca864a8caa9a4b18369abe2c281c5c5d9e9cba99ca4f6a
6
+ metadata.gz: 34d25e182a43788fc032bd77c6472c497a55d08cb6d776575441aee4b5ec11aa55a3f95a7b3d1300a47e43006764494dab809f1a8114076d50cadeb0b59d1021
7
+ data.tar.gz: c9dd87e5ac8d17873aaa3e28a442b4c87225bb31b4573385e0906e852fa58b9c778ad61cf2e0c8e48ca2c90edba9844064ed260ca276ff95511c69ba4af5f44a
@@ -1,6 +1,7 @@
1
1
  mumuki.load(() => {
2
2
  var $subscriptionButtons = $('.discussion-subscription > button');
3
3
  var $upvoteButtons = $('.discussion-upvote > button');
4
+ var $responsibleButton = $('.discussion-responsible > button');
4
5
  let $messagePreviewButton = $('.discussion-new-message-preview-button.preview');
5
6
  let $messageEditButton = $('.discussion-new-message-preview-button.edit');
6
7
  let $newMessageContent = $('.discussion-new-message-content');
@@ -53,9 +54,23 @@ mumuki.load(() => {
53
54
  discussionUpvote: function (url) {
54
55
  Forum.discussionPostAndToggle(url, $upvoteButtons);
55
56
  },
57
+ discussionResponsible: function (url) {
58
+ Forum.discussionPostToggleAndRenderToast(url, $responsibleButton);
59
+ $('.responsible-moderator-badge').toggleClass('d-none');
60
+ },
56
61
  discussionPostAndToggle: function (url, elem) {
57
62
  Forum.discussionPost(url).done(Forum.toggleButton(elem));
58
63
  },
64
+ discussionPostToggleAndRenderToast: function (url, elem) {
65
+ Forum.discussionPost(url)
66
+ .done(function (response) {
67
+ Forum.toggleButton(elem);
68
+ mumuki.toast.addToast(response);
69
+ })
70
+ .fail(function (response) {
71
+ mumuki.toast.addToast(response.responseText);
72
+ });
73
+ },
59
74
  discussionMessageToggleApprove: function (url, elem) {
60
75
  Forum.discussionPost(url).done(function () {
61
76
  elem.toggleClass("selected");
@@ -25,9 +25,9 @@ mumuki.faqs = class {
25
25
  _createNavbar() {
26
26
  const $faqsNavbar = $(".mu-faqs-navbar nav ul");
27
27
  $('.mu-faqs-group').each((_index, faqGroup) => {
28
- const $navItem = this._createNavbarItem($faqsNavbar, faqGroup)
28
+ const $navItem = this._createNavbarItem($faqsNavbar, faqGroup);
29
29
  const $faqGroup = $(faqGroup);
30
- this._configureClickFor($navItem, $faqGroup, $faqsNavbar)
30
+ this._configureClickFor($navItem, $faqGroup, $faqsNavbar);
31
31
  });
32
32
  }
33
33
 
@@ -56,12 +56,12 @@ mumuki.faqs = class {
56
56
  }
57
57
 
58
58
  _createFaqsIcons() {
59
- const $faqIcon = $('<i class="mu-faqs-group-icon fa fa-plus">');
59
+ const $faqIcon = $('<i class="mu-faqs-group-icon fas fa-chevron-down">');
60
60
  $faqIcon.click(function(e){
61
61
  const $elem = $(this);
62
- $elem.toggleClass('fa-plus fa-minus');
62
+ $elem.toggleClass('fa-chevron-down fa-chevron-up');
63
63
  $elem.closest('.mu-faqs-group').toggleClass('active');
64
- })
64
+ });
65
65
  $('.mu-faqs-group').prepend($faqIcon);
66
66
  }
67
67
 
@@ -75,7 +75,7 @@ mumuki.faqs = class {
75
75
  newGroup = [];
76
76
  }
77
77
  newGroup.push(elem);
78
- previousNodeName = elem.nodeName
78
+ previousNodeName = elem.nodeName;
79
79
  });
80
80
 
81
81
  elemsGroups.push(newGroup);
@@ -1,3 +1,14 @@
1
+ mumuki.toast = {
2
+ load() {
3
+ document.querySelectorAll('.toast').forEach((toast) => new bootstrap.Toast(toast).show());
4
+ },
5
+
6
+ addToast(content) {
7
+ $('.toast-container').html(content);
8
+ mumuki.toast.load();
9
+ }
10
+ };
11
+
1
12
  mumuki.load(() => {
2
- document.querySelectorAll('.toast').forEach((toast) => new bootstrap.Toast(toast).show());
13
+ mumuki.toast.load();
3
14
  });
@@ -394,31 +394,6 @@ $moderator-badge-color: #dd9900;
394
394
  }
395
395
  }
396
396
 
397
- $statuses: (
398
- closed: ($danger white $danger),
399
- opened: (white #333333 #eaeaea),
400
- solved: ($success white $success),
401
- pending_review: ($success white $success)
402
- );
403
-
404
- @each $status, $style in $statuses {
405
- $background-color: nth($style, 1);
406
- $font-color: nth($style, 2);
407
- $border-color: nth($style, 3);
408
-
409
- .btn-discussion-#{$status} {
410
- background-color: $background-color;
411
- color: $font-color;
412
- border-color: $border-color;
413
- margin-left: 5px;
414
- &:hover {
415
- color: $font-color;
416
- border-color: darken($border-color, 3%);
417
- background-color: darken($background-color, 3%);
418
- }
419
- }
420
- }
421
-
422
397
  .discussion-requires-attention {
423
398
  margin-right: 20px;
424
399
  label {
@@ -8,7 +8,7 @@
8
8
 
9
9
  width: 100%;
10
10
 
11
- @media (min-width: 768px) {
11
+ @include media-breakpoint-up(md) {
12
12
  width: 75%;
13
13
  float: right;
14
14
  }
@@ -17,8 +17,9 @@
17
17
  padding: 32px;
18
18
  box-shadow: 0 0.375em 2.8125em 0 #d2d5d9;
19
19
  margin-bottom: 32px;
20
+ word-wrap: anywhere;
20
21
 
21
- @media (max-width: 767px) {
22
+ @include media-breakpoint-down(md) {
22
23
  &:not(.active) {
23
24
  h3, p {
24
25
  display: none
@@ -44,7 +45,7 @@
44
45
  margin-top: 10px;
45
46
  cursor: pointer;
46
47
  display: none;
47
- @media (max-width: 767px) {
48
+ @include media-breakpoint-down(md) {
48
49
  display: unset;
49
50
  }
50
51
  }
@@ -2,10 +2,11 @@ class DiscussionsController < ApplicationController
2
2
  include Mumuki::Laboratory::Controllers::Content
3
3
  include WithUserDiscussionValidation
4
4
 
5
- before_action :set_debatable, except: [:subscription]
5
+ before_action :set_debatable, except: [:subscription, :responsible]
6
6
  before_action :authenticate!, only: [:update, :create]
7
7
  before_action :discussion_filter_params, only: :index
8
8
  before_action :read_discussion, only: :show
9
+ before_action :authorize_moderator!, only: [:responsible]
9
10
 
10
11
  helper_method :discussion_filter_params
11
12
 
@@ -19,7 +20,6 @@ class DiscussionsController < ApplicationController
19
20
  end
20
21
 
21
22
  def show
22
- @discussion.update_last_moderator_access! current_user
23
23
  end
24
24
 
25
25
  def update
@@ -37,6 +37,20 @@ class DiscussionsController < ApplicationController
37
37
  head :ok
38
38
  end
39
39
 
40
+ def responsible
41
+ if subject.can_toggle_responsible? current_user
42
+ subject.toggle_responsible! current_user
43
+
44
+ set_flash_responsible_confirmation!
45
+ status = :ok
46
+ else
47
+ set_flash_responsible_alert!
48
+ status = :conflict
49
+ end
50
+
51
+ render :partial => 'layouts/toast', status: status
52
+ end
53
+
40
54
  def create
41
55
  discussion = @debatable.discuss! current_user, discussion_params
42
56
  redirect_to [@debatable, discussion]
@@ -57,6 +71,19 @@ class DiscussionsController < ApplicationController
57
71
  @debatable = Discussion.debatable_for(@debatable_class, params)
58
72
  end
59
73
 
74
+ def set_flash_responsible_confirmation!
75
+ subject.any_responsible? ?
76
+ flash.now.notice = I18n.t('moderator_take_care.you_will_confirmation') :
77
+ flash.now.notice = I18n.t('moderator_take_care.you_wont_confirmation')
78
+
79
+ end
80
+
81
+ def set_flash_responsible_alert!
82
+ subject.any_responsible? ?
83
+ flash.now.alert = I18n.t('moderator_take_care.someone_else_will') :
84
+ flash.now.alert = I18n.t('moderator_take_care.status_changed')
85
+ end
86
+
60
87
  def subject
61
88
  @discussion ||= Discussion.find_by(id: params[:id])
62
89
  end
@@ -1,6 +1,8 @@
1
1
  class ExamAuthorizationRequestsController < ApplicationController
2
2
  def create
3
- authorization_request = ExamAuthorizationRequest.create! authorization_request_params
3
+ authorization_request = ExamAuthorizationRequest.find_or_create_by! create_authorization_request_params do |it|
4
+ it.assign_attributes authorization_request_params
5
+ end
4
6
  current_user.read_notification! authorization_request.exam_registration
5
7
  flash.notice = I18n.t :exam_authorization_request_created
6
8
  redirect_to root_path
@@ -14,6 +16,10 @@ class ExamAuthorizationRequestsController < ApplicationController
14
16
 
15
17
  private
16
18
 
19
+ def create_authorization_request_params
20
+ authorization_request_params.slice :exam_registration_id, :user, :organization
21
+ end
22
+
17
23
  def authorization_request_params
18
24
  params
19
25
  .require(:exam_authorization_request).permit(:exam_id, :exam_registration_id)
@@ -83,10 +83,14 @@ module DiscussionsHelper
83
83
  def discussion_update_status_button(status)
84
84
  button_to t("to_#{status}"),
85
85
  item_discussion_path(@discussion, {status: status}),
86
- class: "btn btn-discussion-#{status}",
86
+ class: "btn btn-#{btn_type_for_discussion_statuses[status.to_sym]}",
87
87
  method: :put
88
88
  end
89
89
 
90
+ def btn_type_for_discussion_statuses
91
+ { closed: 'danger', solved: 'success', opened: 'light' }
92
+ end
93
+
90
94
  def new_discussion_link(teaser_text, link_text)
91
95
  %Q{
92
96
  <h4>
@@ -173,7 +177,7 @@ module DiscussionsHelper
173
177
  end
174
178
 
175
179
  def discussion_info(discussion)
176
- "#{t(:time_since, time: time_ago_in_words(discussion.created_at))} · #{t(:message_count, count: discussion.visible_messages.size)}"
180
+ "#{t(:time_since, time: time_ago_in_words(discussion.created_at))} · #{t(:reply_count, count: discussion.visible_messages.size)}"
177
181
  end
178
182
 
179
183
  def discussion_filter_params_without_page
@@ -188,6 +192,26 @@ module DiscussionsHelper
188
192
  user.abbreviated_name
189
193
  end
190
194
 
195
+ def linked_discussion_user_name(user)
196
+ content_tag :a, discussion_user_name(user)
197
+ end
198
+
199
+ def responsible_moderator_text_for(discussion, user)
200
+ if discussion.responsible?(user)
201
+ t('moderator_take_care.you_are')
202
+ else
203
+ t('moderator_take_care.moderator_is', moderator: discussion_user_name(@discussion.responsible_moderator_by))
204
+ end
205
+ end
206
+
207
+ def responsible_icon
208
+ fa_icon 'hand-paper', text: t('moderator_take_care.i_will')
209
+ end
210
+
211
+ def not_responsible_icon
212
+ fa_icon 'hand-rock', type: :regular, text: t('moderator_take_care.i_wont')
213
+ end
214
+
191
215
  def subscription_icon
192
216
  fa_icon :bell, text: t(:subscribe)
193
217
  end
@@ -2,7 +2,7 @@
2
2
  <div class="discussion-message-bubble">
3
3
  <div class="discussion-message-bubble-header">
4
4
  <div class="discussion-message-bubble-title">
5
- <%= discussion_user_name(discussion.initiator) %>
5
+ <%= linked_discussion_user_name(discussion.initiator) %>
6
6
  <span class="message-date">
7
7
  <%= t(:time_since, time: time_ago_in_words(discussion.created_at)) %>
8
8
  </span>
@@ -2,7 +2,7 @@
2
2
  <div class="discussion-message-bubble">
3
3
  <div class="discussion-message-bubble-header">
4
4
  <div class="discussion-message-bubble-title">
5
- <%= discussion_user_name user %>
5
+ <%= linked_discussion_user_name user %>
6
6
  <% if user.moderator_here? %>
7
7
  <span class="moderator-badge"><%= t(:moderation) %></span>
8
8
  <% end %>
@@ -3,7 +3,7 @@
3
3
  <div class="discussion-message-bubble">
4
4
  <div class="discussion-message-bubble-header">
5
5
  <div class="discussion-message-bubble-title">
6
- <%= user.name %>
6
+ <%= discussion_user_name user %>
7
7
  </div>
8
8
  </div>
9
9
  <div class="discussion-message-bubble-content">
@@ -15,7 +15,7 @@
15
15
  <div class="discussion-message-bubble" id="new-discussion-description-container">
16
16
  <div class="discussion-message-bubble-header">
17
17
  <div class="discussion-message-bubble-title">
18
- <%= @discussion.initiator.name %>
18
+ <%= discussion_user_name @discussion.initiator %>
19
19
  </div>
20
20
  </div>
21
21
  <div class="discussion-message-bubble-content">
@@ -15,6 +15,12 @@
15
15
  <h3 class="flex-grow-1 me-3"><%= t :messages %></h3>
16
16
  <% if current_user && @discussion.persisted? %>
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 %>
18
24
  <% if @discussion.subscribable? %>
19
25
  <div class="discussion-subscription me-1">
20
26
  <%= btn_toggle subscription_icon, unsubscription_icon, current_user.subscribed_to?(@discussion), class: 'btn-sm',
@@ -1 +1 @@
1
- <%= t :exam_authorization_request_approved_html, date: l(authorization_request.exam.start_time, format: :long) %>
1
+ <%= t :exam_authorization_request_approved_html, date: l(authorization_request.exam.start_time.in_time_zone(-3), format: :long) %>
@@ -1,4 +1,4 @@
1
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),
2
+ date: l(authorization_request.exam.start_time.in_time_zone(-3), format: :long),
3
+ end_time: l(authorization_request.exam_registration.end_time.in_time_zone(-3), format: :long),
4
4
  edit_path: url_for(authorization_request.exam_registration) %>
@@ -17,7 +17,7 @@
17
17
  <strong><%= fa_icon :info_circle %> <%= t :important_info %></strong>
18
18
  </h4>
19
19
  <p>
20
- <%= t :exam_registration_explanation_html, date: l(@registration.end_time, format: :long) %>
20
+ <%= t :exam_registration_explanation_html, date: l(@registration.end_time.in_time_zone(-3), format: :long) %>
21
21
  </p>
22
22
  </div>
23
23
  <%= form_for @authorization_request, html: {class: 'mu-form'} do |f| %>
@@ -27,10 +27,11 @@
27
27
  <div class="form-check">
28
28
  <%= f.radio_button(:exam_id, exam.id, id: exam.id, required: true, class: 'form-check-input mu-read-only-input',
29
29
  checked: @authorization_request.exam_id == exam.id) %>
30
- <%= label_tag exam.id, l(exam.start_time, format: :long), class: 'form-check-label' %>
30
+ <%= label_tag exam.id, l(exam.start_time.in_time_zone(-3), format: :long), class: 'form-check-label' %>
31
31
  </div>
32
32
  <% end %>
33
33
  <button class="btn btn-complementary"> <%= t :save %> </button>
34
34
  <% end %>
35
35
  </div>
36
36
  </div>
37
+
@@ -26,16 +26,15 @@
26
26
  <%= label_for_contextualization(@discussion, class: 'd-none d-sm-inline') %> ·
27
27
  <span class="discussion-info">
28
28
  <span class="discussion-initiator-name">
29
- <%= @discussion.initiator.name %>
29
+ <%= discussion_user_name @discussion.initiator %>
30
30
  </span>
31
- <span class="d-none d-sm-inline"><%= discussion_info(@discussion) unless @discussion.new_record? %></span>
31
+ <span><%= discussion_info(@discussion) unless @discussion.new_record? %></span>
32
32
  </span>
33
33
  </div>
34
- <% if @discussion.last_moderator_access_visible_for?(current_user) %>
34
+ <% if @discussion.current_responsible_visible_for?(current_user) %>
35
35
  <h5 class="my-2 me-3">
36
- <span class="badge bg-primary text-wrap">
37
- <%= t :last_seen, name: @discussion.last_moderator_access_by.full_name %>
38
- <%= t :time_since, time: time_ago_in_words(@discussion.last_moderator_access_at) %>
36
+ <span class="badge bg-primary text-wrap responsible-moderator-badge">
37
+ <%= responsible_moderator_text_for(@discussion, current_user) %>
39
38
  </span>
40
39
  </h5>
41
40
  <% end %>
@@ -9,7 +9,7 @@
9
9
  <div class="mu-faqs-content">
10
10
  <%= @faqs %>
11
11
  </div>
12
- <div class="mu-faqs-navbar hidden-xs">
12
+ <div class="mu-faqs-navbar d-none d-md-block">
13
13
  <nav>
14
14
  <ul>
15
15
  </ul>
@@ -58,11 +58,11 @@
58
58
  </span>
59
59
  <span class="d-none d-lg-inline"><%= discussion.exercise.guide.name %> -</span>
60
60
  <span><%= discussion.exercise.name %></span>
61
- <% if discussion.last_moderator_access_visible_for?(current_user) %>
61
+ <% if discussion.current_responsible_visible_for?(current_user) %>
62
62
  <div class="float-end discussion-moderator-access" >
63
- <%= profile_picture_for(discussion.last_moderator_access_by, height: 32) %>
63
+ <%= profile_picture_for(discussion.responsible_moderator_by, height: 32) %>
64
64
  <span class="moderator-initials">
65
- <%= discussion.last_moderator_access_by.name_initials %>
65
+ <%= discussion.responsible_moderator_by.name_initials %>
66
66
  </span>
67
67
  </div>
68
68
  <% end %>
@@ -28,17 +28,11 @@
28
28
 
29
29
  <%= yield :navbar %>
30
30
 
31
- <div class="<%= exercise_container_type %><%= ' mb-5' unless input_kids? %>" id="wrap">
32
- <div class="toast-container bottom-0 start-0 p-3">
33
- <% if notice %>
34
- <%= toast_notice(notice) %>
35
- <% elsif alert %>
36
- <%= toast_alert(alert) %>
37
- <% elsif info %>
38
- <%= toast_info(info) %>
39
- <% end %>
40
- </div>
31
+ <div class="toast-container bottom-0 start-0 p-3">
32
+ <%= render partial: 'layouts/toast' %>
33
+ </div>
41
34
 
35
+ <div class="<%= exercise_container_type %><%= ' mb-5' unless input_kids? %>" id="wrap">
42
36
  <%= yield %>
43
37
  </div>
44
38