Skip to content

Commit

Permalink
Merge pull request #2617 from Alex-Jordan/lti-grades
Browse files Browse the repository at this point in the history
add controls for when grades are sent to the LMS
  • Loading branch information
drgrice1 authored Dec 3, 2024
2 parents a9bc14a + f97d4c5 commit 87f6301
Show file tree
Hide file tree
Showing 15 changed files with 580 additions and 282 deletions.
88 changes: 78 additions & 10 deletions conf/authen_LTI.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -135,19 +135,84 @@ $LTIGradeMode = '';
#$LTIGradeMode = 'course';
#$LTIGradeMode = 'homework';

# When set this variable sends grades back to the LMS every time a user submits an answer. This
# keeps students grades up to date but can be a drain on the server.
$LTIGradeOnSubmit = 1;
# There are several controls for when to report scores to the LMS. Sometimes these controls
# interact with each other, and the details of how they work may depend on whether $LTIGradeMode
# is set to 'course' or 'homework'. So it is recommended to understand all of them and then
# decide how to set them.

# If $LTICheckPrior is 1, then any time WeBWorK is about to send a score to the LMS, it will
# first request from the LMS what that score currently is. Then if there is no significant
# difference between the LMS score and the WeBWorK score, WeBWorK will not follow through with
# updating the LMS score. This is to avoid frequent insignificant updates to a student's scores
# in the LMS. With some LMSs, students may receive notifications each time a score is updated,
# and setting this variable will prevent too many notifications for them. This does create a
# two-phase process, first querying the current score from the LMS and then actually updating
# the score (if there is a significant difference).

# If $LTICheckPrior is set to 1 then the current LMS grade will be checked first, and if the grade
# has not changed then the grade will not be updated. This is intended to reduce changes to LMS
# records when no real grade change occurred. It requires a 2 round process, first querying the
# current grade from the LMS and then when needed making the grade submission.
# Additional details:
# - If the LMS score is not 100%, but the WeBWorK score is, then even if the LMS score is only
# insignificantly less than 100%, it will be updated anyway.
# - If the LMS score is null and the WeBWorK score is 0, this is considered an insignificant
# difference and the LMS score will not be updated to 0. However if it is after the
# $LTISendScoresAfterDate (described below), then the null score will be updated to 0 anyway.
# - "Significant" means an absolute difference of 0.001, or 0.1%. At this time this is not
# configurable.
$LTICheckPrior = 0;

# The system periodically updates student grades on the LMS. This variable controls how often
# that happens. Set to -1 to disable.
$LTIMassUpdateInterval = 86400; #in seconds
# If $LTIGradeOnSubmit is set to 1, then each time a user submits an answer or scores a test,
# that will trigger WeBWorK possibly reporting a score to the LMS. See $LTICheckPrior for one
# reason that WeBWorK might not ultimately send a score. But there are other reasons too.
# WeBWorK will send the score (the assignment's score if $LTIGradeMode is 'homework' or the
# overall course score if $LTIGradeMode is 'course') to the LMS only if either the assignment's
# $LTISendGradesEarlyThreshold (described below) has been met or if it is past that assignment's
# $LTISendScoresAfterDate (also described below).
$LTIGradeOnSubmit = 1;

# In addition to scores possibly being sent to the LMS upon submission, they can be sent by an
# instructor or admin user using the LTI Grades Update Tool. And thirdly, the system can
# periodically update student scores on the LMS on its own. For all three possible triggers for
# scores to be passed to the LMS, $LTISendScoresAfterDate and $LTISendGradesEarlyThreshold can
# affect what is sent. $LTISendScoresAfterDate can be 'open_date', 'reduced_scoring_date',
# 'due_date', 'answer_date', or 'never'. For a given assignment, if it is after the
# $LTISendScoresAfterDate, then WeBWorK will send scores. If $LTISendScoresAfterDate is 'never',
# then there is no date after which WeBWorK is guaranteed to send scores. In that case, scores
# are only sent when a set's $LTISendGradesEarlyThreshold is met (see below).
# - For 'course' grade passback mode, the assignment will be included in the overall course
# grade calculation.
# - For 'homework' grade passback mode, the assignment's score will be sent.

# If $LTISendScoresAfterDate is 'reduced_scoring_date' and an assignment has no reduced scoring
# date or reduced scoring is disabled for that assignment, the fallback is to use the due date.

# For a given assignment, if $LTISendScoresAfterDate is 'never' or if it is before the date
# specified by $LTISendScoresAfterDate, WeBWorK may send a score to the LMS depending on the
# value of $LTISendGradesEarlyThreshold. This variable can either be the string 'attempted' or a
# number from 0 to 1. If this variable is 'attempted', a given set must have been attempted for
# the threshold to have been met, and then the score can be used even if it is before the
# $LTISendScoresAfterDate. For a non-test set, 'attempted' just means that some exercise in the
# set was attempted using the Submit button. For a test, 'attempted' means that either there is
# one version with a graded submission, or there are at least two versions.

# If $LTISendGradesEarlyThreshold is a number from 0 to 1, the score for an assignment needs to
# have reached that number for the threshold to be met, and then the score can be used even if
# it is before the $LTISendScoresAfterDate.

#$LTISendScoresAfterDate = 'open_date';
$LTISendScoresAfterDate = 'reduced_scoring_date';
#$LTISendScoresAfterDate = 'due_date';
#$LTISendScoresAfterDate = 'answer_date';
#$LTISendScoresAfterDate = 'never';

$LTISendGradesEarlyThreshold = 'attempted';
#$LTISendGradesEarlyThreshold = 0;
#$LTISendGradesEarlyThreshold = 0.7;
#$LTISendGradesEarlyThreshold = 1;

# The system periodically updates student scores on the LMS. If it has been at least this many
# seconds since the last mass passback event and someone in the course does anything to load a
# page, then a new mass passback job will begin. Set this to -1 to disable mass passback.
$LTIMassUpdateInterval = 86400;


################################################################################################
# Add an 'LTI' tab to the Course Configuration page
Expand All @@ -170,7 +235,10 @@ $LTIMassUpdateInterval = 86400; #in seconds
#'LTI{v1p3}{LMS_url}',
#'external_auth',
#'LTIGradeMode',
#'LTICheckPrior',
#'LTIGradeOnSubmit',
#'LTISendScoresAfterDate',
#'LTISendGradesEarlyThreshold',
#'LTIMassUpdateInterval',
#'LMSManageUserData',
#'LTI{v1p1}{BasicConsumerSecret}',
Expand Down
3 changes: 0 additions & 3 deletions conf/authen_LTI_1_1.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,4 @@ $LTI{v1p1}{LMSrolesToWeBWorKroles} = {
# $userSet->answer_date($niceAnswerTime);
#};

# Do not change this.
$LTI{v1p1}{grader} = 'WeBWorK::Authen::LTIAdvanced::SubmitGrade';

1; # final line of the file to reassure perl that it was read properly.
3 changes: 0 additions & 3 deletions conf/authen_LTI_1_3.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,4 @@ $LTI{v1p3}{AllowInstitutionRoles} = 0;

$LTI{v1p3}{ignoreMissingSourcedID} = 0;

# Do not change this.
$LTI{v1p3}{grader} = 'WeBWorK::Authen::LTIAdvantage::SubmitGrade';

1; # final line of the file to reassure perl that it was read properly.
2 changes: 1 addition & 1 deletion lib/Caliper/Entity.pm
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ sub problem_set_attempt {
my $extensions = { 'attempt_score' => $score, };

if ($version_id) {
$extensions->{'gateway_score'} = grade_gateway($db, $problem_set_user, $problem_set_user->set_id, $user_id);
$extensions->{'gateway_score'} = grade_gateway($db, $problem_set_user->set_id, $user_id);
}

my $problem_set_attempt = {
Expand Down
185 changes: 185 additions & 0 deletions lib/WeBWorK/Authen/LTI/GradePassback.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
###############################################################################
# WeBWorK Online Homework Delivery System
# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of either: (a) the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version, or (b) the "Artistic License" which comes with this package.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
# Artistic License for more details.
################################################################################

package WeBWorK::Authen::LTI::GradePassback;
use Mojo::Base 'Exporter', -signatures, -async_await;

=head1 NAME
WeBWorK::Authen::LTI::GradePassback - Grade passback utilities for LTI authentication
=cut

use WeBWorK::Utils::DateTime qw(after before);
use WeBWorK::Utils::Sets qw(grade_set grade_gateway);

our @EXPORT_OK = qw(massUpdate passbackGradeOnSubmit getSetPassbackScore);

# These must be required and not used, and must be after the exports are defined above.
# Otherwise this will create a circular dependency with the SubmitGrade modules.
require WeBWorK::Authen::LTIAdvanced::SubmitGrade;
require WeBWorK::Authen::LTIAdvantage::SubmitGrade;

# Perform a mass update of all grades. This is all user grades for course grade mode and all user set grades for
# homework grade mode if $manual_update is false. Otherwise what is updated is determined by a combination of the grade
# mode and the useriD and setID parameters. Note that the only required parameter is $c which should be a
# WeBWorK::Controller object with a valid course environment and database.
sub massUpdate ($c, $manual_update = 0, $userID = undef, $setID = undef) {
my $ce = $c->ce;
my $db = $c->db;

# Sanity check.
unless (ref($ce)) {
warn('course environment is not defined');
return;
}
unless (ref($db)) {
warn('database reference is not defined');
return;
}

# Only run an automatic update if the time interval has passed.
if (!$manual_update) {
my $lastUpdate = $db->getSettingValue('LTILastUpdate') || 0;
my $updateInterval = $ce->{LTIMassUpdateInterval} // -1;
return unless ($updateInterval != -1 && time - $lastUpdate > $updateInterval);
$db->setSettingValue('LTILastUpdate', time);
}

# Send warning if debug_lti_grade_passback is set.
if ($ce->{debug_lti_grade_passback}) {
if ($setID && $userID && $ce->{LTIGradeMode} eq 'homework') {
warn "LTI Mass Update: Queueing grade update for user $userID and set $setID.\n";
} elsif ($setID && $ce->{LTIGradeMode} eq 'homework') {
warn "LTI Mass Update: Queueing grade update for all users assigned to set $setID.\n";
} elsif ($userID) {
warn "LTI Mass Update: Queueing grade update of all sets assigned to user $userID.\n";
} else {
warn "LTI Mass Update: Queueing grade update for all sets and users.\n";
}
}

$c->minion->enqueue(lti_mass_update => [ $userID, $setID ], { notes => { courseID => $ce->{courseName} } });

return;
}

async sub passbackGradeOnSubmit ($c, $userID, $set) {
my $ce = $c->ce;

my $LMSname = $ce->{LTI}{ $ce->{LTIVersion} }{LMS_name};

if ($ce->{LTIGradeOnSubmit}) {
my $LTIGradeResult = 0;

my $grader =
$ce->{LTIVersion} eq 'v1p1'
? WeBWorK::Authen::LTIAdvanced::SubmitGrade->new($c)
: WeBWorK::Authen::LTIAdvantage::SubmitGrade->new($c);

if ($ce->{LTIGradeMode} eq 'course') {
$LTIGradeResult = await $grader->submit_course_grade($userID, $set);
} elsif ($ce->{LTIGradeMode} eq 'homework') {
$LTIGradeResult = await $grader->submit_set_grade($userID, $set->set_id, $set);
}
if ($LTIGradeResult == 0) {
return $c->maketext('Your score was not successfully sent to [_1].', $LMSname);
} elsif ($LTIGradeResult > 0) {
return $c->maketext('Your score was successfully sent to [_1].', $LMSname);
} elsif ($LTIGradeResult < 0) {
return $c->maketext('Your score will be sent to [_1] at a later time.', $LMSname);
}
} elsif ($ce->{LTIMassUpdateInterval} > 0) {
if ($ce->{LTIMassUpdateInterval} < 120) {
return $c->maketext('Scores are sent to [_1] every [quant,_2,second].',
$LMSname, $ce->{LTIMassUpdateInterval});
} elsif ($ce->{LTIMassUpdateInterval} < 7200) {
return $c->maketext('Scores are sent to [_1] every [quant,_2,minute].',
$LMSname, int($ce->{LTIMassUpdateInterval} / 60 + 0.99));
} else {
return $c->maketext('Scores are sent to [_1] every [quant,_2,hour].',
$LMSname, int($ce->{LTIMassUpdateInterval} / 3600 + 0.9999));
}
}
}

sub setAttempted ($problems, $setVersions = undef) {
return 0 unless ref($problems) eq 'ARRAY';

# If this is a test with set versions, then it counts as "attempted" if there is more than one set version.
return 1 if ref($setVersions) eq 'ARRAY' && @$setVersions > 1;

for (@$problems) {
return 1 if $_->attempted || $_->status > 0;
}
return 0;
}

sub earliestGatewayDate ($ce, $userSet, $setVersions) {
# If there are no versions, use the template's date.
return getLTISendScoresAfterDate($userSet, $ce) unless ref($setVersions) eq 'ARRAY';

# Otherwise, use the earliest date among versions.
my $earliest_date = -1;
for (@$setVersions) {
my $versionedSetDate = getLTISendScoresAfterDate($_, $ce);
$earliest_date = $versionedSetDate if $earliest_date == -1 || $versionedSetDate < $earliest_date;
}
return $earliest_date;
}

sub getLTISendScoresAfterDate ($set, $ce) {
if ($ce->{LTISendScoresAfterDate} eq 'open_date') {
return $set->open_date;
} elsif ($ce->{LTISendScoresAfterDate} eq 'reduced_scoring_date') {
return ($ce->{pg}{ansEvalDefaults}{enableReducedScoring}
&& $set->enable_reduced_scoring
&& $set->reduced_scoring_date) ? $set->reduced_scoring_date : $set->due_date;
} elsif ($ce->{LTISendScoresAfterDate} eq 'due_date') {
return $set->due_date;
} elsif ($ce->{LTISendScoresAfterDate} eq 'answer_date') {
return $set->answer_date;
}
}

# Returns a reference to hash with the keys totalRight, total, and score if the
# set has met the conditions for grade pass back to occur, and undef otherwise.
sub getSetPassbackScore ($db, $ce, $userID, $userSet, $gradingSubmission = 0) {
my ($totalRight, $total, $problemRecords, $setVersions) =
$userSet->assignment_type =~ /gateway/
? grade_gateway($db, $userSet->set_id, $userID)
: grade_set($db, $userSet, $userID);

my $return = { totalRight => $totalRight, total => $total, score => $total ? $totalRight / $total : 0 };

return $return if $gradingSubmission && $ce->{LTISendGradesEarlyThreshold} eq 'attempted';

my $criticalDate =
$ce->{LTISendScoresAfterDate} ne 'never'
? ($userSet->assignment_type =~ /gateway/
? earliestGatewayDate($ce, $userSet, $setVersions)
: getLTISendScoresAfterDate($userSet, $ce))
: undef;

return $return
if ($criticalDate && after($criticalDate))
|| ($ce->{LTISendGradesEarlyThreshold} eq 'attempted' && setAttempted($problemRecords, $setVersions))
|| ($ce->{LTISendGradesEarlyThreshold} ne 'attempted'
&& $return->{score} >= $ce->{LTISendGradesEarlyThreshold});

return;
}

1;
71 changes: 0 additions & 71 deletions lib/WeBWorK/Authen/LTI/MassUpdate.pm

This file was deleted.

Loading

0 comments on commit 87f6301

Please sign in to comment.