mumuki-laboratory 9.8.2 → 9.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/mumuki_laboratory/application/bridge.js +15 -9
  3. data/app/assets/javascripts/mumuki_laboratory/application/confirmation.js +10 -8
  4. data/app/assets/javascripts/mumuki_laboratory/application/editors.js +5 -3
  5. data/app/assets/javascripts/mumuki_laboratory/application/messages.js +1 -30
  6. data/app/assets/javascripts/mumuki_laboratory/application/progress.js +4 -4
  7. data/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js +12 -1
  8. data/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js +8 -2
  9. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_discussion.scss +32 -34
  10. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_dropdown.scss +11 -0
  11. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_editor.scss +3 -0
  12. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_faqs.scss +1 -1
  13. data/app/controllers/application_controller.rb +1 -1
  14. data/app/controllers/messages_controller.rb +2 -5
  15. data/app/controllers/users_controller.rb +38 -1
  16. data/app/helpers/application_helper.rb +6 -2
  17. data/app/helpers/discussions_helper.rb +11 -4
  18. data/app/helpers/exam_registration_helper.rb +0 -4
  19. data/app/helpers/icons_helper.rb +1 -1
  20. data/app/helpers/menu_bar_helper.rb +7 -3
  21. data/app/helpers/messages_helper.rb +4 -8
  22. data/app/helpers/notifications_helper.rb +13 -0
  23. data/app/helpers/profile_helper.rb +4 -0
  24. data/app/helpers/time_zone_helper.rb +5 -0
  25. data/app/helpers/user_discussions_helper.rb +38 -0
  26. data/app/helpers/user_menu_helper.rb +4 -0
  27. data/app/mailers/user_mailer.rb +11 -1
  28. data/app/views/discussions/new.html.erb +1 -1
  29. data/app/views/layouts/_copyright.html.erb +1 -1
  30. data/app/views/layouts/_discussions.html.erb +1 -37
  31. data/app/views/layouts/_discussions_list.html.erb +38 -0
  32. data/app/views/layouts/_messages.html.erb +1 -7
  33. data/app/views/layouts/_user_menu.html.erb +1 -0
  34. data/app/views/layouts/exercise_inputs/forms/_interactive_form.html.erb +1 -1
  35. data/app/views/layouts/exercise_inputs/forms/_problem_form.html.erb +1 -1
  36. data/app/views/layouts/exercise_inputs/read_only_editors/_code.html.erb +1 -1
  37. data/app/views/layouts/exercise_inputs/read_only_editors/_multiple_files.html.erb +4 -4
  38. data/app/views/layouts/mailer.html.erb +7 -1
  39. data/app/views/notifications/_custom.html.erb +1 -0
  40. data/app/views/notifications/_dropdown.html.erb +2 -2
  41. data/app/views/notifications/_exam_authorization_request_updated.html.erb +2 -0
  42. data/app/views/notifications/_exam_registration.html.erb +2 -1
  43. data/app/views/notifications/previews/_custom.html.erb +1 -0
  44. data/app/views/notifications/previews/_discussion.html.erb +2 -0
  45. data/app/views/notifications/previews/_exam_authorization_request_updated.html.erb +2 -0
  46. data/app/views/notifications/previews/_exam_registration.html.erb +2 -0
  47. data/app/views/notifications/previews/_message.html.erb +2 -0
  48. data/app/views/user_mailer/1st_reminder.html.erb +7 -349
  49. data/app/views/user_mailer/1st_reminder.text.erb +1 -5
  50. data/app/views/user_mailer/2nd_reminder.html.erb +7 -349
  51. data/app/views/user_mailer/2nd_reminder.text.erb +1 -5
  52. data/app/views/user_mailer/3rd_reminder.html.erb +7 -349
  53. data/app/views/user_mailer/3rd_reminder.text.erb +1 -5
  54. data/app/views/user_mailer/_mail_template.erb +336 -0
  55. data/app/views/user_mailer/certificate.html.erb +7 -335
  56. data/app/views/user_mailer/certificate.text.erb +1 -4
  57. data/app/views/user_mailer/no_submissions_reminder.html.erb +7 -349
  58. data/app/views/user_mailer/no_submissions_reminder.text.erb +1 -5
  59. data/app/views/user_mailer/notification.html.erb +1 -0
  60. data/app/views/user_mailer/notification.text.erb +6 -0
  61. data/app/views/user_mailer/notifications/_custom.html.erb +11 -0
  62. data/app/views/user_mailer/notifications/_exam_authorization_request_updated.html.erb +1 -0
  63. data/app/views/user_mailer/notifications/_exam_registration.html.erb +7 -0
  64. data/app/views/user_mailer/notifications/exam_authorization_request_updated/_approved.html.erb +7 -0
  65. data/app/views/user_mailer/notifications/exam_authorization_request_updated/_rejected.html.erb +7 -0
  66. data/app/views/users/_basic_profile_fields.html.erb +30 -0
  67. data/app/views/users/_profile_fields.html.erb +1 -20
  68. data/app/views/users/discussions.html.erb +19 -11
  69. data/app/views/users/manage_notifications.html.erb +26 -0
  70. data/app/views/users/messages.html.erb +1 -1
  71. data/app/views/users/notifications.html.erb +54 -0
  72. data/config/routes.rb +4 -1
  73. data/lib/mumuki/laboratory.rb +1 -0
  74. data/lib/mumuki/laboratory/controllers/notifications.rb +2 -2
  75. data/lib/mumuki/laboratory/locales/en.yml +47 -7
  76. data/lib/mumuki/laboratory/locales/es-CL.yml +48 -7
  77. data/lib/mumuki/laboratory/locales/es.yml +49 -9
  78. data/lib/mumuki/laboratory/locales/pt.yml +46 -6
  79. data/lib/mumuki/laboratory/mailers/message_delivery.rb +15 -0
  80. data/lib/mumuki/laboratory/version.rb +1 -1
  81. data/spec/controllers/organizations_api_controller_spec.rb +6 -1
  82. data/spec/controllers/students_api_controller_spec.rb +1 -1
  83. data/spec/controllers/users_controller_spec.rb +48 -0
  84. data/spec/dummy/db/schema.rb +9 -1
  85. data/spec/features/not_found_public_flow_spec.rb +1 -1
  86. data/spec/features/notifications_flow_spec.rb +2 -1
  87. data/spec/features/profile_flow_spec.rb +1 -1
  88. data/spec/javascripts/editors-spec.js +21 -3
  89. data/spec/javascripts/submissions-store-spec.js +11 -0
  90. data/spec/mailers/previews/user_mailer_preview.rb +87 -1
  91. metadata +134 -115
  92. data/app/views/messages/errors.html.erb +0 -1
  93. data/app/views/notifications/_discussion.html.erb +0 -1
  94. data/app/views/notifications/_exam_authorization_request.html.erb +0 -1
  95. data/app/views/notifications/_message.html.erb +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab01a709d13fa43fb20ead7bb17a0f8d217ed1fb864423dd2af54ea197109c44
4
- data.tar.gz: 0f88578a615139a0ac93ca07c2ffa2f8580b5f6c99c900dacef03d34ba62d6e1
3
+ metadata.gz: 81fd1285c7db44af533a878ee10b6388976451e1d9b493c5aabf5db6516855bd
4
+ data.tar.gz: b76dcfb86b425f80fa31e2683561d897c7e047fb7ff538319317c1cee40a327c
5
5
  SHA512:
6
- metadata.gz: 438fef387af073a4423ade676a7739933ca090f5319a1d71098c41443c8c928094bcec9b98205ce7e1b43b9295f9270c131a86de553e0eabadb6b7ec63b51c49
7
- data.tar.gz: 5172fa138e0b5acd092e7e99cb1475abe8c07de26c1024d305805104c848a71168fe4a1dec9cc1efc44c51d7c7b2a7a5d37a5cc79cf9264ca165ef76fd38d195
6
+ metadata.gz: 9d583004159cc6bc0d88e318b3b0da5c9363b61a2b57f228f25f9ee19a016acb518f420b70015254367b2dd930b1887ae1ef61332cd349982251a1ecce995199
7
+ data.tar.gz: fec344b55bec29c42ca6b8017b7500ec08e0209aaf7649391890268adcc6f626077cb91040bfb2f391a38fe0a67e7af266b93fa0ccb28b6e01c2f9a5ebe9076b
@@ -13,7 +13,9 @@
13
13
  * @typedef {{
14
14
  * status: SubmissionStatus,
15
15
  * class_for_progress_list_item?: string,
16
- * guide_finished_by_solution?: boolean
16
+ * guide_finished_by_solution?: boolean,
17
+ * title_html?: string,
18
+ * in_gamified_context?: boolean
17
19
  * }} SubmissionResult
18
20
  */
19
21
 
@@ -47,7 +49,13 @@
47
49
  */
48
50
 
49
51
  /**
50
- * @typedef {Contents & {client_result?: SubmissionClientResult}} Submission
52
+ * A submission, composed by:
53
+ *
54
+ * * its actual contents
55
+ * * the client results
56
+ * * the _pristine flag, that tells whether the submission has been sent without previous results
57
+ *
58
+ * @typedef {Contents & {client_result?: SubmissionClientResult, _pristine: boolean}} Submission
51
59
  */
52
60
 
53
61
  /**
@@ -83,12 +91,12 @@ mumuki.bridge = (() => {
83
91
  */
84
92
  _submitSolution(submission) {
85
93
  const lastSubmission = mumuki.SubmissionsStore.getSubmissionResultFor(mumuki.exercise.id, submission);
86
- if (lastSubmission) {
87
- return $.Deferred().resolve(lastSubmission);
88
- } else {
94
+ if (submission._pristine || !lastSubmission) {
89
95
  return this._sendNewSolution(submission).done((result) => {
90
96
  mumuki.SubmissionsStore.setSubmissionResultFor(mumuki.exercise.id, {submission, result});
91
97
  });
98
+ } else {
99
+ return $.Deferred().resolve(lastSubmission);
92
100
  }
93
101
  }
94
102
 
@@ -107,14 +115,12 @@ mumuki.bridge = (() => {
107
115
  }
108
116
 
109
117
  /**
110
- * Pre-renders some html parts of submission UI, adding them to the given result
111
- *
112
118
  * @param {SubmissionResult} result
113
119
  * @returns {SubmissionResult}
120
+ * @see mumuki.renderers.results.preRenderResult
114
121
  */
115
122
  _preRenderResult(result) {
116
- result.class_for_progress_list_item = mumuki.renderers.results.progressListItemClassForStatus(result.status, true);
117
- result.title_html = mumuki.renderers.results.translatedTitleHtml(result.status, result.in_gamified_context);
123
+ mumuki.renderers.results.preRenderResult(result);
118
124
  return result;
119
125
  }
120
126
  }
@@ -1,17 +1,19 @@
1
1
  mumuki.load(() => {
2
- $('.btn-confirmation').on('click change', function (_evt) {
3
- var token = new mumuki.CsrfToken();
2
+ $('.btn-confirmation').on('click change', function (_event) {
3
+ const token = new mumuki.CsrfToken();
4
+ const $element = $(this);
4
5
 
5
6
  $.ajax(token.newRequest({
6
7
  method: 'POST',
7
- url: $(this).data('confirmation-url'),
8
+ url: $element.data('confirmation-url'),
8
9
  xhrFields: {withCredentials: true},
9
- success: function(data){
10
- mumuki.updateProgressBarAndShowModal(data);
10
+ success: function(result) {
11
+ result.status = "passed";
12
+ mumuki.renderers.results.preRenderResult(result)
13
+ mumuki.updateProgressBarAndShowModal(result);
14
+ window.location = $element.attr("href");
11
15
  }
12
16
  }));
13
-
14
- return true;
17
+ return false;
15
18
  });
16
19
  });
17
-
@@ -55,12 +55,14 @@ mumuki.editors = {
55
55
  * @returns {Submission}
56
56
  */
57
57
  getSubmission() {
58
- let content = {};
58
+ let submission = {
59
+ _pristine: $('.submission-results').children().length === 0
60
+ };
59
61
  let contents = this.getContents();
60
62
  contents.forEach((it) => {
61
- content[it.name] = it.value;
63
+ submission[it.name] = it.value;
62
64
  });
63
- return content;
65
+ return submission;
64
66
  },
65
67
 
66
68
  /**
@@ -19,6 +19,7 @@ mumuki.load(() => {
19
19
  $('.pending-messages-filter').removeClass('pending-messages-filter');
20
20
  $('button.btn-submit').removeClass('disabled');
21
21
  $('.pending-messages-text').remove();
22
+ $("a[data-bs-target='#messages']")[0].click();
22
23
  },
23
24
  readMessages: function (url) {
24
25
  Chat.tokenRequest({
@@ -47,35 +48,6 @@ mumuki.load(() => {
47
48
  setMessagesInterval: function () {
48
49
  mumuki.setInterval(Chat.getMessages, 60000);
49
50
  },
50
- submitMessagesForm: function (url, readUrl, errorUrl) {
51
- var $container = $('.mu-view-messages');
52
-
53
- function renderHTML(data) {
54
- $container.empty();
55
- $container.html(data);
56
- $("a[data-bs-target='#messages']").click();
57
- }
58
-
59
- function success(data) {
60
- renderHTML(data);
61
- Chat.readMessages(readUrl);
62
- }
63
-
64
- function error(_xhr) {
65
- Chat.tokenRequest({
66
- url: errorUrl,
67
- success: renderHTML,
68
- xhrFields: {withCredentials: true}
69
- });
70
- }
71
-
72
- $.ajax({
73
- url: url,
74
- success: success,
75
- error: error,
76
- xhrFields: {withCredentials: true}
77
- });
78
- },
79
51
  openNewMessageModal: function () {
80
52
  Chat.$newMessageModal().modal({backdrop: false, keyboard: false});
81
53
  Chat.$body().addClass("new-message-modal-open");
@@ -90,4 +62,3 @@ mumuki.load(() => {
90
62
  mumuki.Chat = Chat;
91
63
 
92
64
  });
93
-
@@ -2,11 +2,11 @@ mumuki.progress = (() => {
2
2
  /**
3
3
  * Updates the current exercise progress indicator
4
4
  *
5
- * @param {SubmissionResult} data
5
+ * @param {SubmissionResult} result
6
6
  * */
7
- function updateProgressBarAndShowModal(data) {
8
- $('.progress-list-item.active').attr('class', data.class_for_progress_list_item);
9
- if(data.guide_finished_by_solution) new bootstrap.Modal('#guide-done').show();
7
+ function updateProgressBarAndShowModal(result) {
8
+ $('.progress-list-item.active').attr('class', result.class_for_progress_list_item);
9
+ if(result.guide_finished_by_solution) new bootstrap.Modal('#guide-done').show();
10
10
  }
11
11
 
12
12
  /**
@@ -71,11 +71,22 @@ mumuki.renderers.results = (() => {
71
71
  return `progress-list-item text-center ${classForStatus(status)} ${active ? 'active' : ''}`;
72
72
  }
73
73
 
74
+ /**
75
+ * Pre-renders some html parts of submission UI, adding them to the given result
76
+ *
77
+ * @param {SubmissionResult} result
78
+ */
79
+ function preRenderResult(result) {
80
+ result.class_for_progress_list_item = progressListItemClassForStatus(result.status, true);
81
+ result.title_html = translatedTitleHtml(result.status, result.in_gamified_context);
82
+ }
83
+
74
84
  return {
75
85
  classForStatus,
76
86
  iconForStatus,
77
87
  progressListItemClassForStatus,
78
- translatedTitleHtml
88
+ translatedTitleHtml,
89
+ preRenderResult
79
90
  };
80
91
  })();
81
92
 
@@ -89,8 +89,14 @@ mumuki.SubmissionsStore = (() => {
89
89
 
90
90
  // private API
91
91
 
92
- _asString(object) {
93
- return JSON.stringify(object);
92
+ /**
93
+ * Serializes the submission and result.
94
+ * Private attributes are ignored
95
+ */
96
+ _asString(submissionAndResult) {
97
+ return JSON.stringify(submissionAndResult, (key, value) => {
98
+ if (!key.startsWith("_")) return value
99
+ });
94
100
  }
95
101
 
96
102
  _keyFor(exerciseId) {
@@ -50,36 +50,28 @@ $moderator-badge-color: #dd9900;
50
50
  }
51
51
  }
52
52
 
53
- .discussion-pagination {
54
- display: flex;
55
- flex-direction: row;
56
- align-items: center;
57
- justify-content: center;
58
- }
59
-
60
53
  .discussion-language-icon {
61
- float: right;
62
- margin-right: 10px;
63
- margin-top: 6px;
64
- -ms-transform: scale(1.4, 1.4);
65
- -webkit-transform: scale(1.4, 1.4);
66
- transform: scale(1.4, 1.4);
54
+ display: inline;
55
+ padding-right: 5px;
67
56
  }
68
57
 
69
- .discussion-icon {
70
- -ms-transform: scale(0.8, 0.8);
71
- -webkit-transform: scale(0.8, 0.8);
72
- transform: scale(0.8, 0.8);
73
- margin-left: 5px;
74
- float: right;
75
- .fa-stack-1x {
76
- -ms-transform: scale(0.9, 0.9);
77
- -webkit-transform: scale(0.9, 0.9);
78
- transform: scale(0.9, 0.9);
58
+ .discussion-validated-messages-count {
59
+ .fa-check {
60
+ font-size: calc(#{$font-size-base} * 0.5);
61
+ position: absolute;
62
+ margin-left: -13px;
63
+ margin-top: 10px;
79
64
  }
80
65
  }
81
66
  }
82
67
 
68
+ .discussion-pagination {
69
+ display: flex;
70
+ flex-direction: row;
71
+ align-items: center;
72
+ justify-content: center;
73
+ }
74
+
83
75
  .discussions-no-filtered-results {
84
76
  padding: 20px;
85
77
  border: 1px solid $discussion-toolbar-border-color;
@@ -96,7 +88,7 @@ $moderator-badge-color: #dd9900;
96
88
  }
97
89
 
98
90
  .discussion-status-icon {
99
- padding-right: 10px;
91
+ padding-left: 2px;
100
92
  }
101
93
 
102
94
  .discussion-row {
@@ -106,11 +98,26 @@ $moderator-badge-color: #dd9900;
106
98
  font-size: 18px;
107
99
  font-weight: bold;
108
100
  }
101
+ .discussion-title-icons {
102
+ display: flex;
103
+ flex-shrink: 0;
104
+ align-items: center;
105
+ font-size: calc(#{$font-size-base} * 0.95);
106
+
107
+ > * {
108
+ padding-left: 20px;
109
+ }
110
+ }
109
111
  .discussion-description {
110
112
  overflow: hidden;
111
113
  display: block;
112
114
  text-overflow: ellipsis;
113
115
  white-space: nowrap;
116
+ margin-bottom: 20px;
117
+ font-size: calc(#{$font-size-base} * 0.95);
118
+ }
119
+ .discussion-initiator {
120
+ font-size: calc(#{$font-size-base} * 0.85);
114
121
  }
115
122
  }
116
123
 
@@ -308,6 +315,7 @@ $moderator-badge-color: #dd9900;
308
315
  border: 1px solid $discussion-message-border-color;
309
316
  border-radius: 3px;
310
317
  position: relative;
318
+ margin-bottom: 20px;
311
319
  .discussion-message-bubble-header {
312
320
  background-color: $mu-color-highlight-background;
313
321
  height: 40px;
@@ -400,13 +408,3 @@ $moderator-badge-color: #dd9900;
400
408
  font-weight: normal;
401
409
  }
402
410
  }
403
-
404
- .discussion-moderator-access {
405
- margin-right: 20px;
406
- display: flex;
407
- flex-direction: column;
408
- align-items: center;
409
- .moderator-initials {
410
- font-size: 14px;
411
- }
412
- }
@@ -1,3 +1,14 @@
1
1
  .fixed-icon {
2
2
  margin-right: 5px;
3
3
  }
4
+
5
+ .mu-notification-preview {
6
+ max-width: 350px;
7
+ @extend .d-inline-block;
8
+ @extend .text-truncate;
9
+ }
10
+
11
+ .mu-stealth-link {
12
+ text-decoration: none;
13
+ color: unset;
14
+ }
@@ -115,3 +115,6 @@ body.fullscreen {
115
115
  }
116
116
  }
117
117
 
118
+ .pending-messages-text {
119
+ top: auto;
120
+ }
@@ -21,7 +21,7 @@
21
21
 
22
22
  @include media-breakpoint-down(md) {
23
23
  &:not(.active) {
24
- h3, p {
24
+ h3, p, ul {
25
25
  display: none
26
26
  }
27
27
  h2 {
@@ -37,7 +37,7 @@ class ApplicationController < ActionController::Base
37
37
  :login_button,
38
38
  :notifications_count,
39
39
  :has_notifications?,
40
- :notifications,
40
+ :user_notifications,
41
41
  :subject,
42
42
  :should_choose_organization?,
43
43
  :current_immersive_organizations,
@@ -2,7 +2,8 @@ class MessagesController < AjaxController
2
2
  before_action :set_exercise!, only: [:create, :read_messages]
3
3
 
4
4
  def index
5
- render json: {has_messages: has_messages?, messages_count: messages_count}
5
+ pending_messages = current_user.unread_messages
6
+ render json: {has_messages: pending_messages.present?, messages_count: pending_messages.count}
6
7
  end
7
8
 
8
9
  def read_messages
@@ -15,10 +16,6 @@ class MessagesController < AjaxController
15
16
  redirect_to @exercise
16
17
  end
17
18
 
18
- def errors
19
- render 'messages/errors', layout: false
20
- end
21
-
22
19
  private
23
20
 
24
21
  def set_exercise!
@@ -3,6 +3,8 @@ class UsersController < ApplicationController
3
3
 
4
4
  before_action :authenticate!, except: :terms
5
5
  before_action :set_user!
6
+ before_action :set_notification!, only: :toggle_read
7
+ before_action :verify_owns_notification!, only: :toggle_read
6
8
  skip_before_action :validate_accepted_role_terms!
7
9
 
8
10
  def update
@@ -27,7 +29,7 @@ class UsersController < ApplicationController
27
29
  end
28
30
 
29
31
  def discussions
30
- @watched_discussions = current_user.watched_discussions_in_organization
32
+ @watched_discussions = current_user.watched_discussions_in_organization.scoped_query_by(discussion_filter_params).unread_first
31
33
  end
32
34
 
33
35
  def activity
@@ -53,8 +55,39 @@ class UsersController < ApplicationController
53
55
  super << [:avatar_id, :avatar_type]
54
56
  end
55
57
 
58
+ def notifications
59
+ @notifications = @user.notifications_in_organization.order(created_at: :desc).page(params[:page])
60
+ end
61
+
62
+ def toggle_read
63
+ @notification.toggle! :read
64
+ redirect_to notifications_user_path
65
+ end
66
+
67
+ def show_manage_notifications
68
+ render 'manage_notifications'
69
+ end
70
+
71
+ def manage_notifications
72
+ @user.update! ignored_notifications: manage_notifications_params.reject { |_, allowed| allowed.to_boolean }.keys
73
+
74
+ redirect_to notifications_user_path, notice: I18n.t(:preferences_updated_successfully)
75
+ end
76
+
56
77
  private
57
78
 
79
+ def manage_notifications_params
80
+ params.require(:notifications)
81
+ end
82
+
83
+ def set_notification!
84
+ @notification = Notification.find(params[:id])
85
+ end
86
+
87
+ def verify_owns_notification!
88
+ raise Mumuki::Domain::NotFoundError unless @notification.user == @user
89
+ end
90
+
58
91
  def validate_user_profile!
59
92
  end
60
93
 
@@ -71,4 +104,8 @@ class UsersController < ApplicationController
71
104
  nil
72
105
  end
73
106
  end
107
+
108
+ def discussion_filter_params
109
+ @filter_params ||= params.permit([:page])
110
+ end
74
111
  end