Send score for all members of a programming group

This commit is contained in:
kiragrammel
2023-08-10 17:11:15 +02:00
committed by Sebastian Serth
parent 2fb8def1d0
commit e2baa2ee55
12 changed files with 77 additions and 47 deletions

View File

@ -211,10 +211,13 @@ CodeOceanEditorSubmissions = {
Turbolinks.visit(response.redirect); Turbolinks.visit(response.redirect);
} else if (response.status === 'container_depleted') { } else if (response.status === 'container_depleted') {
this.showContainerDepletedMessage(); this.showContainerDepletedMessage();
} else if (response.message) { } else {
$.flash.danger({ for (let [type, text] of Object.entries(response)) {
text: response.message $.flash[type]({
}); text: text,
showPermanent: true // We might display a very long text message!
})
}
} }
this.initializeEventHandlers(); this.initializeEventHandlers();
}) })

View File

@ -18,6 +18,7 @@ class ApplicationController < ActionController::Base
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
rescue_from ActionController::InvalidAuthenticityToken, with: :render_csrf_error rescue_from ActionController::InvalidAuthenticityToken, with: :render_csrf_error
add_flash_types :danger, :warning, :info, :success
def current_user def current_user
@current_user ||= find_or_login_current_user&.store_current_study_group_id(session[:study_group_id]) @current_user ||= find_or_login_current_user&.store_current_study_group_id(session[:study_group_id])

View File

@ -9,6 +9,7 @@ module Lti
MAXIMUM_SCORE = 1 MAXIMUM_SCORE = 1
MAXIMUM_SESSION_AGE = 60.minutes MAXIMUM_SESSION_AGE = 60.minutes
SESSION_PARAMETERS = %w[launch_presentation_return_url lis_outcome_service_url lis_result_sourcedid].freeze SESSION_PARAMETERS = %w[launch_presentation_return_url lis_outcome_service_url lis_result_sourcedid].freeze
ERROR_STATUS = %w[error unsupported].freeze
def build_tool_provider(options = {}) def build_tool_provider(options = {})
if options[:consumer] && options[:parameters] if options[:consumer] && options[:parameters]
@ -135,21 +136,27 @@ module Lti
private :return_to_consumer private :return_to_consumer
def send_score(submission) def send_scores(submission)
unless (0..MAXIMUM_SCORE).cover?(submission.normalized_score) unless (0..MAXIMUM_SCORE).cover?(submission.normalized_score)
raise Error.new("Score #{submission.normalized_score} must be between 0 and #{MAXIMUM_SCORE}!") raise Error.new("Score #{submission.normalized_score} must be between 0 and #{MAXIMUM_SCORE}!")
end end
if submission.contributor.consumer submission.users.map {|user| send_score_for submission, user }
lti_parameter = LtiParameter.where(consumers_id: submission.contributor.consumer.id, end
external_users_id: submission.contributor_id,
private :send_scores
def send_score_for(submission, user)
if user.consumer
lti_parameter = LtiParameter.where(consumers_id: user.consumer.id,
external_users_id: user.id,
exercises_id: submission.exercise_id).last exercises_id: submission.exercise_id).last
provider = build_tool_provider(consumer: submission.contributor.consumer, parameters: lti_parameter.lti_parameters) provider = build_tool_provider(consumer: user.consumer, parameters: lti_parameter&.lti_parameters)
end end
if provider.nil? if provider.nil?
{status: 'error'} {status: 'error', user: user.displayname}
elsif provider.outcome_service? elsif provider.outcome_service?
Sentry.set_extras({ Sentry.set_extras({
provider: provider.inspect, provider: provider.inspect,
@ -171,17 +178,17 @@ module Lti
begin begin
response = provider.post_replace_result!(normalized_lti_score) 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} {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 rescue IMS::LTI::XMLParseError, Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET, SocketError
# A parsing error might happen if the LTI provider is down and doesn't return a valid XML response # A parsing error might happen if the LTI provider is down and doesn't return a valid XML response
{status: 'error'} {status: 'error', user: user.displayname}
end end
else else
{status: 'unsupported'} {status: 'unsupported', user: user.displayname}
end end
end end
private :send_score private :send_score_for
def set_current_user def set_current_user
@current_user = ExternalUser.find_or_create_by(consumer_id: @consumer.id, external_id: @provider.user_id) @current_user = ExternalUser.find_or_create_by(consumer_id: @consumer.id, external_id: @provider.user_id)

View File

@ -536,25 +536,39 @@ class ExercisesController < ApplicationController
Rails.logger.debug { "Runner error while submitting submission #{@submission.id}: #{e.message}" } Rails.logger.debug { "Runner error while submitting submission #{@submission.id}: #{e.message}" }
respond_to do |format| respond_to do |format|
format.html { redirect_to(implement_exercise_path(@submission.exercise)) } format.html { redirect_to(implement_exercise_path(@submission.exercise)) }
format.json { render(json: {message: I18n.t('exercises.editor.depleted'), status: :container_depleted}) } format.json { render(json: {danger: I18n.t('exercises.editor.depleted'), status: :container_depleted}) }
end end
end end
def transmit_lti_score def transmit_lti_score
response = send_score(@submission) responses = send_scores(@submission)
messages = {}
failed_users = []
if response[:status] == 'success' responses.each do |response|
if response[:score_sent] != @submission.normalized_score if Lti::ERROR_STATUS.include? response[:status]
# Score has been reduced due to the passed deadline failed_users << response[:user]
flash.now[:warning] = I18n.t('exercises.submit.too_late') elsif response[:score_sent] != @submission.normalized_score # the score was sent successfully, but received too late
flash.keep(:warning) messages[:warning] = I18n.t('exercises.submit.too_late')
end end
redirect_after_submit end
if failed_users.size == responses.size # all submissions failed
messages[:danger] = I18n.t('exercises.submit.failure')
elsif failed_users.size.positive? # at least one submission failed
messages[:warning] = [[messages[:warning]], I18n.t('exercises.submit.warning_not_for_all_users_submitted', user: failed_users.join(', '))].join('<br><br>')
messages[:warning] = "#{messages[:warning]}\n\n#{I18n.t('exercises.submit.warning_not_for_all_users_submitted', user: failed_users.join(', '))}".strip
else else
respond_to do |format| messages.each do |type, message_text|
format.html { redirect_to(implement_exercise_path(@submission.exercise, alert: I18n.t('exercises.submit.failure'))) } flash.now[type] = message_text
format.json { render(json: {message: I18n.t('exercises.submit.failure')}, status: :service_unavailable) } flash.keep(type)
end end
return redirect_after_submit
end
respond_to do |format|
format.html { redirect_to(implement_exercise_path(@submission.exercise), **messages) }
format.json { render(json: messages) } # We must not change the HTTP status code here, since otherwise the custom message is not displayed.
end end
end end

View File

@ -36,8 +36,8 @@ class RemoteEvaluationController < ApplicationController
def try_lti def try_lti
# TODO: Need to consider and support programming groups # TODO: Need to consider and support programming groups
if !@submission.user.nil? && lti_outcome_service?(@submission.exercise_id, @submission.user.id) if !@submission.user.nil? && lti_outcome_service?(@submission.exercise_id, @submission.user.id)
lti_response = send_score(@submission) lti_responses = send_scores(@submission)
process_lti_response(lti_response) process_lti_response(lti_responses.first)
else else
{ {
message: "Your submission was successfully scored with #{@submission.normalized_score * 100}%. " \ message: "Your submission was successfully scored with #{@submission.normalized_score * 100}%. " \

View File

@ -72,11 +72,11 @@ class Submission < ApplicationRecord
end end
def normalized_score def normalized_score
if !score.nil? && !exercise.maximum_score.nil? && exercise.maximum_score.positive? @normalized_score ||= if !score.nil? && !exercise.maximum_score.nil? && exercise.maximum_score.positive?
score / exercise.maximum_score score / exercise.maximum_score
else else
0 0
end end
end end
def percentage def percentage

View File

@ -1,6 +1,6 @@
h1 = t('.headline') h1 = t('.headline')
- consumer = @submission.user.consumer - consumer = current_user.consumer
p p
= t(".success_#{consumer ? 'with' : 'without'}_outcome", consumer: consumer) = t(".success_#{consumer ? 'with' : 'without'}_outcome", consumer: consumer)

View File

@ -550,6 +550,7 @@ de:
too_late: Ihre Abgabe wurde erfolgreich gespeichert, ging jedoch nach der Abgabefrist ein. too_late: Ihre Abgabe wurde erfolgreich gespeichert, ging jedoch nach der Abgabefrist ein.
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_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. 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.
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: study_group_dashboard:
live_dashboard: Live Dashboard live_dashboard: Live Dashboard
time_spent_per_learner: Verwendete Zeit pro Lerner time_spent_per_learner: Verwendete Zeit pro Lerner

View File

@ -550,6 +550,7 @@ en:
too_late: Your submission was saved successfully but was received after the deadline passed. too_late: Your submission was saved successfully but was received after the deadline passed.
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_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! 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!
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: study_group_dashboard:
live_dashboard: Live Dashboard live_dashboard: Live Dashboard
time_spent_per_learner: Time spent per Learner time_spent_per_learner: Time spent per Learner

View File

@ -102,7 +102,7 @@ describe Lti do
end end
end end
describe '#send_score' do describe '#send_scores' do
let(:consumer) { create(:consumer) } let(:consumer) { create(:consumer) }
let(:score) { 0.5 } let(:score) { 0.5 }
let(:submission) { create(:submission) } let(:submission) { create(:submission) }
@ -114,7 +114,7 @@ describe Lti do
context 'with an invalid score' do context 'with an invalid score' do
it 'raises an exception' do it 'raises an exception' do
allow(submission).to receive(:normalized_score).and_return Lti::MAXIMUM_SCORE * 2 allow(submission).to receive(:normalized_score).and_return Lti::MAXIMUM_SCORE * 2
expect { controller.send(:send_score, submission) }.to raise_error(Lti::Error) expect { controller.send(:send_scores, submission) }.to raise_error(Lti::Error)
end end
end end
@ -124,13 +124,13 @@ describe Lti do
it 'returns a corresponding status' do it 'returns a corresponding status' do
allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:outcome_service?).and_return(false) allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:outcome_service?).and_return(false)
allow(submission).to receive(:normalized_score).and_return score allow(submission).to receive(:normalized_score).and_return score
expect(controller.send(:send_score, submission)[:status]).to eq('unsupported') expect(controller.send(:send_scores, submission).first[:status]).to eq('unsupported')
end end
end end
context 'when grading is supported' do context 'when grading is supported' do
let(:response) { double } let(:response) { double }
let(:send_score) { controller.send(:send_score, submission) } let(:send_scores) { controller.send(:send_scores, submission).first }
before do before do
allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:outcome_service?).and_return(true) allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:outcome_service?).and_return(true)
@ -144,13 +144,13 @@ describe Lti do
it 'sends the score' do it 'sends the score' do
expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:post_replace_result!).with(score) expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:post_replace_result!).with(score)
send_score send_scores
end end
it 'returns code, message, and status' do it 'returns code, message, and status' do
expect(send_score[:code]).to eq(response.response_code) expect(send_scores[:code]).to eq(response.response_code)
expect(send_score[:message]).to eq(response.body) expect(send_scores[:message]).to eq(response.body)
expect(send_score[:status]).to eq(response.code_major) expect(send_scores[:status]).to eq(response.code_major)
end end
end end
end end
@ -160,7 +160,7 @@ describe Lti do
submission.contributor.consumer = nil submission.contributor.consumer = nil
allow(submission).to receive(:normalized_score).and_return score allow(submission).to receive(:normalized_score).and_return score
expect(controller.send(:send_score, submission)[:status]).to eq('error') expect(controller.send(:send_scores, submission).first[:status]).to eq('error')
end end
end end
end end

View File

@ -325,7 +325,7 @@ describe ExercisesController do
context 'when the score transmission succeeds' do context 'when the score transmission succeeds' do
before do before do
allow(controller).to receive(:send_score).and_return(status: 'success') allow(controller).to receive(:send_scores).and_return([{status: 'success'}])
perform_request perform_request
end end
@ -341,7 +341,7 @@ describe ExercisesController do
context 'when the score transmission fails' do context 'when the score transmission fails' do
before do before do
allow(controller).to receive(:send_score).and_return(status: 'unsupported') allow(controller).to receive(:send_scores).and_return([{status: 'unsupported'}])
perform_request perform_request
end end
@ -351,8 +351,11 @@ describe ExercisesController do
expect(assigns(:submission)).to be_a(Submission) expect(assigns(:submission)).to be_a(Submission)
end end
it 'returns an error message' do
expect(response.parsed_body).to eq('danger' => I18n.t('exercises.submit.failure'))
end
expect_json expect_json
expect_http_status(:service_unavailable)
end end
end end
@ -369,7 +372,7 @@ describe ExercisesController do
end end
it 'does not send scores' do it 'does not send scores' do
expect(controller).not_to receive(:send_score) expect(controller).not_to receive(:send_scores)
end end
expect_json expect_json

View File

@ -91,7 +91,7 @@ describe Exercise do
context 'without submissions' do context 'without submissions' do
it 'returns nil' do it 'returns nil' do
expect(exercise.average_score).to be 0 expect(exercise.average_score).to be 0.0
end end
end end