Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add controls for when grades are sent to the LMS #2617

Merged
merged 27 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9938ca8
add controls for when grades are sent to the LMS
Alex-Jordan Nov 8, 2024
4fd4a57
remove unused argument to grade_gateway()
Alex-Jordan Nov 8, 2024
94d38ba
simplify check if gateway set has been attempted
Alex-Jordan Nov 9, 2024
147a961
fix division by zero bug
Alex-Jordan Nov 9, 2024
1b7c707
modularize more with new date and threshold considerations for LTI gr…
Alex-Jordan Nov 10, 2024
f6efa8c
debugging PR 2617
Alex-Jordan Nov 10, 2024
02c9ba9
put x() around config value labels
Alex-Jordan Nov 10, 2024
10ad001
simplify how can_submit_LMS_score is used, and use it earlier
Alex-Jordan Nov 11, 2024
118b732
move can_submit_LMS_score to general set utilities
Nov 12, 2024
b879a98
check that reduced scoring date is enabled or else move on to the clo…
Nov 12, 2024
8964a0c
fix logic for homework_always with LTIGradeOnSubmit
Nov 12, 2024
22457bc
various feedback from PR#2617
Alex-Jordan Nov 14, 2024
f4373dd
minimize calls to grade_set
Alex-Jordan Nov 16, 2024
f251b04
feedback from PR#2617
Alex-Jordan Nov 18, 2024
8f0955f
more feedback from PR#2617
Alex-Jordan Nov 19, 2024
00c7234
handling "0/0" scores for the LMS
Alex-Jordan Nov 19, 2024
16bbf2b
efficiency overhaul of grade passback controls, and other feedback fr…
Alex-Jordan Nov 21, 2024
8f50372
Rework of LTI grade pass back.
drgrice1 Nov 19, 2024
5d7062a
Tweak the wording of the `LTICheckPrior` configuration value document…
drgrice1 Nov 21, 2024
cb89fb9
Merge pull request #32 from drgrice1/lti-grades-rework
Alex-Jordan Nov 21, 2024
5354878
Merge branch 'develop' into lti-grades
Alex-Jordan Nov 21, 2024
734246b
extend grade_all_sets to return a triple including references to the …
Alex-Jordan Nov 26, 2024
3067000
fix a mistake in LTISendScoresAfterDate config options
Alex-Jordan Nov 26, 2024
ac5953b
Add log entries for when grade passback does is blocked
Alex-Jordan Nov 26, 2024
a717243
bring back scalar context output for grade_all_sets
Alex-Jordan Nov 26, 2024
c5fdb38
more log messages for submit_course_grade
Alex-Jordan Nov 28, 2024
f97d4c5
return -1 (not 0) when no sets meet grade passback criteria for cours…
Alex-Jordan Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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