mumuki-laboratory 5.8.3 → 5.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/application/submission.js +25 -4
  3. data/app/controllers/application_controller.rb +4 -1
  4. data/app/controllers/concerns/organizations_controller_template.rb +3 -2
  5. data/app/controllers/exercise_solutions_controller.rb +1 -1
  6. data/app/controllers/exercises_controller.rb +3 -7
  7. data/app/controllers/login_controller.rb +6 -0
  8. data/app/helpers/contextualization_result_helper.rb +1 -1
  9. data/app/helpers/exercise_input_helper.rb +20 -5
  10. data/app/helpers/messages_helper.rb +4 -4
  11. data/app/helpers/organization_list_helper.rb +4 -0
  12. data/app/models/assignment.rb +50 -7
  13. data/app/models/concerns/contextualization.rb +3 -3
  14. data/app/models/concerns/guide_container.rb +15 -0
  15. data/app/models/concerns/submittable/submittable.rb +1 -1
  16. data/app/models/concerns/with_assignments.rb +10 -41
  17. data/app/models/concerns/with_discussions.rb +1 -1
  18. data/app/models/discussion.rb +7 -1
  19. data/app/models/exam.rb +14 -0
  20. data/app/models/exercise.rb +17 -1
  21. data/app/models/exercise/challenge.rb +1 -5
  22. data/app/models/guide.rb +1 -1
  23. data/app/models/language.rb +25 -8
  24. data/app/models/submission/submission.rb +1 -1
  25. data/app/views/discussions/_message.html.erb +1 -1
  26. data/app/views/exercise_solutions/_out_of_attempts.html.erb +6 -0
  27. data/app/views/exercise_solutions/_results.html.erb +3 -3
  28. data/app/views/exercises/_read_only.html.erb +1 -1
  29. data/app/views/layouts/_kids.html.erb +1 -1
  30. data/app/views/layouts/_organizations_listing.html.erb +1 -1
  31. data/app/views/layouts/exercise_inputs/forms/_interactive_form.html.erb +6 -6
  32. data/app/views/layouts/exercise_inputs/forms/_kids_form.html.erb +1 -1
  33. data/app/views/layouts/exercise_inputs/forms/_playground_form.html.erb +1 -1
  34. data/app/views/layouts/exercise_inputs/forms/_problem_form.html.erb +5 -5
  35. data/app/views/layouts/exercise_inputs/read_only_editors/_text.erb +3 -0
  36. data/app/views/layouts/modals/_kids_results.html.erb +1 -1
  37. data/db/migrate/20180725145801_add_submissions_caps_to_exams.rb +6 -0
  38. data/lib/mumuki/laboratory/controllers/results_rendering.rb +7 -5
  39. data/lib/mumuki/laboratory/locales/en.yml +4 -0
  40. data/lib/mumuki/laboratory/locales/es.yml +5 -0
  41. data/lib/mumuki/laboratory/locales/pt.yml +4 -0
  42. data/lib/mumuki/laboratory/mumukit/directives.rb +1 -0
  43. data/lib/mumuki/laboratory/status.rb +4 -0
  44. data/lib/mumuki/laboratory/status/discussion/discussion.rb +2 -14
  45. data/lib/mumuki/laboratory/status/submission/aborted.rb +4 -4
  46. data/lib/mumuki/laboratory/status/submission/errored.rb +4 -0
  47. data/lib/mumuki/laboratory/status/submission/failed.rb +4 -0
  48. data/lib/mumuki/laboratory/status/submission/manual_evaluation_pending.rb +1 -1
  49. data/lib/mumuki/laboratory/status/submission/passed.rb +0 -4
  50. data/lib/mumuki/laboratory/status/submission/passed_with_warnings.rb +5 -1
  51. data/lib/mumuki/laboratory/status/submission/pending.rb +4 -0
  52. data/lib/mumuki/laboratory/status/submission/running.rb +4 -0
  53. data/lib/mumuki/laboratory/status/submission/submission.rb +7 -17
  54. data/lib/mumuki/laboratory/version.rb +1 -1
  55. data/spec/controllers/discussions_controller_spec.rb +7 -0
  56. data/spec/controllers/exercise_solutions_controller_spec.rb +6 -1
  57. data/spec/controllers/messages_controller_spec.rb +7 -0
  58. data/spec/dummy/db/schema.rb +2 -0
  59. data/spec/factories/exam_factory.rb +1 -2
  60. data/spec/features/progressive_tips_spec.rb +4 -4
  61. data/spec/helpers/organization_list_helper_spec.rb +20 -0
  62. data/spec/models/assignment_spec.rb +2 -0
  63. data/spec/models/event_generation_spec.rb +6 -6
  64. data/spec/models/exercise_spec.rb +39 -32
  65. data/spec/models/guide_spec.rb +2 -2
  66. data/spec/models/interactive_spec.rb +1 -1
  67. data/spec/models/query_spec.rb +1 -2
  68. data/spec/models/question_spec.rb +1 -3
  69. data/spec/models/solution_spec.rb +33 -2
  70. data/spec/spec_helper.rb +1 -0
  71. data/vendor/assets/javascripts/codemirror-modes/php.js +236 -0
  72. metadata +14 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 625ae660ff8299245d10e105830ddcf35e14a1700f36ecad6d338cbd0bee64bd
4
- data.tar.gz: 43691be79aa7d382429e9443ed0727c19ded873284baab6c1ad8f8c9232d09b7
3
+ metadata.gz: 1aba4e2ab138f4c9e7310afc18ca35639c12d06c70980fd9d912abf24af1c937
4
+ data.tar.gz: 98afb11476a5221d39783b233ec4f7e1e05ec569c62a1c14a78a4bf138b91ab7
5
5
  SHA512:
6
- metadata.gz: ca3b02948675b7bf146dac98f74ba891ecfb6feb979853b52d7bbce804a34b350278677beac6a782d79b8399a737e9243bfcca8cb0dde1128e7f966811ad1abe
7
- data.tar.gz: bcb56b9bbac91d7600e69502432d6d176ffff4783a7c80c809a54606d8faceb56a49e232765fdd29fb18f9c52d6a1425ba73a64cf329210b2b188b0c029fc7f0
6
+ metadata.gz: 8b6e21ceb45a76cfd5eb468271b30dbfe095efb33bf3a5f01b715721a9bba2a20e8dfb76a859379bf7a495e9abe1f7cdf5d6b048f3841d2dca645360a9ee8a2b
7
+ data.tar.gz: 1b8f6d4d19ba9e689d62f7e96e2b9d5cb985517d9c29a0b4b49f594fa3c7d5cfdbbefef408994e4af6eed85578f1b8ba1659db92de5d007f7773c625b6daeef8
@@ -22,7 +22,8 @@ var mumuki = mumuki || {};
22
22
  this.submissionsErrorTemplate.show();
23
23
  animateTimeoutError(submitButton);
24
24
  },
25
- done: function () {
25
+ done: function (data, submitButton) {
26
+ submitButton.updateAttemptsLeft(data);
26
27
  mumuki.pin.scroll();
27
28
  }
28
29
  };
@@ -34,7 +35,6 @@ var mumuki = mumuki || {};
34
35
 
35
36
  SubmitButton.prototype = {
36
37
  disable: function () {
37
- this.setWaitingText();
38
38
  this.submissionControls.attr('disabled', 'disabled');
39
39
  },
40
40
  setWaitingText: function () {
@@ -47,6 +47,24 @@ var mumuki = mumuki || {};
47
47
  enable: function () {
48
48
  this.setSendText();
49
49
  this.submissionControls.removeAttr('disabled');
50
+ },
51
+ reachedMaxAttempts: function () {
52
+ return $('#attempts-left-text').attr('data-disabled') === "true";
53
+ },
54
+ updateAttemptsLeft: function (data) {
55
+ $('#attempts-left-text').replaceWith(data['remaining_attempts_html']);
56
+ this.checkAttemptsLeft();
57
+ },
58
+ preventSubmission: function () {
59
+ this.disable();
60
+ this.submitButton.on('click', function (e) {
61
+ e.preventDefault();
62
+ })
63
+ },
64
+ checkAttemptsLeft: function () {
65
+ if (this.reachedMaxAttempts()) {
66
+ this.preventSubmission();
67
+ }
50
68
  }
51
69
  };
52
70
 
@@ -58,6 +76,7 @@ var mumuki = mumuki || {};
58
76
 
59
77
  var btnSubmit = $('.btn-submit');
60
78
  var submissionControl = $('.submission_control');
79
+
61
80
  var submitButton = new SubmitButton(btnSubmit, submissionControl);
62
81
 
63
82
  var bridge = new mumuki.bridge.Laboratory;
@@ -65,6 +84,7 @@ var mumuki = mumuki || {};
65
84
  btnSubmit.on('click', function (e) {
66
85
  e.preventDefault();
67
86
  submitButton.disable();
87
+ submitButton.setWaitingText();
68
88
  resultsBox.waiting();
69
89
 
70
90
  mumuki.editor.syncContent();
@@ -74,12 +94,13 @@ var mumuki = mumuki || {};
74
94
  resultsBox.success(data, submitButton);
75
95
  }).fail(function () {
76
96
  resultsBox.error(submitButton);
77
- }).always(function () {
97
+ }).always(function (data) {
78
98
  $(document).renderMuComponents();
79
- resultsBox.done();
99
+ resultsBox.done(data, submitButton);
80
100
  });
81
101
  });
82
102
 
103
+ submitButton.checkAttemptsLeft();
83
104
  });
84
105
 
85
106
  function getContent(){
@@ -1,6 +1,8 @@
1
1
  class ApplicationController < ActionController::Base
2
2
  Mumukit::Login.configure_controller! self
3
3
 
4
+ protect_from_forgery with: :exception
5
+
4
6
  include Mumuki::Laboratory::Controllers::CurrentOrganization
5
7
  include Mumukit::Login::AuthenticationHelpers
6
8
 
@@ -41,7 +43,8 @@ class ApplicationController < ActionController::Base
41
43
 
42
44
  # ensures contents are accessible to current user
43
45
  def validate_accessible!
44
- accessible_subject.validate_accessible_for!(current_user) unless (current_user? && current_user.teacher_here?)
46
+ return if current_user&.teacher_here?
47
+ accessible_subject.validate_accessible_for! current_user
45
48
  end
46
49
 
47
50
  # required by Mumukit::Login
@@ -28,8 +28,9 @@ module OrganizationsControllerTemplate
28
28
  :logo_url, :banner_url, :open_graph_image_url, :favicon_url, :breadcrumb_image_url,
29
29
  :raise_hand_enabled, :report_issue_enabled, :forum_enabled,
30
30
  :feedback_suggestions_enabled, :public, :immersive,
31
- :theme_stylesheet, :extension_javascript,
32
- :terms_of_service, :community_link, login_methods: [])
31
+ :theme_stylesheet, :extension_javascript, :terms_of_service,
32
+ :community_link, :login_provider, :embeddable,
33
+ login_methods: [])
33
34
  .tap { |it| it.merge!(book: Book.find_by!(slug: it[:book])) if it[:book] }
34
35
  end
35
36
  end
@@ -7,7 +7,7 @@ class ExerciseSolutionsController < AjaxController
7
7
  before_action :validate_accessible!, only: :create
8
8
 
9
9
  def create
10
- assignment = @exercise.submit_solution!(current_user, solution_params)
10
+ assignment = @exercise.try_submit_solution!(current_user, solution_params)
11
11
  render_results_json assignment, status: assignment.status
12
12
  end
13
13
 
@@ -3,7 +3,6 @@ class ExercisesController < ApplicationController
3
3
  include Mumuki::Laboratory::Controllers::ExerciseSeed
4
4
 
5
5
  before_action :set_guide!, only: :show
6
- before_action :set_default_content!, only: :show, if: :current_user?
7
6
  before_action :set_assignment!, only: :show, if: :current_user?
8
7
  before_action :validate_accessible!, only: :show
9
8
  before_action :start!, only: :show
@@ -31,14 +30,11 @@ class ExercisesController < ApplicationController
31
30
  @exercise.navigable_parent.start! current_user
32
31
  end
33
32
 
34
- def set_default_content!
35
- @files = @exercise.files_for(current_user)
36
- @current_content = @exercise.current_content_for(current_user)
37
- @default_content = @exercise.default_content_for(current_user)
38
- end
39
-
40
33
  def set_assignment!
41
34
  @assignment = @exercise.assignment_for(current_user)
35
+ @files = @assignment.files
36
+ @current_content = @assignment.current_content
37
+ @default_content = @assignment.default_content
42
38
  end
43
39
 
44
40
  def set_guide!
@@ -1,8 +1,14 @@
1
1
  class LoginController < ApplicationController
2
2
  Mumukit::Login.configure_login_controller! self
3
3
 
4
+ skip_before_action :verify_authenticity_token, if: lambda { Rails.env.development? }
5
+
4
6
  private
5
7
 
8
+ def organization_name
9
+ params[:organization] || super
10
+ end
11
+
6
12
  def login_failure!
7
13
  redirect_to root_path, alert: request.params['message']
8
14
  end
@@ -23,7 +23,7 @@ module ContextualizationResultHelper
23
23
 
24
24
  def render_test_results(contextualization)
25
25
  if contextualization.test_results.present?
26
- render partial: 'layouts/test_results', locals: { contextualization: contextualization}
26
+ render partial: 'layouts/test_results', locals: { contextualization: contextualization }
27
27
  else
28
28
  render partial: 'layouts/result', locals: { contextualization: contextualization }
29
29
  end
@@ -47,15 +47,30 @@ module ExerciseInputHelper
47
47
  !assignment.passed? && organization.ask_for_help_enabled?
48
48
  end
49
49
 
50
- def render_submit_button(exercise)
51
- options = submit_button_options(exercise)
50
+ def render_submit_button(assignment)
51
+ options = submit_button_options(assignment.exercise)
52
52
  text = t(options.t) if options.t.present?
53
53
  waiting_text = t(options.waiting_t) if options.waiting_t.present?
54
- %Q{<#{options.tag} for="#{options.for}"
54
+ %Q{
55
+ <div class="btn-submit-container">
56
+ <#{options.tag} for="#{options.for}"
55
57
  class="btn btn-success btn-block btn-submit #{options.classes}"
56
58
  data-waiting="#{waiting_text}">
57
- #{fa_icon options.fa_icon, text: text}
58
- </#{options.tag}>}.html_safe
59
+ #{fa_icon options.fa_icon}
60
+ #{text} #{remaining_attempts_text(assignment)}
61
+ </#{options.tag}>
62
+ </div>
63
+ }.html_safe
64
+ end
65
+
66
+ def remaining_attempts_text(assignment)
67
+ if assignment.limited?
68
+ %Q{
69
+ <span id="attempts-left-text" data-disabled="#{!assignment.attempts_left?}">
70
+ (#{t(:attempts_left, count: assignment.attempts_left)})
71
+ </span>
72
+ }
73
+ end
59
74
  end
60
75
 
61
76
  def render_custom_editor(exercise, read_only=false)
@@ -4,19 +4,19 @@ module MessagesHelper
4
4
  end
5
5
 
6
6
  def hidden_pending(assignment)
7
- assignment&.pending_messages? ? '' : 'hidden'
7
+ assignment.pending_messages? ? '' : 'hidden'
8
8
  end
9
9
 
10
10
  def disabled_submit_class(assignment)
11
- assignment&.pending_messages? ? 'disabled' : ''
11
+ assignment.pending_messages? ? 'disabled' : ''
12
12
  end
13
13
 
14
14
  def pending_messages_filter(assignment)
15
- 'pending-messages-filter' if assignment&.pending_messages?
15
+ 'pending-messages-filter' if assignment.pending_messages?
16
16
  end
17
17
 
18
18
  def read_messages_caption(assignment)
19
- assignment&.pending_messages? ? :read_messages : :exit
19
+ assignment.pending_messages? ? :read_messages : :exit
20
20
  end
21
21
 
22
22
  def sender_class(message)
@@ -2,4 +2,8 @@ module OrganizationListHelper
2
2
  def organizations_for(user)
3
3
  (user.accessible_organizations + [Organization.central]).uniq.compact
4
4
  end
5
+
6
+ def organization_switch_url(organization)
7
+ organization.url_for(controller_name == 'users' ? root_path : request.path)
8
+ end
5
9
  end
@@ -2,17 +2,21 @@ class Assignment < ApplicationRecord
2
2
  include Contextualization
3
3
  include WithMessages
4
4
 
5
+ markdown_on :extra_preview
6
+
5
7
  belongs_to :exercise
6
8
  has_one :guide, through: :exercise
7
- has_many :messages, -> { where.not(submission_id: nil).order(date: :desc) }, foreign_key: :submission_id, primary_key: :submission_id
9
+ has_many :messages, -> { where.not(submission_id: nil).order(date: :desc) }, foreign_key: :submission_id, primary_key: :submission_id, dependent: :delete_all
8
10
 
9
11
  belongs_to :submitter, class_name: 'User'
10
12
 
11
13
  validates_presence_of :exercise, :submitter
12
14
 
13
- delegate :language, :name, to: :exercise
15
+ delegate :language, :name, :navigable_parent,
16
+ :limited?, :input_kids?, :choice?, to: :exercise
14
17
 
15
18
  alias_attribute :status, :submission_status
19
+ alias_attribute :attempts_count, :attemps_count
16
20
 
17
21
  scope :by_exercise_ids, -> (exercise_ids) {
18
22
  where(exercise_id: exercise_ids) if exercise_ids
@@ -22,6 +26,11 @@ class Assignment < ApplicationRecord
22
26
  joins(:submitter).where('users.name' => usernames) if usernames
23
27
  }
24
28
 
29
+ defaults do
30
+ self.query_results = []
31
+ self.expectation_results = []
32
+ end
33
+
25
34
  def evaluate_manually!(teacher_evaluation)
26
35
  update! status: teacher_evaluation[:status], manual_evaluation_comment: teacher_evaluation[:manual_evaluation]
27
36
  end
@@ -61,11 +70,15 @@ class Assignment < ApplicationRecord
61
70
  end
62
71
 
63
72
  def test
64
- exercise.test_for submitter
73
+ exercise.test && language.interpolate_references_for(self, exercise.test)
65
74
  end
66
75
 
67
76
  def extra
68
- exercise.extra_for submitter
77
+ exercise.extra && language.interpolate_references_for(self, exercise.extra)
78
+ end
79
+
80
+ def extra_preview
81
+ Mumukit::ContentType::Markdown.highlighted_code(language.name, extra)
69
82
  end
70
83
 
71
84
  def run_update!
@@ -104,7 +117,6 @@ class Assignment < ApplicationRecord
104
117
  end
105
118
 
106
119
  def as_platform_json
107
- navigable_parent = exercise.navigable_parent
108
120
  as_json(except: [:exercise_id, :submission_id, :id, :submitter_id, :solution, :created_at, :updated_at, :submission_status],
109
121
  include: {
110
122
  guide: {
@@ -136,8 +148,39 @@ class Assignment < ApplicationRecord
136
148
  @tips ||= exercise.assist_with(self)
137
149
  end
138
150
 
139
- def increment_attemps!
140
- self.attemps_count += 1 unless passed?
151
+ def increment_attempts!
152
+ self.attempts_count += 1 if should_retry?
153
+ end
154
+
155
+ def attempts_left
156
+ navigable_parent.attempts_left_for(self)
157
+ end
158
+
159
+ # Tells wether the submitter of this
160
+ # assignment can keep on sending submissions
161
+ # which is true for non limited or for assignments
162
+ # that have not reached their submissions limit
163
+ def attempts_left?
164
+ !limited? || attempts_left > 0
165
+ end
166
+
167
+ def current_content
168
+ solution || default_content
169
+ end
170
+
171
+ def current_content_at(index)
172
+ exercise.sibling_at(index).assignment_for(submitter).current_content
173
+ end
174
+
175
+ def default_content
176
+ @default_content ||= language.interpolate_references_for(self, exercise.default_content)
177
+ end
178
+
179
+ def files
180
+ language
181
+ .directives_sections
182
+ .split_sections(current_content)
183
+ .map { |name, content| Mumuki::Laboratory::File.new name, content }
141
184
  end
142
185
 
143
186
  private
@@ -28,7 +28,7 @@ module Contextualization
28
28
 
29
29
  delegate :visible_success_output?, to: :exercise
30
30
  delegate :output_content_type, to: :language
31
- delegate :should_retry?, :to_submission_status, :passed?, :aborted?, to: :submission_status
31
+ delegate :should_retry?, :to_submission_status, *Mumuki::Laboratory::Status::Submission.test_selectors, to: :submission_status
32
32
  delegate :inspection_keywords, to: :exercise
33
33
  end
34
34
 
@@ -47,11 +47,11 @@ module Contextualization
47
47
  end
48
48
 
49
49
  def results_visible?
50
- (visible_success_output? || should_retry?) && !exercise.choices?
50
+ (visible_success_output? || !passed?) && !exercise.choices? && !manual_evaluation_pending?
51
51
  end
52
52
 
53
53
  def result_preview
54
- result.truncate(100) if should_retry?
54
+ result.truncate(100) unless passed?
55
55
  end
56
56
 
57
57
  def result_html
@@ -25,6 +25,21 @@ module GuideContainer
25
25
  def validate_accessible_for!(user)
26
26
  end
27
27
 
28
+ # Tells the number of remaning
29
+ # attemps for a given assignment that belongs to this
30
+ # container, or `nil`, if this container does not impose
31
+ # any limit to the number of submissions
32
+ def attempts_left_for(assignment)
33
+ nil
34
+ end
35
+
36
+ # Tells if this guide container
37
+ # imposes any kind of limit to the number of submission
38
+ # to its assignments
39
+ def limited?
40
+ false
41
+ end
42
+
28
43
  def timed?
29
44
  false
30
45
  end
@@ -4,7 +4,7 @@ module Submittable
4
4
  end
5
5
 
6
6
  def find_assignment_and_submit!(user, submission)
7
- assignment = find_or_init_assignment_for user
7
+ assignment = assignment_for user
8
8
  results = submission.run! assignment, evaluation_class.new
9
9
  [assignment, results]
10
10
  end
@@ -1,63 +1,32 @@
1
1
  module WithAssignments
2
2
  extend ActiveSupport::Concern
3
3
 
4
+ # TODO we must avoid _for(user) methods when they
5
+ # are hidden the assignment object, since an assignment already encapsulates
6
+ # the exercise-user pair, and many times they impose a performance hit,
7
+ # since in the normal scenario the assignment object already exists
8
+
4
9
  included do
5
10
  has_many :assignments, dependent: :destroy
6
11
  end
7
12
 
8
- def current_content_for(user)
9
- assignment_for(user)&.solution || default_content_for(user)
10
- end
11
-
12
- def files_for(user)
13
- language
14
- .directives_sections
15
- .split_sections(current_content_for user)
16
- .map { |name, content| Mumuki::Laboratory::File.new name, content }
17
- end
18
-
19
- def interpolate_for(user, field)
20
- language.interpolate(field, user.interpolations, lambda { |content| replace_content_reference(user, content) })
21
- end
22
-
23
- def default_content_for(user)
24
- interpolate_for user, default_content || ''
25
- end
26
-
27
- def extra_for(user)
28
- interpolate_for user, extra
29
- end
30
-
31
- def test_for(user)
32
- test && interpolate_for(user, test)
33
- end
34
-
35
- def replace_content_reference(user, interpolee)
36
- case interpolee
37
- when /previousContent|previousSolution/
38
- previous.current_content_for(user)
39
- when /(solution|content)\[(-?\d*)\]/
40
- sibling_at($2.to_i).current_content_for(user)
41
- end
42
- end
43
-
44
13
  def messages_for(user)
45
- assignment_for(user)&.messages || []
14
+ assignment_for(user).messages
46
15
  end
47
16
 
48
17
  def has_messages_for?(user)
49
18
  messages_for(user).present?
50
19
  end
51
20
 
52
- def assignment_for(user)
21
+ def find_assignment_for(user)
53
22
  assignments.find_by(submitter: user)
54
23
  end
55
24
 
56
25
  def status_for(user)
57
- assignment_for(user).defaulting(Mumuki::Laboratory::Status::Submission::Pending, &:status) if user
26
+ assignment_for(user).status if user
58
27
  end
59
28
 
60
- def find_or_init_assignment_for(user)
61
- assignment_for(user) || user.assignments.build(exercise: self)
29
+ def assignment_for(user)
30
+ find_assignment_for(user) || user.assignments.build(exercise: self)
62
31
  end
63
32
  end