mumuki-laboratory 9.0.0 → 9.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) 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/discussions.js +43 -2
  4. data/app/assets/javascripts/mumuki_laboratory/application/faqs.js +90 -0
  5. data/app/assets/javascripts/mumuki_laboratory/application/organization.js +32 -0
  6. data/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js +1 -1
  7. data/app/assets/javascripts/mumuki_laboratory/application/user.js +49 -5
  8. data/app/assets/stylesheets/mumuki_laboratory/application/_modules.scss +3 -0
  9. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_activity.scss +12 -0
  10. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_certificate.scss +33 -0
  11. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_discussion.scss +14 -2
  12. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_faqs.scss +84 -0
  13. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_user_menu.scss +30 -2
  14. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_user_profile.scss +1 -0
  15. data/app/controllers/certificates_controller.rb +28 -0
  16. data/app/controllers/concerns/with_certificate_render.rb +25 -0
  17. data/app/controllers/discussions_messages_controller.rb +6 -2
  18. data/app/controllers/faqs_controller.rb +6 -0
  19. data/app/controllers/users_controller.rb +17 -0
  20. data/app/helpers/certificate_helper.rb +13 -0
  21. data/app/helpers/discussions_helper.rb +5 -1
  22. data/app/helpers/links_helper.rb +9 -1
  23. data/app/helpers/menu_bar_helper.rb +14 -10
  24. data/app/helpers/user_activity_helper.rb +48 -0
  25. data/app/helpers/user_menu_helper.rb +27 -5
  26. data/app/mailers/application_mailer.rb +0 -1
  27. data/app/mailers/user_mailer.rb +9 -0
  28. data/app/views/certificates/_certificate.html.erb +44 -0
  29. data/app/views/certificates/_download.html.erb +20 -0
  30. data/app/views/certificates/verify.html.erb +40 -0
  31. data/app/views/discussions/_description_message.html.erb +1 -1
  32. data/app/views/discussions/_message.html.erb +1 -1
  33. data/app/views/discussions/_new_message.html.erb +13 -2
  34. data/app/views/discussions/new.html.erb +1 -1
  35. data/app/views/faqs/index.html.erb +20 -0
  36. data/app/views/layouts/_main.html.erb +4 -0
  37. data/app/views/layouts/_user_menu.html.erb +12 -16
  38. data/app/views/layouts/application.html.erb +6 -1
  39. data/app/views/layouts/exercise_inputs/editors/_code.html.erb +2 -1
  40. data/app/views/user_mailer/certificate.html.erb +339 -0
  41. data/app/views/user_mailer/certificate.text.erb +10 -0
  42. data/app/views/users/_activity_indicator.html.erb +17 -0
  43. data/app/views/users/activity.html.erb +37 -0
  44. data/app/views/users/certificates.html.erb +32 -0
  45. data/config/routes.rb +9 -0
  46. data/lib/mumuki/laboratory/extensions.rb +1 -0
  47. data/lib/mumuki/laboratory/extensions/date_and_time.rb +11 -0
  48. data/lib/mumuki/laboratory/locales/en.yml +20 -1
  49. data/lib/mumuki/laboratory/locales/es-CL.yml +25 -3
  50. data/lib/mumuki/laboratory/locales/es.yml +25 -3
  51. data/lib/mumuki/laboratory/locales/pt.yml +26 -1
  52. data/lib/mumuki/laboratory/version.rb +1 -1
  53. data/spec/controllers/certificates_controller_spec.rb +15 -0
  54. data/spec/controllers/discussions_messages_controller_spec.rb +20 -1
  55. data/spec/dummy/db/schema.rb +5 -1
  56. data/spec/features/certificate_programs_flow_spec.rb +17 -0
  57. data/spec/features/discussion_flow_spec.rb +2 -0
  58. data/spec/features/menu_bar_spec.rb +20 -0
  59. data/spec/features/profile_flow_spec.rb +12 -0
  60. data/spec/features/user_activity_flow_spec.rb +65 -0
  61. data/spec/helpers/application_helper_spec.rb +10 -0
  62. data/spec/helpers/certificate_helper_spec.rb +15 -0
  63. data/spec/helpers/user_activity_helper_spec.rb +32 -0
  64. metadata +171 -99
@@ -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)
@@ -84,6 +92,6 @@ module LinksHelper
84
92
  end
85
93
 
86
94
  def url_for_bibliotheca_guide(guide)
87
- "#{url_for_application(:bibliotheca_ui)}/#/guides/#{guide.slug}"
95
+ "#{url_for_application(:bibliotheca_ui).chomp('/')}/guides/#{guide.slug}"
88
96
  end
89
97
  end
@@ -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
@@ -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>