mumuki-laboratory 5.0.12 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +339 -0
  3. data/app/assets/javascripts/application/bridge.js +36 -7
  4. data/app/assets/javascripts/application/kids.js +92 -7
  5. data/app/assets/javascripts/application/submission.js +2 -1
  6. data/app/assets/stylesheets/application/modules/_highlight.scss +8 -0
  7. data/app/assets/stylesheets/application/modules/_kids.scss +92 -27
  8. data/app/controllers/api/base_controller.rb +23 -0
  9. data/app/controllers/api/courses_controller.rb +26 -0
  10. data/app/controllers/api/organizations_controller.rb +28 -0
  11. data/app/controllers/api/roles_controller.rb +47 -0
  12. data/app/controllers/api/students_controller.rb +9 -0
  13. data/app/controllers/api/teachers_controller.rb +9 -0
  14. data/app/controllers/api/users_controller.rb +18 -0
  15. data/app/controllers/assets_controller.rb +2 -1
  16. data/app/controllers/concerns/on_base_organization_only.rb +11 -0
  17. data/app/controllers/concerns/organizations_controller_template.rb +34 -0
  18. data/app/controllers/concerns/users_controller_template.rb +28 -0
  19. data/app/controllers/concerns/with_api_errors.rb +37 -0
  20. data/app/controllers/concerns/with_authorization.rb +19 -0
  21. data/app/controllers/concerns/with_errors_filter.rb +32 -0
  22. data/app/helpers/reset_button_helper.rb +1 -1
  23. data/app/models/api_client.rb +27 -0
  24. data/app/models/api_client_spec.rb +6 -0
  25. data/app/models/concerns/with_assignments.rb +4 -1
  26. data/app/models/course.rb +4 -6
  27. data/app/models/exercise/challenge.rb +5 -6
  28. data/app/models/language.rb +1 -1
  29. data/app/models/organization.rb +4 -0
  30. data/app/models/user.rb +22 -1
  31. data/app/views/exercise_solutions/_kids_results_button.html.erb +11 -0
  32. data/app/views/exercise_solutions/_results.html.erb +1 -7
  33. data/app/views/exercise_solutions/_results_button.html.erb +7 -0
  34. data/app/views/layouts/_kids.html.erb +5 -2
  35. data/app/views/layouts/exercise_inputs/editors/_custom.html.erb +1 -0
  36. data/app/views/layouts/exercise_inputs/forms/_kids_form.html.erb +3 -0
  37. data/app/views/layouts/exercise_inputs/layouts/_input_kids.html.erb +3 -3
  38. data/app/views/layouts/modals/_kids_context.html.erb +0 -3
  39. data/app/views/layouts/modals/_kids_results.html.erb +2 -0
  40. data/config/routes.rb +23 -0
  41. data/db/migrate/20180129142749_add_api_client.rb +11 -0
  42. data/lib/mumuki/laboratory/controllers/results_rendering.rb +7 -1
  43. data/lib/mumuki/laboratory/extensions/string.rb +8 -0
  44. data/lib/mumuki/laboratory/locales/en.yml +4 -0
  45. data/lib/mumuki/laboratory/locales/es.yml +4 -0
  46. data/lib/mumuki/laboratory/locales/pt.yml +4 -0
  47. data/lib/mumuki/laboratory/mumukit/auth.rb +7 -0
  48. data/lib/mumuki/laboratory/version.rb +1 -1
  49. data/public/amarillo_exito.svg +11 -0
  50. data/public/amarillo_fracaso.svg +5 -0
  51. data/public/compass_rose.svg +1417 -0
  52. data/spec/controllers/api_clients_controller.rb +26 -0
  53. data/spec/controllers/confirmations_controller_spec.rb +1 -1
  54. data/spec/controllers/courses_api_controller_spec.rb +28 -0
  55. data/spec/controllers/exercise_solutions_controller_spec.rb +1 -1
  56. data/spec/controllers/messages_controller_spec.rb +1 -1
  57. data/spec/controllers/organizations_api_controller_spec.rb +235 -0
  58. data/spec/controllers/students_api_controller_spec.rb +101 -0
  59. data/spec/controllers/users_api_controller_spec.rb +56 -0
  60. data/spec/dummy/db/schema.rb +9 -0
  61. data/spec/factories/api_client_factory.rb +18 -0
  62. data/spec/factories/course_factory.rb +9 -0
  63. data/spec/features/chapter_spec.rb +1 -1
  64. data/spec/features/complements_flow_spec.rb +1 -1
  65. data/spec/features/exams_flow_spec.rb +1 -1
  66. data/spec/features/exercise_flow_spec.rb +1 -1
  67. data/spec/features/guide_reset_spec.rb +1 -1
  68. data/spec/features/guides_flow_spec.rb +1 -1
  69. data/spec/features/home_public_flow_spec.rb +1 -1
  70. data/spec/features/lessons_flow_spec.rb +1 -1
  71. data/spec/features/links_flow_spec.rb +1 -1
  72. data/spec/features/login_flow_spec.rb +1 -1
  73. data/spec/features/profile_flow_spec.rb +1 -1
  74. data/spec/features/standard_flow_spec.rb +1 -1
  75. data/spec/helpers/application_helper_spec.rb +1 -1
  76. data/spec/helpers/email_helper_spec.rb +1 -1
  77. data/spec/helpers/exercise_input_helper_spec.rb +1 -1
  78. data/spec/helpers/with_breadcrumbs_spec.rb +1 -1
  79. data/spec/helpers/with_navigation_spec.rb +1 -1
  80. data/spec/models/assignment_spec.rb +1 -1
  81. data/spec/models/book_import_spec.rb +1 -1
  82. data/spec/models/book_spec.rb +1 -1
  83. data/spec/models/course_spec.rb +1 -1
  84. data/spec/models/event_generation_spec.rb +1 -1
  85. data/spec/models/exam_spec.rb +1 -1
  86. data/spec/models/exercise_spec.rb +11 -3
  87. data/spec/models/guide_spec.rb +1 -1
  88. data/spec/models/interactive_spec.rb +1 -1
  89. data/spec/models/lesson_spec.rb +1 -1
  90. data/spec/models/message_spec.rb +1 -1
  91. data/spec/models/navigation_spec.rb +1 -1
  92. data/spec/models/organization_spec.rb +1 -1
  93. data/spec/models/problem_spec.rb +1 -1
  94. data/spec/models/question_spec.rb +1 -1
  95. data/spec/models/reading_spec.rb +1 -1
  96. data/spec/models/solution_spec.rb +1 -1
  97. data/spec/models/usage_spec.rb +1 -1
  98. data/spec/models/user_changed_spec.rb +1 -1
  99. data/spec/models/user_spec.rb +5 -5
  100. data/spec/spec_helper.rb +19 -3
  101. metadata +37 -2
@@ -0,0 +1,28 @@
1
+ module UsersControllerTemplate
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :set_user!, only: [:show, :update]
6
+ before_action :set_new_user!, only: :create
7
+ before_action :protect_permissions_assignment!, only: [:create, :update]
8
+ end
9
+
10
+ private
11
+
12
+ def protect_permissions_assignment!
13
+ current_user.protect_permissions_assignment! user_params[:permissions], @user.permissions
14
+ end
15
+
16
+ def user_params
17
+ params.require(:user).permit(:first_name, :last_name, :email, :image_url, permissions: Mumukit::Auth::Roles::ROLES)
18
+ end
19
+
20
+ def set_user!
21
+ @user = User.find_by uid: params[:id]
22
+ end
23
+
24
+ def set_new_user!
25
+ @user = User.new user_params
26
+ end
27
+
28
+ end
@@ -0,0 +1,37 @@
1
+ module WithApiErrors
2
+ # TODO we should extend DynamiceErrors
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ unless Rails.application.config.consider_all_requests_local
7
+ rescue_from ActionController::RoutingError, with: :not_found!
8
+ end
9
+
10
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found!
11
+ rescue_from Mumukit::Auth::UnauthorizedAccessError, with: :forbidden!
12
+ rescue_from ActiveRecord::RecordInvalid, with: :bad_record!
13
+ end
14
+
15
+ private
16
+
17
+ def bad_record!(e)
18
+ render_errors! e.record.errors, 400
19
+ end
20
+
21
+ def not_found!(e)
22
+ render_error! e, 404
23
+ end
24
+
25
+ def forbidden!(e)
26
+ render_error! e, 403
27
+ end
28
+
29
+ def render_error!(e, status)
30
+ render_errors! [e.message], status
31
+ end
32
+
33
+ def render_errors!(errors, status)
34
+ summary = { errors: errors }
35
+ render json: summary, status: status
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ module WithAuthorization
2
+ extend ActiveSupport::Concern
3
+
4
+ def authorize_janitor!
5
+ authorize! :janitor
6
+ end
7
+
8
+ def authorize_owner!
9
+ authorize! :owner
10
+ end
11
+
12
+ def authorization_slug
13
+ protection_slug || '_/_'
14
+ end
15
+
16
+ def protection_slug
17
+ @slug
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ module WithErrorsFilter
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ around_action :catch_exceptions
6
+ rescue_from Exception do |e| catch_unhandled_errors(e) end
7
+ end
8
+
9
+ def catch_exceptions
10
+ yield
11
+ rescue ActiveRecord::RecordInvalid => e
12
+ render! e.record.errors
13
+ end
14
+
15
+ def catch_unhandled_errors(e)
16
+ summary = {
17
+ errors: {
18
+ :exception => "#{e.class.name} : #{e.message}"
19
+ }
20
+ }
21
+ summary[:trace] = e.backtrace[0, 10] if Rails.env.development?
22
+
23
+ render json: summary, status: :internal_server_error
24
+ end
25
+
26
+ private
27
+
28
+ def render!(errors)
29
+ summary = { errors: errors }
30
+ render json: summary, status: :bad_request
31
+ end
32
+ end
@@ -1,6 +1,6 @@
1
1
  module ResetButtonHelper
2
2
  def restart_icon
3
- fa_icon('trash-o', title: t(:restart), class: 'fa-fw', role: 'button', tabindex: '0', 'aria-label': t(:restart))
3
+ fa_icon('undo', title: t(:restart), class: 'fa-fw', role: 'button', tabindex: '0', 'aria-label': t(:restart))
4
4
  end
5
5
 
6
6
  def restart_guide_link(guide)
@@ -0,0 +1,27 @@
1
+ class ApiClient < ApplicationRecord
2
+
3
+ belongs_to :user
4
+ accepts_nested_attributes_for :user
5
+ validates_presence_of :description
6
+
7
+ #TODO add JWT expiration and api client invalidation
8
+ #TODO rename toke to encoded_token
9
+ before_create :set_encoded_token!
10
+
11
+ def verify!
12
+ raise 'Invalid Api Client' if Mumukit::Auth::Token.decode(token).uid != user.uid
13
+ end
14
+
15
+ def self.verify_token!(token)
16
+ client = find_by token: token
17
+ raise 'No Api Client found for Token' unless client
18
+ client.verify!
19
+ end
20
+
21
+ private
22
+
23
+ def set_encoded_token!
24
+ self.token = Mumukit::Auth::Token.encode user.uid, {}
25
+ end
26
+
27
+ end
@@ -0,0 +1,6 @@
1
+ require 'rails_helper'
2
+
3
+ describe ApiClient, type: :model do
4
+ let(:api_client) {create :api_client}
5
+ it {expect { Mumukit::Auth::Token.decode(api_client.token).verify_client!}.to_not raise_error }
6
+ end
@@ -19,7 +19,10 @@ module WithAssignments
19
19
  end
20
20
 
21
21
  def interpolate_for(user, field)
22
- language.directives_interpolations.interpolate(field, lambda { |content| replace_content_reference(user, content) }).first
22
+ language
23
+ .directives_interpolations
24
+ .interpolate(field, lambda { |content| replace_content_reference(user, content) })
25
+ .first
23
26
  end
24
27
 
25
28
  def default_content_for(user)
data/app/models/course.rb CHANGED
@@ -11,12 +11,10 @@ class Course < ApplicationRecord
11
11
  end
12
12
 
13
13
  def slug=(slug)
14
- self[:slug] = slug
15
- self[:organization_id] = Organization.find_by(name: Mumukit::Auth::Slug.parse(slug).organization).id
16
- end
14
+ s = Mumukit::Auth::Slug.parse(slug)
17
15
 
18
- def organization=(organization)
19
- self[:organization_id] = organization.id
20
- self[:slug] = "#{organization.name}/#{code}"
16
+ self[:slug] = slug
17
+ self[:code] = s.course
18
+ self[:organization_id] = Organization.find_by!(name: s.organization).id
21
19
  end
22
20
  end
@@ -13,12 +13,11 @@ class Challenge < Exercise
13
13
  end
14
14
 
15
15
  def extra
16
- extra_code = [guide.extra, self[:extra]].compact.join("\n")
17
- if extra_code.empty? or extra_code.end_with? "\n"
18
- extra_code
19
- else
20
- "#{extra_code}\n"
21
- end
16
+ [guide.extra, self[:extra]]
17
+ .compact
18
+ .join("\n")
19
+ .strip
20
+ .ensure_newline
22
21
  end
23
22
 
24
23
  private
@@ -32,7 +32,7 @@ class Language < ApplicationRecord
32
32
  end
33
33
 
34
34
  def import!
35
- import_from_json! Mumukit::Bridge::Runner.new(runner_url).importable_info
35
+ import_from_json! bridge.importable_info
36
36
  end
37
37
 
38
38
  def devicon
@@ -80,6 +80,10 @@ class Organization < ApplicationRecord
80
80
  errors_explanations.try { |it| it[code.to_s] } || I18n.t(advice)
81
81
  end
82
82
 
83
+ def self.accessible_as(user, role)
84
+ all.select { |it| it.public? || user.has_permission?(role, it.slug) }
85
+ end
86
+
83
87
  private
84
88
 
85
89
  def ensure_consistent_public_login
data/app/models/user.rb CHANGED
@@ -27,6 +27,8 @@ class User < ApplicationRecord
27
27
 
28
28
  after_initialize :init
29
29
 
30
+ before_validation :set_uid!
31
+
30
32
  def last_lesson
31
33
  last_guide.try(:lesson)
32
34
  end
@@ -74,7 +76,7 @@ class User < ApplicationRecord
74
76
  end
75
77
  end
76
78
 
77
- def transfer_progress_to!(another)
79
+ def copy_progress_to!(another)
78
80
  transaction do
79
81
  assignments.update_all(submitter_id: another.id)
80
82
  if another.never_submitted? || last_submission_date.try { |it| it > another.last_submission_date }
@@ -99,8 +101,27 @@ class User < ApplicationRecord
99
101
  Rails.application.message_verifier(:unsubscribe)
100
102
  end
101
103
 
104
+ def attach!(role, course)
105
+ add_permission! role, course.slug
106
+ save_and_notify!
107
+ end
108
+
109
+ def detach!(role, course)
110
+ remove_permission! role, course.slug
111
+ save_and_notify!
112
+ end
113
+
114
+ def self.create_if_necessary(user)
115
+ user[:uid] ||= user[:email]
116
+ where(uid: user[:uid]).first_or_create(user)
117
+ end
118
+
102
119
  private
103
120
 
121
+ def set_uid!
122
+ self.uid ||= email
123
+ end
124
+
104
125
  def init
105
126
  self.image_url ||= "user_shape.png"
106
127
  end
@@ -0,0 +1,11 @@
1
+ <div class="row">
2
+ <div class="col-md-12">
3
+ <div class="actions">
4
+ <% if assignment.passed? %>
5
+ <%= next_exercise_button(@exercise) %>
6
+ <% else %>
7
+ <button class="btn btn-success btn-block" data-dismiss="modal" aria-label="<%= t :retry_exercise %>"> <%= t :retry_exercise %> </button>
8
+ <% end %>
9
+ </div>
10
+ </div>
11
+ </div>
@@ -65,10 +65,4 @@
65
65
 
66
66
  <%= corollary_box @exercise unless assignment.should_retry? %>
67
67
 
68
- <div class="row">
69
- <div class="col-md-12">
70
- <div class="actions">
71
- <%= next_exercise_button(@exercise) %>
72
- </div>
73
- </div>
74
- </div>
68
+ <%= render partial: 'exercise_solutions/results_button', locals: {assignment: assignment} unless @exercise.input_kids? %>
@@ -0,0 +1,7 @@
1
+ <div class="row">
2
+ <div class="col-md-12">
3
+ <div class="actions">
4
+ <%= next_exercise_button(@exercise) %>
5
+ </div>
6
+ </div>
7
+ </div>
@@ -10,6 +10,9 @@
10
10
  <li data-target="hint" title="<%= t :need_a_hint %>"><i class="fa fa-fw fa-lightbulb-o"></i></li>
11
11
  </ul>
12
12
  <% end %>
13
- <div class="description"><%= @exercise.description_task %></div>
14
- <div class="hint" style="display: none"><%= @exercise.hint_html %></div>
13
+ <div class="mu-kids-character-speech-bubble-normal">
14
+ <div class="description"><%= @exercise.description_task %></div>
15
+ <div class="hint" style="display: none"><%= @exercise.hint_html %></div>
16
+ </div>
17
+ <div class="mu-kids-character-speech-bubble-failed" style="display: none"></div>
15
18
  </div>
@@ -2,4 +2,5 @@
2
2
 
3
3
  <%= render_custom_editor(@exercise) %>
4
4
  <%= form.hidden_field :content, id: "mu-custom-editor-value", value: @current_content %>
5
+ <%= form.hidden_field :default_content, id: "mu-custom-editor-default-value", value: @exercise.default_content %>
5
6
  <%= form.hidden_field :content_extra, id: "mu-custom-editor-extra", value: @exercise.extra %>
@@ -7,6 +7,9 @@
7
7
  <div class="actions mu-kids-submit-button">
8
8
  <%= render_submit_button(@exercise) %>
9
9
  </div>
10
+ <div class="actions mu-kids-reset-button"></div>
11
+ <img class="mu-kids-compass-rose" src="/compass_rose.svg">
12
+ <div class="mu-kids-overlay" style="display: none"></div>
10
13
  <% end %>
11
14
  <% else %>
12
15
  <p><%= t :you_must_sign_in_before_submitting %></p>
@@ -20,12 +20,12 @@
20
20
 
21
21
  <div class="mu-kids-states">
22
22
  <div class="mu-kids-state mu-state-initial mu-kids-gbs-board-initial">
23
- <strong>Tablero inicial</strong>
23
+ <strong id="mu-initial-state-text"><%= t :initial_state %></strong>
24
+ <strong id="mu-actual-state-text"><%= t :actual_state %></strong>
24
25
  <div class="mu-kids-state-image"><%= @exercise.initial_state&.html_safe %></div>
25
26
  </div>
26
- <hr>
27
27
  <div class="mu-kids-state mu-state-final">
28
- <strong>Tablero esperado</strong>
28
+ <strong><%= t :expected_state %></strong>
29
29
  <div class="mu-kids-state-image"><%= @exercise.final_state&.html_safe %></div>
30
30
  </div>
31
31
  </div>
@@ -2,9 +2,6 @@
2
2
  <div class="modal-dialog">
3
3
  <div class="modal-content">
4
4
  <div class="modal-header">
5
- <button type="button" class="close" data-dismiss="modal" aria-label="<%= t :continue_exercise %>">
6
- <span aria-hidden="true">&times;</span>
7
- </button>
8
5
  <h4 class="text-center">
9
6
  <strong><%= "#{@exercise.number}. #{@exercise.name}" %></strong>
10
7
  </h4>
@@ -20,6 +20,8 @@
20
20
  </div>
21
21
  </div>
22
22
  </div>
23
+ <div class="modal-footer">
24
+ </div>
23
25
  </div>
24
26
  </div>
25
27
  </div>
data/config/routes.rb CHANGED
@@ -49,6 +49,29 @@ Rails.application.routes.draw do
49
49
  # Organizations assets
50
50
  get '/theme_stylesheet' => 'assets#theme_stylesheet'
51
51
  get '/extension_javascript' => 'assets#extension_javascript'
52
+
53
+
54
+ namespace :api do
55
+ organization_regexp = Regexp.new Mumukit::Platform::Organization::Helpers.valid_name_regex
56
+ uid_regexp = /[^\/]+/
57
+
58
+ resources :users, only: [:create, :update], constraints: {id: uid_regexp}
59
+ resources :organizations,
60
+ only: [:index, :show, :create, :update],
61
+ constraints: {id: organization_regexp}
62
+
63
+ resources :courses, only: [:create]
64
+ constraints(uid: uid_regexp) do
65
+ '/courses/:organization/:course'.tap do |it|
66
+ post "#{it}/students" => 'students#create'
67
+ post "#{it}/students/:uid/attach" => 'students#attach'
68
+ post "#{it}/students/:uid/detach" => 'students#detach'
69
+ post "#{it}/teachers" => 'teachers#create'
70
+ post "#{it}/teachers/:uid/attach" => 'teachers#attach'
71
+ post "#{it}/teachers/:uid/detach" => 'teachers#detach'
72
+ end
73
+ end
74
+ end
52
75
  end
53
76
 
54
77
  #Rescue not found routes
@@ -0,0 +1,11 @@
1
+ class AddApiClient < ActiveRecord::Migration[5.1]
2
+ def change
3
+ create_table :api_clients do |t|
4
+ t.string :description
5
+ t.string :token
6
+ t.references :user
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -12,7 +12,8 @@ module Mumuki::Laboratory::Controllers::ResultsRendering
12
12
  guide_finished_by_solution: guide_finished_by_solution?,
13
13
  class_for_progress_list_item: class_for_progress_list_item(@exercise, true),
14
14
  html: render_results_html(assignment),
15
- title_html: render_results_title_html(assignment))
15
+ title_html: render_results_title_html(assignment),
16
+ button_html: render_results_button_html(assignment))
16
17
  end
17
18
 
18
19
  def render_results_html(assignment)
@@ -25,6 +26,11 @@ module Mumuki::Laboratory::Controllers::ResultsRendering
25
26
  locals: {assignment: assignment}
26
27
  end
27
28
 
29
+ def render_results_button_html(assignment)
30
+ render_to_string partial: 'exercise_solutions/kids_results_button',
31
+ locals: {assignment: assignment}
32
+ end
33
+
28
34
  def guide_finished_by_solution?
29
35
  !@guide_previously_done && @exercise.guide_done_for?(current_user)
30
36
  end