mumuki-laboratory 9.9.0 → 9.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) 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/progress.js +4 -4
  6. data/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js +12 -1
  7. data/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js +8 -2
  8. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_discussion.scss +32 -34
  9. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_dropdown.scss +11 -0
  10. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_faqs.scss +1 -1
  11. data/app/controllers/application_controller.rb +1 -1
  12. data/app/controllers/users_controller.rb +38 -1
  13. data/app/helpers/application_helper.rb +2 -2
  14. data/app/helpers/discussions_helper.rb +11 -4
  15. data/app/helpers/icons_helper.rb +1 -1
  16. data/app/helpers/menu_bar_helper.rb +7 -3
  17. data/app/helpers/notifications_helper.rb +13 -0
  18. data/app/helpers/time_zone_helper.rb +5 -0
  19. data/app/helpers/user_discussions_helper.rb +38 -0
  20. data/app/helpers/user_menu_helper.rb +4 -0
  21. data/app/mailers/user_mailer.rb +11 -1
  22. data/app/views/discussions/new.html.erb +1 -1
  23. data/app/views/layouts/_discussions.html.erb +1 -37
  24. data/app/views/layouts/_discussions_list.html.erb +38 -0
  25. data/app/views/layouts/_user_menu.html.erb +1 -0
  26. data/app/views/layouts/exercise_inputs/read_only_editors/_code.html.erb +1 -1
  27. data/app/views/layouts/exercise_inputs/read_only_editors/_multiple_files.html.erb +4 -4
  28. data/app/views/layouts/mailer.html.erb +7 -1
  29. data/app/views/notifications/_custom.html.erb +1 -0
  30. data/app/views/notifications/_dropdown.html.erb +2 -2
  31. data/app/views/notifications/_exam_authorization_request_updated.html.erb +2 -0
  32. data/app/views/notifications/_exam_registration.html.erb +2 -1
  33. data/app/views/notifications/previews/_custom.html.erb +1 -0
  34. data/app/views/notifications/previews/_discussion.html.erb +2 -0
  35. data/app/views/notifications/previews/_exam_authorization_request_updated.html.erb +2 -0
  36. data/app/views/notifications/previews/_exam_registration.html.erb +2 -0
  37. data/app/views/notifications/previews/_message.html.erb +2 -0
  38. data/app/views/user_mailer/1st_reminder.html.erb +7 -349
  39. data/app/views/user_mailer/1st_reminder.text.erb +0 -4
  40. data/app/views/user_mailer/2nd_reminder.html.erb +7 -349
  41. data/app/views/user_mailer/2nd_reminder.text.erb +0 -4
  42. data/app/views/user_mailer/3rd_reminder.html.erb +7 -349
  43. data/app/views/user_mailer/3rd_reminder.text.erb +0 -4
  44. data/app/views/user_mailer/_mail_template.erb +336 -0
  45. data/app/views/user_mailer/certificate.html.erb +7 -335
  46. data/app/views/user_mailer/certificate.text.erb +0 -3
  47. data/app/views/user_mailer/no_submissions_reminder.html.erb +7 -349
  48. data/app/views/user_mailer/no_submissions_reminder.text.erb +0 -4
  49. data/app/views/user_mailer/notification.html.erb +1 -0
  50. data/app/views/user_mailer/notification.text.erb +6 -0
  51. data/app/views/user_mailer/notifications/_custom.html.erb +11 -0
  52. data/app/views/user_mailer/notifications/_exam_authorization_request_updated.html.erb +1 -0
  53. data/app/views/user_mailer/notifications/_exam_registration.html.erb +7 -0
  54. data/app/views/user_mailer/notifications/exam_authorization_request_updated/_approved.html.erb +7 -0
  55. data/app/views/user_mailer/notifications/exam_authorization_request_updated/_rejected.html.erb +7 -0
  56. data/app/views/users/discussions.html.erb +19 -11
  57. data/app/views/users/manage_notifications.html.erb +26 -0
  58. data/app/views/users/notifications.html.erb +54 -0
  59. data/config/routes.rb +4 -0
  60. data/lib/mumuki/laboratory.rb +1 -0
  61. data/lib/mumuki/laboratory/controllers/notifications.rb +2 -2
  62. data/lib/mumuki/laboratory/locales/en.yml +45 -5
  63. data/lib/mumuki/laboratory/locales/es-CL.yml +46 -5
  64. data/lib/mumuki/laboratory/locales/es.yml +47 -7
  65. data/lib/mumuki/laboratory/locales/pt.yml +44 -4
  66. data/lib/mumuki/laboratory/mailers/message_delivery.rb +15 -0
  67. data/lib/mumuki/laboratory/version.rb +1 -1
  68. data/spec/controllers/users_controller_spec.rb +48 -0
  69. data/spec/dummy/db/schema.rb +7 -1
  70. data/spec/features/notifications_flow_spec.rb +2 -1
  71. data/spec/javascripts/editors-spec.js +21 -3
  72. data/spec/javascripts/submissions-store-spec.js +11 -0
  73. data/spec/mailers/previews/user_mailer_preview.rb +87 -1
  74. metadata +135 -116
  75. data/app/views/notifications/_discussion.html.erb +0 -1
  76. data/app/views/notifications/_exam_authorization_request.html.erb +0 -1
  77. data/app/views/notifications/_message.html.erb +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49d2ca01f9b5f0c006b68ae6a0587ee4231db269c9d6bb4dd3a5a77833757c7f
4
- data.tar.gz: '0093396f29a33c579c63d5f227df36772f9aaee6867603f77a6d878f7eeb277d'
3
+ metadata.gz: 67eb631fe5c878f8151a859f82bcffc8225e087e59669f600e08ff66cc9034c0
4
+ data.tar.gz: ab208353d092d920d65d899ad84aecb58f7f89e15ecae80f6869e09e0df85b1e
5
5
  SHA512:
6
- metadata.gz: 4e03ec14d6309220ef8bac5fc19691a4659f6612936831c17601338ce10a309b896c2c352424e8f063514cf95f7c7edde0de225b0d387b8cf1452c2dc6b46986
7
- data.tar.gz: 4fe91de131798c91a7db006e4e89f6601c46c7bf61490c211f3090921ccdf08243695980c24a6ac720487d8d627a668c072ee051d5dfeb4c55815410f0badc70
6
+ metadata.gz: 9b33f896c2ae44fee775b055f2dfcd965280452a3ef073a19a8650d0ca4c7e66c223247a28e5d126bc7a8dda256ae74683488d2090f796d97aa390a63bc3c00a
7
+ data.tar.gz: 21b1a86962e2edfcb17e09a96ae9983cf89828251cf283b3a0dd2004a8b98b09d495cf75b2995793d02d138aba5eaaba9cda6bf4c8f22e722881a0a312f94c87
@@ -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
  /**
@@ -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
+ }
@@ -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,
@@ -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
@@ -45,8 +45,8 @@ module ApplicationHelper
45
45
  }.html_safe
46
46
  end
47
47
 
48
- def notification_preview_for(target)
49
- render "notifications/#{target.class.name.underscore}", { target: target }
48
+ def notification_preview_for(notification)
49
+ render "notifications/previews/#{notification.subject}", { notification: notification }
50
50
  end
51
51
 
52
52
  def current_time_zone_html
@@ -60,11 +60,18 @@ module DiscussionsHelper
60
60
  }.html_safe
61
61
  end
62
62
 
63
- def discussion_messages_icon(discussion)
63
+ def discussion_messages_count(discussion)
64
64
  %Q{
65
- <span class="discussion-icon fa-stack fa-xs">
66
- <i class="far fa-comment fa-stack-2x"></i>
67
- <i class="fas fa-stack-1x">#{discussion.validated_messages_count}</i>
65
+ <span class="discussion-messages-count">
66
+ #{fa_icon :comments, type: :regular, text: discussion.messages_count}
67
+ </span>
68
+ }.html_safe
69
+ end
70
+
71
+ def discussion_validated_messages_count(discussion)
72
+ %Q{
73
+ <span class="discussion-validated-messages-count">
74
+ #{fa_icon :comment, type: :regular}#{fa_icon :check, text: discussion.validated_messages_count}
68
75
  </span>
69
76
  }.html_safe
70
77
  end
@@ -50,6 +50,6 @@ module IconsHelper
50
50
  end
51
51
 
52
52
  def icon_for_read(read)
53
- tag('i', class: "fa#{read ? 'r' : 's'} fa-envelope")
53
+ tag.i(class: "fa#{read ? 'r' : 's'} fa-envelope#{read ? '-open' : ''}")
54
54
  end
55
55
  end
@@ -39,8 +39,12 @@ module MenuBarHelper
39
39
  li_tag menu_item('sign-out-alt', :sign_out, logout_path(origin: url_for))
40
40
  end
41
41
 
42
- def menu_item(icon, name, url, translation_params = {})
43
- link_to fixed_fa_icon(icon, text: t(name, translation_params)), url, role: 'menuitem', tabindex: '-1', class: 'dropdown-item'
42
+ def menu_item(icon, name, url, css_class = nil, **translation_params)
43
+ menu_text_item(icon, t(name, translation_params), url, css_class)
44
+ end
45
+
46
+ def menu_text_item(icon, text, url, css_class = nil)
47
+ link_to fixed_fa_icon(icon, text: text), url, role: 'menuitem', tabindex: '-1', class: "dropdown-item #{css_class}"
44
48
  end
45
49
 
46
50
  def any_menu_bar_links?
@@ -48,6 +52,6 @@ module MenuBarHelper
48
52
  end
49
53
 
50
54
  def menu_link_to_faqs
51
- li_tag menu_item('question', :faqs, faqs_path)
55
+ li_tag menu_item(:info, :faqs_abbreviated, faqs_path)
52
56
  end
53
57
  end