mumuki-laboratory 9.0.1 → 9.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/mumuki_laboratory/application/certificate.js +17 -0
  3. data/app/assets/javascripts/mumuki_laboratory/application/codemirror.js +10 -2
  4. data/app/assets/javascripts/mumuki_laboratory/application/discussions.js +43 -2
  5. data/app/assets/javascripts/mumuki_laboratory/application/faqs.js +90 -0
  6. data/app/assets/javascripts/mumuki_laboratory/application/multiple-files.js +7 -0
  7. data/app/assets/javascripts/mumuki_laboratory/application/organization.js +32 -0
  8. data/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js +1 -1
  9. data/app/assets/javascripts/mumuki_laboratory/application/user.js +49 -5
  10. data/app/assets/stylesheets/mumuki_laboratory/application/_modules.scss +3 -0
  11. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_activity.scss +12 -0
  12. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_certificate.scss +33 -0
  13. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_discussion.scss +14 -2
  14. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_faqs.scss +84 -0
  15. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_terms.scss +3 -2
  16. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_user_menu.scss +30 -2
  17. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_user_profile.scss +1 -0
  18. data/app/controllers/certificates_controller.rb +28 -0
  19. data/app/controllers/concerns/with_certificate_render.rb +25 -0
  20. data/app/controllers/discussions_messages_controller.rb +6 -2
  21. data/app/controllers/faqs_controller.rb +6 -0
  22. data/app/controllers/users_controller.rb +17 -0
  23. data/app/helpers/certificate_helper.rb +13 -0
  24. data/app/helpers/discussions_helper.rb +5 -1
  25. data/app/helpers/links_helper.rb +8 -0
  26. data/app/helpers/menu_bar_helper.rb +14 -10
  27. data/app/helpers/multiple_file_editor_helper.rb +2 -1
  28. data/app/helpers/user_activity_helper.rb +48 -0
  29. data/app/helpers/user_menu_helper.rb +27 -5
  30. data/app/mailers/application_mailer.rb +0 -1
  31. data/app/mailers/user_mailer.rb +9 -0
  32. data/app/views/certificates/_certificate.html.erb +44 -0
  33. data/app/views/certificates/_download.html.erb +20 -0
  34. data/app/views/certificates/verify.html.erb +40 -0
  35. data/app/views/discussions/_description_message.html.erb +1 -1
  36. data/app/views/discussions/_message.html.erb +1 -1
  37. data/app/views/discussions/_new_message.html.erb +13 -2
  38. data/app/views/discussions/new.html.erb +1 -1
  39. data/app/views/faqs/index.html.erb +20 -0
  40. data/app/views/layouts/_main.html.erb +4 -0
  41. data/app/views/layouts/_user_menu.html.erb +13 -16
  42. data/app/views/layouts/application.html.erb +6 -1
  43. data/app/views/layouts/exercise_inputs/editors/_code.html.erb +2 -1
  44. data/app/views/layouts/exercise_inputs/editors/_multiple_files.html.erb +1 -1
  45. data/app/views/user_mailer/certificate.html.erb +339 -0
  46. data/app/views/user_mailer/certificate.text.erb +10 -0
  47. data/app/views/users/_activity_indicator.html.erb +17 -0
  48. data/app/views/users/activity.html.erb +37 -0
  49. data/app/views/users/certificates.html.erb +32 -0
  50. data/config/initializers/inflections.rb +3 -0
  51. data/config/routes.rb +9 -0
  52. data/lib/mumuki/laboratory/extensions.rb +1 -0
  53. data/lib/mumuki/laboratory/extensions/date_and_time.rb +11 -0
  54. data/lib/mumuki/laboratory/locales/en.yml +20 -1
  55. data/lib/mumuki/laboratory/locales/es-CL.yml +25 -3
  56. data/lib/mumuki/laboratory/locales/es.yml +25 -3
  57. data/lib/mumuki/laboratory/locales/pt.yml +26 -1
  58. data/lib/mumuki/laboratory/version.rb +1 -1
  59. data/spec/controllers/certificates_controller_spec.rb +15 -0
  60. data/spec/controllers/discussions_messages_controller_spec.rb +20 -1
  61. data/spec/dummy/db/schema.rb +20 -3
  62. data/spec/features/certificate_programs_flow_spec.rb +17 -0
  63. data/spec/features/discussion_flow_spec.rb +2 -0
  64. data/spec/features/menu_bar_spec.rb +20 -0
  65. data/spec/features/profile_flow_spec.rb +12 -0
  66. data/spec/features/user_activity_flow_spec.rb +65 -0
  67. data/spec/helpers/certificate_helper_spec.rb +15 -0
  68. data/spec/helpers/user_activity_helper_spec.rb +32 -0
  69. metadata +170 -97
@@ -31,6 +31,9 @@
31
31
  }
32
32
  .terms-card-body {
33
33
  padding: 1.25rem;
34
+ p {
35
+ text-align: justify;
36
+ }
34
37
  }
35
38
 
36
39
  .terms-acceptance {
@@ -41,5 +44,3 @@
41
44
  display: block
42
45
  }
43
46
  }
44
-
45
-
@@ -1,12 +1,41 @@
1
1
  .mu-user-menu {
2
2
  display: flex;
3
+ margin-top: 25px;
4
+ }
5
+
6
+ .mu-user-menu-content {
7
+ flex-grow: 1;
3
8
  }
4
9
 
5
10
  .mu-user-menu-header {
6
11
  font-size: 15px;
7
12
  color: $mu-color-disabled;
8
13
  text-transform: uppercase;
9
- margin-bottom: 30px;
14
+ padding-left: 2px;
15
+ cursor: pointer;
16
+
17
+ @media only screen and (min-width: $screen-md-min) {
18
+ padding-left: 15px;
19
+ cursor: auto;
20
+
21
+ i {
22
+ display: none;
23
+ }
24
+
25
+ span {
26
+ padding-left: 0px;
27
+ }
28
+ }
29
+ }
30
+
31
+ .mu-user-menu-items {
32
+ margin-top: 30px;
33
+
34
+ &.mu-hidden-sm-screen {
35
+ @media only screen and (max-width: $screen-sm-max) {
36
+ display: none;
37
+ }
38
+ }
10
39
  }
11
40
 
12
41
  .mu-user-menu-item {
@@ -30,6 +59,5 @@
30
59
  border-top: 2px solid $mu-color-separator;
31
60
 
32
61
  margin: 25px 0;
33
- width: 100%
34
62
  }
35
63
  }
@@ -0,0 +1,28 @@
1
+ class CertificatesController < ApplicationController
2
+ include WithCertificateRender
3
+
4
+ before_action :authorize_if_private!, only: [:show]
5
+ before_action :set_certificate!
6
+ before_action :validate_current_user!, only: [:download]
7
+
8
+ def verify
9
+ end
10
+
11
+ def show
12
+ end
13
+
14
+ def download
15
+ send_data pdf_for(@certificate), filename: @certificate.filename
16
+ end
17
+
18
+ private
19
+
20
+ def set_certificate!
21
+ @certificate = Certificate.find_by! code: params[:code]
22
+ end
23
+
24
+ def validate_current_user!
25
+ raise Mumuki::Domain::NotFoundError unless @certificate.for_user? current_user
26
+ end
27
+
28
+ end
@@ -0,0 +1,25 @@
1
+ require 'wicked_pdf'
2
+ require 'rqrcode'
3
+
4
+ module WithCertificateRender
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ helper_method :qr_for
10
+ end
11
+
12
+ def qr_for(certificate)
13
+ qr = RQRCode::QRCode.new(verify_certificate_url certificate.code).as_svg(color: '0B465D')
14
+ "data:image/svg+xml,#{URI.encode(qr)}"
15
+ end
16
+
17
+ def pdf_for(certificate)
18
+ pdf_html = render_to_string(partial: 'certificates/download', locals: { certificate: certificate })
19
+ WickedPdf.new.pdf_from_string pdf_html,
20
+ orientation: 'Landscape',
21
+ page_size: 'A5',
22
+ margin: { top: 0.5, left: 1, bottom: 0.5, right: 1 }
23
+
24
+ end
25
+ end
@@ -3,7 +3,7 @@ class DiscussionsMessagesController < AjaxController
3
3
 
4
4
  before_action :set_discussion!, only: [:create, :destroy]
5
5
  before_action :authorize_user!, only: [:destroy]
6
- before_action :authorize_moderator!, only: [:question, :approve]
6
+ before_action :authorize_moderator!, only: [:question, :approve, :preview]
7
7
 
8
8
  def create
9
9
  @discussion.submit_message! message_params, current_user
@@ -15,8 +15,12 @@ class DiscussionsMessagesController < AjaxController
15
15
  redirect_back(fallback_location: root_path)
16
16
  end
17
17
 
18
+ def preview
19
+ render json: { preview: Message.new(content: params[:content]).content_html }
20
+ end
21
+
18
22
  def approve
19
- current_message.toggle_approved!
23
+ current_message.toggle_approved! current_user
20
24
  head :ok
21
25
  end
22
26
 
@@ -0,0 +1,6 @@
1
+ class FAQsController < ApplicationController
2
+ def index
3
+ @faqs = Organization.current.faqs_html
4
+ raise Mumuki::Domain::NotFoundError unless @faqs.present?
5
+ end
6
+ end
@@ -30,6 +30,14 @@ class UsersController < ApplicationController
30
30
  @watched_discussions = current_user.watched_discussions_in_organization
31
31
  end
32
32
 
33
+ def activity
34
+ @activity = UserStats.stats_for(current_user).activity date_range_params
35
+ end
36
+
37
+ def certificates
38
+ @certificates ||= current_user.certificates_in_organization
39
+ end
40
+
33
41
  def unsubscribe
34
42
  user_id = User.unsubscription_verifier.verify(params[:id])
35
43
  User.find(user_id).unsubscribe_from_reminders!
@@ -50,4 +58,13 @@ class UsersController < ApplicationController
50
58
  @user = current_user
51
59
  end
52
60
 
61
+ def date_range_params
62
+ @date_from = params[:date_from].try { |it| Date.parse it }
63
+ to = params[:date_to].try { |it| Date.parse it }
64
+ if @date_from && to
65
+ @date_from.beginning_of_day..(to - 1.day).end_of_day
66
+ else
67
+ nil
68
+ end
69
+ end
53
70
  end
@@ -0,0 +1,13 @@
1
+ module CertificateHelper
2
+ def linkedin_post_url(certificate)
3
+ URI::HTTPS.build host: 'www.linkedin.com',
4
+ path: '/profile/add',
5
+ query: Rack::Utils.build_query(startTask: 'CERTIFICATION_NAME',
6
+ name: certificate.title,
7
+ organizationId: ENV['MUMUKI_LINKEDIN_ORGANIZATION_ID'],
8
+ issueYear: certificate.created_at.year,
9
+ issueMonth: certificate.created_at.month,
10
+ certUrl: verify_certificate_url(certificate.code),
11
+ certId: certificate.code)
12
+ end
13
+ end
@@ -12,7 +12,7 @@ module DiscussionsHelper
12
12
  end
13
13
 
14
14
  def user_discussions_link
15
- discussions_link user_discussions_icon(t(:my_doubts)), user_path(anchor: 'discussions') if current_user.watched_discussions.present?
15
+ discussions_link(user_discussions_icon(t(:my_doubts)), discussions_user_path) if current_user.watched_discussions.present?
16
16
  end
17
17
 
18
18
  def others_discussions_icon(text)
@@ -195,4 +195,8 @@ module DiscussionsHelper
195
195
  def should_show_approved_for?(user, message)
196
196
  !user&.moderator_here? && message.approved? && !message.from_moderator?
197
197
  end
198
+
199
+ def discussion_user_name(user)
200
+ user.name
201
+ end
198
202
  end
@@ -59,10 +59,18 @@ module LinksHelper
59
59
  link_to t(:forum_terms), discussions_terms_path, target: '_blank'
60
60
  end
61
61
 
62
+ def link_to_faqs
63
+ link_to t(:faqs), faqs_path, target: '_blank' if faqs_enabled_here?
64
+ end
65
+
62
66
  def turbolinks_enable_for(exercise)
63
67
  %Q{data-turbolinks="#{!exercise.input_kids?}"}.html_safe
64
68
  end
65
69
 
70
+ def faqs_enabled_here?
71
+ Organization.current.faqs.present?
72
+ end
73
+
66
74
  private
67
75
 
68
76
  def extract_name(named, options)
@@ -1,9 +1,9 @@
1
1
  module MenuBarHelper
2
2
  def menu_bar_links
3
3
  [
4
- link_to_profile,
5
- link_to_classroom,
6
- link_to_bibliotheca,
4
+ menu_link_to_profile,
5
+ menu_link_to_classroom,
6
+ menu_link_to_bibliotheca,
7
7
  solve_discussions_link,
8
8
  user_discussions_link
9
9
  ]
@@ -17,25 +17,25 @@ module MenuBarHelper
17
17
  content_tag :li, link
18
18
  end
19
19
 
20
- def link_to_profile
20
+ def menu_link_to_profile
21
21
  menu_item('user', :my_account, user_path)
22
22
  end
23
23
 
24
- def link_to_classroom
25
- link_to_application 'graduation-cap', :classroom_ui, :teacher_here?
24
+ def menu_link_to_classroom
25
+ menu_link_to_application 'graduation-cap', :classroom_ui, :teacher_here?
26
26
  end
27
27
 
28
- def link_to_bibliotheca
29
- link_to_application :book, :bibliotheca_ui, :writer?
28
+ def menu_link_to_bibliotheca
29
+ menu_link_to_application :book, :bibliotheca_ui, :writer?
30
30
  end
31
31
 
32
- def link_to_application(icon, app_name, minimal_permissions)
32
+ def menu_link_to_application(icon, app_name, minimal_permissions)
33
33
  return unless current_user&.send(minimal_permissions)
34
34
  url = url_for_application(app_name)
35
35
  menu_item icon, app_name, url
36
36
  end
37
37
 
38
- def logout_link
38
+ def logout_menu_link
39
39
  li_tag menu_item('sign-out-alt', :sign_out, logout_path(origin: url_for))
40
40
  end
41
41
 
@@ -46,4 +46,8 @@ module MenuBarHelper
46
46
  def any_menu_bar_links?
47
47
  menu_bar_links.any?
48
48
  end
49
+
50
+ def menu_link_to_faqs
51
+ li_tag menu_item('question', :faqs, faqs_path)
52
+ end
49
53
  end
@@ -9,6 +9,7 @@ module MultipleFileEditorHelper
9
9
 
10
10
  def multifile_hidden_inputs
11
11
  hidden_field_tag('highlight-modes', highlight_modes.to_json) +
12
- hidden_field_tag('multifile-locales', multifile_locales.to_json)
12
+ hidden_field_tag('multifile-locales', multifile_locales.to_json) +
13
+ hidden_field_tag('multifile-default-content', @files.to_json)
13
14
  end
14
15
  end
@@ -0,0 +1,48 @@
1
+ module UserActivityHelper
2
+ def activity_selector_week_range_for(organization = Organization.current)
3
+ start = organization.activity_start_date min_week
4
+ (start.prev_occurring(:monday) + 1..Date.today)
5
+ .step(7)
6
+ .to_a
7
+ .reverse
8
+ .map { |it| [it, it + 7.days] }
9
+ end
10
+
11
+ def mark_period_if_active(period_start)
12
+ active_period?(period_start) && 'class=active'
13
+ end
14
+
15
+ def solved_exercises_percentage
16
+ percentage = @activity[:exercises][:solved_count].to_f / @activity[:exercises][:count] * 100
17
+ "#{percentage.ceil}%"
18
+ end
19
+
20
+ def exercises_activity_stats
21
+ solved_count = {
22
+ name: t(:solved_exercises_count, count: @activity[:exercises][:solved_count]),
23
+ value: @activity[:exercises][:solved_count] }
24
+ solved_percentage = {
25
+ name: t(:solved_exercises_percentage),
26
+ value: solved_exercises_percentage }
27
+
28
+ @date_from ? [solved_count] : [solved_count, solved_percentage]
29
+ end
30
+
31
+ def messages_activity_stats
32
+ count = @activity[:messages][:count]
33
+ approved = @activity[:messages][:approved]
34
+
35
+ [{name: t(:messages_pluralized, count: count), value: count},
36
+ {name: t(:approved_messages, count: approved), value: approved}]
37
+ end
38
+
39
+ private
40
+
41
+ def active_period?(period_start)
42
+ period_start ? @date_from == period_start : !@date_from
43
+ end
44
+
45
+ def min_week
46
+ 8.week.ago.to_date
47
+ end
48
+ end
@@ -1,18 +1,40 @@
1
1
  module UserMenuHelper
2
+ def user_menu_header
3
+ content_tag :div, user_menu_header_icon, class: 'mu-user-menu-header'
4
+ end
5
+
6
+ def user_menu_divider
7
+ content_tag :div, '', class: 'mu-user-menu-divider horizontal'
8
+ end
9
+
2
10
  def profile_user_menu_link
3
- user_menu_link t(:my_profile), user_path, 'show'
11
+ user_menu_item t(:my_profile), user_path, 'show'
4
12
  end
5
13
 
6
14
  def messages_user_menu_link
7
- user_menu_link t(:messages), messages_user_path, 'messages'
15
+ user_menu_item t(:messages), messages_user_path, 'messages'
8
16
  end
9
17
 
10
18
  def discussions_user_menu_link
11
- user_menu_link t(:discussions), discussions_user_path, 'discussions'
19
+ user_menu_item t(:discussions), discussions_user_path, 'discussions' if current_user&.can_discuss_here?
20
+ end
21
+
22
+ def activity_user_menu_link
23
+ user_menu_item t(:activity), activity_user_path, 'activity'
12
24
  end
13
25
 
14
- def user_menu_link(label, path, active_on)
26
+ def certificates_user_menu_link
27
+ user_menu_item t(:certificates), certificates_user_path, 'certificates'
28
+ end
29
+
30
+ private
31
+
32
+ def user_menu_item(label, path, active_on)
15
33
  link_klass = 'active' if action_name == active_on
16
- link_to label, path, { class: link_klass }.compact
34
+ content_tag :div, link_to(label, path, { class: link_klass }.compact), class: 'mu-user-menu-item'
35
+ end
36
+
37
+ def user_menu_header_icon
38
+ fa_icon('chevron-down', text: t(:my_account), id: 'mu-user-menu-header-icon', right: true)
17
39
  end
18
40
  end
@@ -13,4 +13,3 @@ class ApplicationMailer < ActionMailer::Base
13
13
  mailer_environment_variables.all? { |env_var| ENV[env_var].present? }
14
14
  end
15
15
  end
16
-
@@ -1,4 +1,6 @@
1
1
  class UserMailer < ApplicationMailer
2
+ include WithCertificateRender
3
+
2
4
  def welcome_email(user, organization)
3
5
  with_locale(user, organization) do
4
6
  organization_name = organization.display_name || t(:your_new_organization)
@@ -18,6 +20,13 @@ class UserMailer < ApplicationMailer
18
20
  end
19
21
  end
20
22
 
23
+ def certificate(certificate)
24
+ with_locale certificate.user, certificate.organization do
25
+ attachments[certificate.filename] = pdf_for(certificate)
26
+ mail to: certificate.user.email, subject: t(:certificate)
27
+ end
28
+ end
29
+
21
30
  def with_locale(user, organization = nil, &block)
22
31
  @user = user
23
32
  @unsubscribe_code = User.unsubscription_verifier.generate(user.id)
@@ -0,0 +1,44 @@
1
+ <style>
2
+
3
+ .mu-certificate-box {
4
+ overflow: hidden;
5
+ height: 210mm;
6
+ width: 297mm;
7
+ max-height: 210mm;
8
+ max-width: 297mm;
9
+ min-height: 210mm;
10
+ min-width: 297mm;
11
+ margin: 0;
12
+ box-sizing: border-box;
13
+ position: relative;
14
+ border: 1px solid lightgrey;
15
+ }
16
+
17
+ .mu-certificate-box section {
18
+ background: transparent;
19
+ z-index: 10;
20
+ }
21
+
22
+ .background-image {
23
+ position: absolute;
24
+ height: 210mm;
25
+ width: 297mm;
26
+ margin: 0;
27
+ padding: 0;
28
+ z-index: 0;
29
+ }
30
+
31
+ .qr-code {
32
+ position: absolute;
33
+ z-index: 10;
34
+ }
35
+
36
+ </style>
37
+
38
+ <div class="mu-certificate-box">
39
+ <img src="<%= certificate.background_image_url %>" class="background-image"/>
40
+ <%= render inline: certificate.template_html_erb, locals: certificate.template_locals %>
41
+ <a href="<%= verify_certificate_path certificate.code %>" target="_blank">
42
+ <div><img class="qr-code" src="<%= qr_for certificate %>"/></div>
43
+ </a>
44
+ </div>