diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index b74b13633..b8cdc66ea 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -785,14 +785,11 @@ var CodeOceanEditor = { case 'not_for_all_users_submitted': this.showNotForAllUsersSubmittedMessage(output.failed_users); break; - case 'too_late_and_not_for_all_users_submitted': - this.showTooLateAndNotForAllUsersSubmittedMessage(output.score_sent, output.failed_users); - break; case 'scoring_too_late': this.showScoringTooLateMessage(output.score_sent); break; - case 'full_score_reached': - this.showScoringFullScoreMessage(output.url); + case 'exercise_finished': + this.showExerciseFinishedMessage(output.url); break; } }, @@ -874,25 +871,18 @@ var CodeOceanEditor = { }); }, - showTooLateAndNotForAllUsersSubmittedMessage: function (score_sent, failed_users) { - $.flash.warning({ - icon: ['fa-solid', 'fa-triangle-exclamation'], - text: I18n.t('exercises.submit.too_late_and_not_for_all_users_submitted', {score_sent: score_sent, user: failed_users}) - }); - }, - showScoringTooLateMessage: function (score_sent) { - $.flash.warning({ + $.flash.info({ icon: ['fa-solid', 'fa-triangle-exclamation'], text: I18n.t('exercises.submit.too_late', {score_sent: score_sent}) }); }, - showScoringFullScoreMessage: function (url) { + showExerciseFinishedMessage: function (url) { $.flash.success({ showPermanent: true, icon: ['fa-solid', 'fa-graduation-cap'], - text: I18n.t('exercises.submit.full_score', {url: url}) + text: I18n.t('exercises.submit.exercise_finished', {url: url}) }); }, diff --git a/app/controllers/concerns/lti.rb b/app/controllers/concerns/lti.rb index 30735de65..9cc2a97e4 100644 --- a/app/controllers/concerns/lti.rb +++ b/app/controllers/concerns/lti.rb @@ -123,47 +123,66 @@ def send_scores(submission) raise Error.new("Score #{submission.normalized_score} must be between 0 and #{MAXIMUM_SCORE}!") end - submission.users.map {|user| send_score_for submission, user } + # Prepare score to be sent + score = submission.normalized_score + deadline = :none + if submission.before_deadline? + # Keep the full score + deadline = :before_deadline + elsif submission.within_grace_period? + # Reduce score by 20% + score *= 0.8 + deadline = :within_grace_period + elsif submission.after_late_deadline? + # Reduce score by 100% + score *= 0.0 + deadline = :after_late_deadline + end + + # Actually send the score for all users + detailed_results = submission.users.map {|user| send_score_for submission, user, score } + + # Prepare return value + erroneous_results = detailed_results.filter {|result| ERROR_STATUS.include?(result[:status]) } + statistics = { + all: detailed_results, + success: detailed_results - erroneous_results, + error: erroneous_results, + } + + { + users: statistics.transform_values {|value| value.pluck(:user) }, + score: {original: submission.normalized_score, sent: score}, + deadline:, + detailed_results:, + } end private :send_scores - def send_score_for(submission, user) - if user.external_user? && user.consumer - lti_parameter = user.lti_parameters.find_by(exercise: submission.exercise, study_group: submission.study_group) - provider = build_tool_provider(consumer: user.consumer, parameters: lti_parameter&.lti_parameters) - end - - if provider.nil? - {status: 'error', user: user.displayname} - elsif provider.outcome_service? - Sentry.set_extras({ - provider: provider.inspect, - score: submission.normalized_score, - lti_parameter: lti_parameter.inspect, - session: session.to_hash, - exercise_id: submission.exercise_id, - }) - normalized_lti_score = submission.normalized_score - if submission.before_deadline? - # Keep the full score - elsif submission.within_grace_period? - # Reduce score by 20% - normalized_lti_score *= 0.8 - elsif submission.after_late_deadline? - # Reduce score by 100% - normalized_lti_score *= 0.0 - end - - begin - response = provider.post_replace_result!(normalized_lti_score) - {code: response.response_code, message: response.post_response.body, status: response.code_major, score_sent: normalized_lti_score, user: user.displayname} - rescue IMS::LTI::XMLParseError, Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET, SocketError, EOFError - # A parsing error might happen if the LTI provider is down and doesn't return a valid XML response - {status: 'error', user: user.displayname} - end - else - {status: 'unsupported', user: user.displayname} + def send_score_for(submission, user, score) + return {status: 'error', user:} unless user.external_user? && user.consumer + + lti_parameter = user.lti_parameters.find_by(exercise: submission.exercise, study_group: submission.study_group) + provider = build_tool_provider(consumer: user.consumer, parameters: lti_parameter&.lti_parameters) + return {status: 'error', user:} if provider.nil? + return {status: 'unsupported', user:} unless provider.outcome_service? + + Sentry.set_extras({ + provider: provider.inspect, + normalized_score: submission.normalized_score, + score:, + lti_parameter: lti_parameter.inspect, + session: defined?(session) ? session.to_hash : nil, + exercise_id: submission.exercise_id, + }) + + begin + response = provider.post_replace_result!(score) + {code: response.response_code, message: response.post_response.body, status: response.code_major, user:} + rescue IMS::LTI::XMLParseError, Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET, SocketError, EOFError + # A parsing error might happen if the LTI provider is down and doesn't return a valid XML response + {status: 'error', user:} end end diff --git a/app/controllers/concerns/redirect_behavior.rb b/app/controllers/concerns/redirect_behavior.rb index 7c69b2c35..ce9ca7ca8 100644 --- a/app/controllers/concerns/redirect_behavior.rb +++ b/app/controllers/concerns/redirect_behavior.rb @@ -24,7 +24,7 @@ def redirect_after_submit rfc = @submission.own_unsolved_rfc(current_user) if rfc # set a message that informs the user that his own RFC should be closed. - flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_own_rfc') + flash[:notice] = I18n.t('exercises.submit.exercise_finished_redirect_to_own_rfc') flash.keep(:notice) respond_to do |format| @@ -38,7 +38,7 @@ def redirect_after_submit rfc = @submission.unsolved_rfc(current_user) unless rfc.nil? || @embed_options[:disable_redirect_to_rfcs] || @embed_options[:disable_rfc] # set a message that informs the user that his score was perfect and help in RFC is greatly appreciated. - flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_rfc') + flash[:notice] = I18n.t('exercises.submit.exercise_finished_redirect_to_rfc') flash.keep(:notice) # increase counter 'times_featured' in rfc diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index d888f890a..6ea22535e 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -279,20 +279,18 @@ def score # To enable hints when scoring a submission, uncomment the next line: # send_hints(client_socket, StructuredError.where(submission: @submission)) - # Submit the score - if @submission.users.map {|user| lti_outcome_service?(@submission.exercise, user, @submission.study_group_id) }.any? - url = finalize_submission_path(@submission) - submit_info = transmit_lti_score - submit_info&.fetch(:status, [])&.each do |s| + # The own score can only be transmitted when the lti_outcome_service is available + # if it is not available the user also does not send the score for the programming group members + if lti_outcome_service?(@submission.exercise, @current_user, @submission.study_group_id) + transmit_lti_score(client_socket) + else + if @submission.score == @submission.exercise.maximum_score client_socket&.send_data({ cmd: :status, - status: s, - url:, - score_sent: submit_info[:score_sent], - failed_users: submit_info[:failed_users], + status: :exercise_finished, + url: finalize_submission_path(@submission), }.to_json) end - else finalize end rescue Runner::Error::RunnerInUse => e @@ -509,39 +507,42 @@ def set_testrun } end - def transmit_lti_score - responses = send_scores(@submission) - failed_users = [] - too_late = false - status = [] - score_sent = nil - - responses.each do |response| - if Lti::ERROR_STATUS.include? response[:status] - failed_users << response[:user] - elsif response[:score_sent] != @submission.normalized_score # the score was sent successfully, but received too late - too_late = true - score_sent = response[:score_sent] - end + def check_scoring_too_late(submit_info, client_socket) + if submit_info[:deadline] == :within_grace_period || submit_info[:deadline] == :after_late_deadline + client_socket&.send_data({ + cmd: :status, + status: :scoring_too_late, + score_sent: submit_info[:score][:sent], + }.to_json) end + end - if failed_users.size == responses.size # all submissions failed - status.push(:scoring_failure) - {status:} - elsif too_late & failed_users.size.positive? # submissions are too late and at least one submission failed - status.push(:too_late_and_not_for_all_users_submitted) - failed_users = failed_users.join(', ') - {status:, score_sent:, failed_users:} - elsif failed_users.size.positive? # at least one submission failed - status.push(:not_for_all_users_submitted) - failed_users = failed_users.join(', ') - {status:, failed_users:} - elsif too_late # submissions are too late - status.push(:scoring_too_late) - {status:, score_sent:} - elsif @submission.score == @submission.exercise.maximum_score - status.push(:full_score_reached) - {status:} + def transmit_lti_score(client_socket) + submit_info = send_scores(@submission) + + if (submit_info[:users][:all].count == submit_info[:users][:success].count) & (submit_info[:score][:original] == BigDecimal('1.0')) + client_socket&.send_data({ + cmd: :status, + status: :exercise_finished, + url: finalize_submission_path(@submission), + }.to_json) + + check_scoring_too_late(submit_info, client_socket) + elsif submit_info[:users][:all].count == submit_info[:users][:error].count + client_socket&.send_data({ + cmd: :status, + status: :scoring_failure, + }.to_json) + elsif submit_info[:users][:all].count != submit_info[:users][:success].count + failed_users = submit_info[:users][:error].map(&:displayname).join(', ') + + client_socket&.send_data({ + cmd: :status, + status: :not_for_all_users_submitted, + failed_users:, + }.to_json) + + check_scoring_too_late(submit_info, client_socket) end end diff --git a/config/locales/de.yml b/config/locales/de.yml index 5194fb88e..ab194c934 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -558,10 +558,9 @@ de: submit: failure: Die Bewertung wurde erfolgreich durchgeführt, aber beim Übermitteln Ihrer Punktzahl ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut. too_late: Ihre Abgabe wurde erfolgreich gespeichert, ging jedoch nach der Abgabefrist ein, sodass nur %{score_sent} Punkte übertragen wurden. - too_late_and_not_for_all_users_submitted: "Die Punkteübertragung war nur teilweise erfolgreich. Ihre Abgabe wurde erfolgreich gespeichert, ging jedoch nach der Abgabefrist ein, sodass nur %{score_sent} Punkte übertragen wurden. Für %{user} konnten die Punkte nicht übertragen werden. Diese Person(en) sollte die Aufgabe über die e-Learning Platform erneut öffnen und anschließend die Punkte selbst übermitteln." - full_score: Herzlichen Glückwunsch! Sie haben die maximale Punktzahl für diese Aufgabe erreicht. Klicken Sie hier, um die Aufgabe jetzt abzuschließen. - full_score_redirect_to_rfc: Herzlichen Glückwunsch! Sie haben die maximale Punktzahl für diese Aufgabe an den Kurs übertragen. Ein anderer Teilnehmer hat eine Frage zu der von Ihnen gelösten Aufgabe. Er würde sich sicherlich sehr über ihre Hilfe und Kommentare freuen. - full_score_redirect_to_own_rfc: Herzlichen Glückwunsch! Sie haben die maximale Punktzahl für diese Aufgabe an den Kurs übertragen. Ihre Frage ist damit wahrscheinlich gelöst? Falls ja, fügen Sie doch den entscheidenden Kniff als Antwort hinzu und markieren die Frage als gelöst, bevor sie das Fenster schließen. + exercise_finished: Herzlichen Glückwunsch! Sie haben die Aufgabe vollständig gelöst. Klicken Sie hier, um die Aufgabe jetzt abzuschließen. + exercise_finished_redirect_to_rfc: Herzlichen Glückwunsch! Sie haben die Aufgabe vollständig gelöst und die Punkte übertragen. Ein anderer Teilnehmer hat eine Frage zu der von Ihnen gelösten Aufgabe. Er würde sich sicherlich sehr über Ihre Hilfe und Kommentare freuen. + exercise_finished_redirect_to_own_rfc: Herzlichen Glückwunsch! Sie haben die Aufgabe vollständig gelöst und die Punkte übertragen. Ihre Frage ist damit wahrscheinlich gelöst? Falls ja, fügen Sie doch den entscheidenden Kniff als Antwort hinzu und markieren die Frage als gelöst, bevor Sie das Fenster schließen. warning_not_for_all_users_submitted: "Die Punkteübertragung war nur teilweise erfolgreich. Die Punkte konnten nicht für %{user} übertragen werden. Diese Person(en) sollte die Aufgabe über die e-Learning Platform erneut öffnen und anschließend die Punkte selbst übermitteln." study_group_dashboard: live_dashboard: Live Dashboard diff --git a/config/locales/en.yml b/config/locales/en.yml index fccf3f946..8220b8615 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -558,10 +558,9 @@ en: submit: failure: The scoring was performed successfully but an error occurred while transmitting your score. Please try again later. too_late: Your submission was saved successfully but was received after the deadline, so that only %{score_sent} points were transmitted. - too_late_and_not_for_all_users_submitted: "The transmission of points was only partially successful. Your submission was saved successfully but was received after the deadline, so that only %{score_sent} points were transmitted. For %{user} the score was not transmitted. The user(s) should reopen the exercise via the e-learning platform and then try to submit the points themselves." - full_score: Congratulations! You have achieved the highest possible score for this exercise. Please click here to finish the exercise now. - full_score_redirect_to_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Another participant has a question concerning the exercise you just solved. Your help and comments will be greatly appreciated! - full_score_redirect_to_own_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Your question concerning the exercise is solved? If so, please share the essential insight with your fellows and mark the question as solved, before you close this window! + exercise_finished: Congratulations! You have completely solved this exercise. Please click here to finish the exercise now. + exercise_finished_redirect_to_rfc: Congratulations! You have completely solved this exercise and submitted the points. Another participant has a question concerning the exercise you just solved. Your help and comments will be greatly appreciated! + exercise_finished_redirect_to_own_rfc: Congratulations! You have completely solved this exercise and submitted the points. Your question concerning the exercise is solved? If so, please share the essential insight with your fellows and mark the question as solved, before you close this window! warning_not_for_all_users_submitted: "The transmission of points was only partially successful. The score was not transmitted for %{user}. The user(s) should reopen the exercise via the e-learning platform and then try to submit the points themselves." study_group_dashboard: live_dashboard: Live Dashboard