mirror of
https://github.com/moodle/moodle.git
synced 2025-01-18 05:58:34 +01:00
Merge branch 'MDL-77232' of https://github.com/davidpesce/moodle
This commit is contained in:
commit
74a44f78a3
@ -18,18 +18,6 @@ namespace enrol_lti\local\ltiadvantage\task;
|
||||
|
||||
use core\task\scheduled_task;
|
||||
use enrol_lti\helper;
|
||||
use enrol_lti\local\ltiadvantage\lib\http_client;
|
||||
use enrol_lti\local\ltiadvantage\lib\issuer_database;
|
||||
use enrol_lti\local\ltiadvantage\lib\launch_cache_session;
|
||||
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\user_repository;
|
||||
use Packback\Lti1p3\LtiAssignmentsGradesService;
|
||||
use Packback\Lti1p3\LtiGrade;
|
||||
use Packback\Lti1p3\LtiLineitem;
|
||||
use Packback\Lti1p3\LtiRegistration;
|
||||
use Packback\Lti1p3\LtiServiceConnector;
|
||||
|
||||
/**
|
||||
* LTI Advantage task responsible for pushing grades to tool platforms.
|
||||
@ -50,224 +38,11 @@ class sync_grades extends scheduled_task {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync grades to the platform using the Assignment and Grade Services.
|
||||
*
|
||||
* @param \stdClass $resource the enrol_lti_tools data record for the shared resource.
|
||||
* @return array an array containing the
|
||||
*/
|
||||
protected function sync_grades_for_resource($resource): array {
|
||||
$usercount = 0;
|
||||
$sendcount = 0;
|
||||
$userrepo = new user_repository();
|
||||
$resourcelinkrepo = new resource_link_repository();
|
||||
$appregistrationrepo = new application_registration_repository();
|
||||
$issuerdb = new issuer_database($appregistrationrepo, new deployment_repository());
|
||||
|
||||
if ($users = $userrepo->find_by_resource($resource->id)) {
|
||||
$completion = new \completion_info(get_course($resource->courseid));
|
||||
$syncedusergrades = []; // Keep track of those users who have had their grade synced during this run.
|
||||
foreach ($users as $user) {
|
||||
$mtracecontent = "for the user '{$user->get_localid()}', for the resource '$resource->id' and the course " .
|
||||
"'$resource->courseid'";
|
||||
$usercount++;
|
||||
|
||||
// Check if we do not have a grade service endpoint in either of the resource links.
|
||||
// Remember, not all launches need to support grade services.
|
||||
$userresourcelinks = $resourcelinkrepo->find_by_resource_and_user($resource->id, $user->get_id());
|
||||
$userlastgrade = $user->get_lastgrade();
|
||||
mtrace("Found ".count($userresourcelinks)." resource link(s) $mtracecontent. Attempting to sync grades for all.");
|
||||
|
||||
foreach ($userresourcelinks as $userresourcelink) {
|
||||
mtrace("Processing resource link '{$userresourcelink->get_resourcelinkid()}'.");
|
||||
if (!$gradeservice = $userresourcelink->get_grade_service()) {
|
||||
mtrace("Skipping - No grade service found $mtracecontent.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$context = \context::instance_by_id($resource->contextid, IGNORE_MISSING)) {
|
||||
mtrace("Failed - Invalid contextid '$resource->contextid' for the resource '$resource->id'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
$grade = false;
|
||||
$dategraded = false;
|
||||
if ($context->contextlevel == CONTEXT_COURSE) {
|
||||
if ($resource->gradesynccompletion && !$completion->is_course_complete($user->get_localid())) {
|
||||
mtrace("Skipping - Course not completed $mtracecontent.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the grade.
|
||||
if ($grade = grade_get_course_grade($user->get_localid(), $resource->courseid)) {
|
||||
$grademax = floatval($grade->item->grademax);
|
||||
$dategraded = $grade->dategraded;
|
||||
$grade = $grade->grade;
|
||||
}
|
||||
} else if ($context->contextlevel == CONTEXT_MODULE) {
|
||||
$cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST);
|
||||
|
||||
if ($resource->gradesynccompletion) {
|
||||
$data = $completion->get_data($cm, false, $user->get_localid());
|
||||
if (!in_array($data->completionstate, [COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE])) {
|
||||
mtrace("Skipping - Activity not completed $mtracecontent.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$grades = grade_get_grades($cm->course, 'mod', $cm->modname, $cm->instance,
|
||||
$user->get_localid());
|
||||
if (!empty($grades->items[0]->grades)) {
|
||||
$grade = reset($grades->items[0]->grades);
|
||||
if (!empty($grade->item)) {
|
||||
$grademax = floatval($grade->item->grademax);
|
||||
} else {
|
||||
$grademax = floatval($grades->items[0]->grademax);
|
||||
}
|
||||
$dategraded = $grade->dategraded;
|
||||
$grade = $grade->grade;
|
||||
}
|
||||
}
|
||||
|
||||
if ($grade === false || $grade === null || strlen($grade) < 1) {
|
||||
mtrace("Skipping - Invalid grade $mtracecontent.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($grademax)) {
|
||||
mtrace("Skipping - Invalid grademax $mtracecontent.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!grade_floats_different($grade, $userlastgrade)) {
|
||||
mtrace("Not sent - The grade $mtracecontent was not sent as the grades are the same.");
|
||||
continue;
|
||||
}
|
||||
$floatgrade = $grade / $grademax;
|
||||
|
||||
try {
|
||||
// Get an AGS instance for the corresponding application registration and service data.
|
||||
$appregistration = $appregistrationrepo->find_by_deployment(
|
||||
$userresourcelink->get_deploymentid()
|
||||
);
|
||||
$registration = $issuerdb->findRegistrationByIssuer(
|
||||
$appregistration->get_platformid()->out(false),
|
||||
$appregistration->get_clientid()
|
||||
);
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/filelib.php');
|
||||
$sc = new LtiServiceConnector(new launch_cache_session(), new http_client(new \curl()));
|
||||
|
||||
$lineitemurl = $gradeservice->get_lineitemurl();
|
||||
$lineitemsurl = $gradeservice->get_lineitemsurl();
|
||||
$servicedata = [
|
||||
'lineitems' => $lineitemsurl ? $lineitemsurl->out(false) : null,
|
||||
'lineitem' => $lineitemurl ? $lineitemurl->out(false) : null,
|
||||
'scope' => $gradeservice->get_scopes(),
|
||||
];
|
||||
|
||||
$ags = $this->get_ags($sc, $registration, $servicedata);
|
||||
$ltigrade = LtiGrade::new()
|
||||
->setScoreGiven($grade)
|
||||
->setScoreMaximum($grademax)
|
||||
->setUserId($user->get_sourceid())
|
||||
->setTimestamp(date(\DateTimeInterface::ISO8601, $dategraded))
|
||||
->setActivityProgress('Completed')
|
||||
->setGradingProgress('FullyGraded');
|
||||
|
||||
if (empty($servicedata['lineitem'])) {
|
||||
// The launch did not include a couple lineitem, so find or create the line item for grading.
|
||||
$lineitem = $ags->findOrCreateLineitem(new LtiLineitem([
|
||||
'label' => $this->get_line_item_label($resource, $context),
|
||||
'scoreMaximum' => $grademax,
|
||||
'tag' => 'grade',
|
||||
'resourceId' => $userresourcelink->get_resourceid(),
|
||||
'resourceLinkId' => $userresourcelink->get_resourcelinkid()
|
||||
]));
|
||||
$response = $ags->putGrade($ltigrade, $lineitem);
|
||||
} else {
|
||||
// Let AGS find the coupled line item.
|
||||
$response = $ags->putGrade($ltigrade);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send.");
|
||||
mtrace($e->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($response['status'] == 200) {
|
||||
$user->set_lastgrade(grade_floatval($grade));
|
||||
$syncedusergrades[$user->get_id()] = $user;
|
||||
mtrace("Success - The grade '$floatgrade' $mtracecontent was sent.");
|
||||
} else {
|
||||
mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send.");
|
||||
mtrace("Header: {$response['headers']['httpstatus']}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update the lastgrade value for any users who had a grade synced. Allows skipping on future runs if not changed.
|
||||
// Update the count of total users having their grades synced, not the total number of grade sync calls made.
|
||||
foreach ($syncedusergrades as $ltiuser) {
|
||||
$userrepo->save($ltiuser);
|
||||
$sendcount = $sendcount + 1;
|
||||
}
|
||||
}
|
||||
return [$usercount, $sendcount];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string label for the line item associated with the resource, based on the course or module name.
|
||||
*
|
||||
* @param \stdClass $resource the enrol_lti_tools record.
|
||||
* @param \context $context the context of the resource - either course or module.
|
||||
* @return string the label to use in the line item.
|
||||
*/
|
||||
protected function get_line_item_label(\stdClass $resource, \context $context): string {
|
||||
$resourcename = 'default';
|
||||
if ($context->contextlevel == CONTEXT_COURSE) {
|
||||
global $DB;
|
||||
$coursenamesql = "SELECT c.fullname
|
||||
FROM {enrol_lti_tools} t
|
||||
JOIN {enrol} e
|
||||
ON (e.id = t.enrolid)
|
||||
JOIN {course} c
|
||||
ON {c.id} = e.courseid
|
||||
WHERE t.id = :resourceid";
|
||||
$coursename = $DB->get_field_sql($coursenamesql, ['resourceid' => $resource->id]);
|
||||
$resourcename = format_string($coursename, true, ['context' => $context->id]);
|
||||
} else if ($context->contextlevel == CONTEXT_MODULE) {
|
||||
foreach (get_fast_modinfo($resource->courseid)->get_cms() as $mod) {
|
||||
if ($mod->context->id == $context->id) {
|
||||
$resourcename = $mod->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $resourcename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ags instance to make the call to the platform.
|
||||
*
|
||||
* @param LtiServiceConnector $sc a service connector instance.
|
||||
* @param LtiRegistration $registration the registration instance.
|
||||
* @param array $sd the service data.
|
||||
* @return LtiAssignmentsGradesService
|
||||
*/
|
||||
protected function get_ags(LtiServiceConnector $sc, LtiRegistration $registration, array $sd): LtiAssignmentsGradesService {
|
||||
return new LtiAssignmentsGradesService($sc, $registration, $sd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the synchronisation of grades from the tool to any registered platforms.
|
||||
* Creates adhoc tasks (one per resource) to synchronize grades from the tool to any registered platforms.
|
||||
*
|
||||
* @return bool|void
|
||||
*/
|
||||
public function execute() {
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/lib/completionlib.php');
|
||||
require_once($CFG->libdir . '/gradelib.php');
|
||||
require_once($CFG->dirroot . '/grade/querylib.php');
|
||||
|
||||
if (!is_enabled_auth('lti')) {
|
||||
mtrace('Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti')));
|
||||
@ -289,13 +64,12 @@ class sync_grades extends scheduled_task {
|
||||
}
|
||||
|
||||
foreach ($resources as $resource) {
|
||||
mtrace("Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$resource->courseid'.");
|
||||
$task = new \enrol_lti\local\ltiadvantage\task\sync_tool_grades();
|
||||
$task->set_custom_data($resource);
|
||||
$task->set_component('enrol_lti');
|
||||
\core\task\manager::queue_adhoc_task($task, true);
|
||||
}
|
||||
|
||||
[$usercount, $sendcount] = $this->sync_grades_for_resource($resource);
|
||||
|
||||
mtrace("Completed - Synced grades for tool '$resource->id' in the course '$resource->courseid'. " .
|
||||
"Processed $usercount users; sent $sendcount grades.");
|
||||
mtrace("");
|
||||
}
|
||||
mtrace('Spawned ' . count($resources) . ' adhoc tasks to sync grades.');
|
||||
}
|
||||
}
|
||||
|
273
enrol/lti/classes/local/ltiadvantage/task/sync_tool_grades.php
Normal file
273
enrol/lti/classes/local/ltiadvantage/task/sync_tool_grades.php
Normal file
@ -0,0 +1,273 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace enrol_lti\local\ltiadvantage\task;
|
||||
|
||||
use core\task\adhoc_task;
|
||||
use enrol_lti\local\ltiadvantage\lib\http_client;
|
||||
use enrol_lti\local\ltiadvantage\lib\issuer_database;
|
||||
use enrol_lti\local\ltiadvantage\lib\launch_cache_session;
|
||||
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\user_repository;
|
||||
use Packback\Lti1p3\LtiAssignmentsGradesService;
|
||||
use Packback\Lti1p3\LtiGrade;
|
||||
use Packback\Lti1p3\LtiLineitem;
|
||||
use Packback\Lti1p3\LtiRegistration;
|
||||
use Packback\Lti1p3\LtiServiceConnector;
|
||||
|
||||
/**
|
||||
* LTI Advantage task responsible for pushing grades to tool platforms.
|
||||
*
|
||||
* @package enrol_lti
|
||||
* @copyright 2023 David Pesce <david.pesce@exputo.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class sync_tool_grades extends adhoc_task {
|
||||
|
||||
/**
|
||||
* Sync grades to the platform using the Assignment and Grade Services (AGS).
|
||||
*
|
||||
* @param \stdClass $resource the enrol_lti_tools data record for the shared resource.
|
||||
* @return array an array containing the
|
||||
*/
|
||||
protected function sync_grades_for_resource($resource): array {
|
||||
$usercount = 0;
|
||||
$sendcount = 0;
|
||||
$userrepo = new user_repository();
|
||||
$resourcelinkrepo = new resource_link_repository();
|
||||
$appregistrationrepo = new application_registration_repository();
|
||||
$issuerdb = new issuer_database($appregistrationrepo, new deployment_repository());
|
||||
|
||||
if ($users = $userrepo->find_by_resource($resource->id)) {
|
||||
$completion = new \completion_info(get_course($resource->courseid));
|
||||
$syncedusergrades = []; // Keep track of those users who have had their grade synced during this run.
|
||||
foreach ($users as $user) {
|
||||
$mtracecontent = "for the user '{$user->get_localid()}', for the resource '$resource->id' and the course " .
|
||||
"'$resource->courseid'";
|
||||
$usercount++;
|
||||
|
||||
// Check if we do not have a grade service endpoint in either of the resource links.
|
||||
// Remember, not all launches need to support grade services.
|
||||
$userresourcelinks = $resourcelinkrepo->find_by_resource_and_user($resource->id, $user->get_id());
|
||||
$userlastgrade = $user->get_lastgrade();
|
||||
mtrace("Found ".count($userresourcelinks)." resource link(s) $mtracecontent. Attempting to sync grades for all.");
|
||||
|
||||
foreach ($userresourcelinks as $userresourcelink) {
|
||||
mtrace("Processing resource link '{$userresourcelink->get_resourcelinkid()}'.");
|
||||
if (!$gradeservice = $userresourcelink->get_grade_service()) {
|
||||
mtrace("Skipping - No grade service found $mtracecontent.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$context = \context::instance_by_id($resource->contextid, IGNORE_MISSING)) {
|
||||
mtrace("Failed - Invalid contextid '$resource->contextid' for the resource '$resource->id'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
$grade = false;
|
||||
$dategraded = false;
|
||||
if ($context->contextlevel == CONTEXT_COURSE) {
|
||||
if ($resource->gradesynccompletion && !$completion->is_course_complete($user->get_localid())) {
|
||||
mtrace("Skipping - Course not completed $mtracecontent.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the grade.
|
||||
if ($grade = grade_get_course_grade($user->get_localid(), $resource->courseid)) {
|
||||
$grademax = floatval($grade->item->grademax);
|
||||
$dategraded = $grade->dategraded;
|
||||
$grade = $grade->grade;
|
||||
}
|
||||
} else if ($context->contextlevel == CONTEXT_MODULE) {
|
||||
$cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST);
|
||||
|
||||
if ($resource->gradesynccompletion) {
|
||||
$data = $completion->get_data($cm, false, $user->get_localid());
|
||||
if (!in_array($data->completionstate, [COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE])) {
|
||||
mtrace("Skipping - Activity not completed $mtracecontent.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$grades = grade_get_grades($cm->course, 'mod', $cm->modname, $cm->instance,
|
||||
$user->get_localid());
|
||||
if (!empty($grades->items[0]->grades)) {
|
||||
$grade = reset($grades->items[0]->grades);
|
||||
if (!empty($grade->item)) {
|
||||
$grademax = floatval($grade->item->grademax);
|
||||
} else {
|
||||
$grademax = floatval($grades->items[0]->grademax);
|
||||
}
|
||||
$dategraded = $grade->dategraded;
|
||||
$grade = $grade->grade;
|
||||
}
|
||||
}
|
||||
|
||||
if ($grade === false || $grade === null || strlen($grade) < 1) {
|
||||
mtrace("Skipping - Invalid grade $mtracecontent.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($grademax)) {
|
||||
mtrace("Skipping - Invalid grademax $mtracecontent.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!grade_floats_different($grade, $userlastgrade)) {
|
||||
mtrace("Not sent - The grade $mtracecontent was not sent as the grades are the same.");
|
||||
continue;
|
||||
}
|
||||
$floatgrade = $grade / $grademax;
|
||||
|
||||
try {
|
||||
// Get an AGS instance for the corresponding application registration and service data.
|
||||
$appregistration = $appregistrationrepo->find_by_deployment(
|
||||
$userresourcelink->get_deploymentid()
|
||||
);
|
||||
$registration = $issuerdb->findRegistrationByIssuer(
|
||||
$appregistration->get_platformid()->out(false),
|
||||
$appregistration->get_clientid()
|
||||
);
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/filelib.php');
|
||||
$sc = new LtiServiceConnector(new launch_cache_session(), new http_client(new \curl()));
|
||||
|
||||
$lineitemurl = $gradeservice->get_lineitemurl();
|
||||
$lineitemsurl = $gradeservice->get_lineitemsurl();
|
||||
$servicedata = [
|
||||
'lineitems' => $lineitemsurl ? $lineitemsurl->out(false) : null,
|
||||
'lineitem' => $lineitemurl ? $lineitemurl->out(false) : null,
|
||||
'scope' => $gradeservice->get_scopes(),
|
||||
];
|
||||
|
||||
$ags = $this->get_ags($sc, $registration, $servicedata);
|
||||
$ltigrade = LtiGrade::new()
|
||||
->setScoreGiven($grade)
|
||||
->setScoreMaximum($grademax)
|
||||
->setUserId($user->get_sourceid())
|
||||
->setTimestamp(date(\DateTimeInterface::ISO8601, $dategraded))
|
||||
->setActivityProgress('Completed')
|
||||
->setGradingProgress('FullyGraded');
|
||||
|
||||
if (empty($servicedata['lineitem'])) {
|
||||
// The launch did not include a couple lineitem, so find or create the line item for grading.
|
||||
$lineitem = $ags->findOrCreateLineitem(new LtiLineitem([
|
||||
'label' => $this->get_line_item_label($resource, $context),
|
||||
'scoreMaximum' => $grademax,
|
||||
'tag' => 'grade',
|
||||
'resourceId' => $userresourcelink->get_resourceid(),
|
||||
'resourceLinkId' => $userresourcelink->get_resourcelinkid()
|
||||
]));
|
||||
$response = $ags->putGrade($ltigrade, $lineitem);
|
||||
} else {
|
||||
// Let AGS find the coupled line item.
|
||||
$response = $ags->putGrade($ltigrade);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send.");
|
||||
mtrace($e->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($response['status'] == 200) {
|
||||
$user->set_lastgrade(grade_floatval($grade));
|
||||
$syncedusergrades[$user->get_id()] = $user;
|
||||
mtrace("Success - The grade '$floatgrade' $mtracecontent was sent.");
|
||||
} else {
|
||||
mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send.");
|
||||
mtrace("Header: {$response['headers']['httpstatus']}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update the lastgrade value for any users who had a grade synced. Allows skipping on future runs if not changed.
|
||||
// Update the count of total users having their grades synced, not the total number of grade sync calls made.
|
||||
foreach ($syncedusergrades as $ltiuser) {
|
||||
$userrepo->save($ltiuser);
|
||||
$sendcount = $sendcount + 1;
|
||||
}
|
||||
}
|
||||
return [$usercount, $sendcount];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string label for the line item associated with the resource, based on the course or module name.
|
||||
*
|
||||
* @param \stdClass $resource the enrol_lti_tools record.
|
||||
* @param \context $context the context of the resource - either course or module.
|
||||
* @return string the label to use in the line item.
|
||||
*/
|
||||
protected function get_line_item_label(\stdClass $resource, \context $context): string {
|
||||
$resourcename = 'default';
|
||||
if ($context->contextlevel == CONTEXT_COURSE) {
|
||||
global $DB;
|
||||
$coursenamesql = "SELECT c.fullname
|
||||
FROM {enrol_lti_tools} t
|
||||
JOIN {enrol} e
|
||||
ON (e.id = t.enrolid)
|
||||
JOIN {course} c
|
||||
ON {c.id} = e.courseid
|
||||
WHERE t.id = :resourceid";
|
||||
$coursename = $DB->get_field_sql($coursenamesql, ['resourceid' => $resource->id]);
|
||||
$resourcename = format_string($coursename, true, ['context' => $context->id]);
|
||||
} else if ($context->contextlevel == CONTEXT_MODULE) {
|
||||
foreach (get_fast_modinfo($resource->courseid)->get_cms() as $mod) {
|
||||
if ($mod->context->id == $context->id) {
|
||||
$resourcename = $mod->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $resourcename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an Assignment and Grade Services (AGS) instance to make the call to the platform.
|
||||
*
|
||||
* @param LtiServiceConnector $sc a service connector instance.
|
||||
* @param LtiRegistration $registration the registration instance.
|
||||
* @param array $sd the service data.
|
||||
* @return LtiAssignmentsGradesService
|
||||
*/
|
||||
protected function get_ags(LtiServiceConnector $sc, LtiRegistration $registration, array $sd): LtiAssignmentsGradesService {
|
||||
return new LtiAssignmentsGradesService($sc, $registration, $sd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the synchronisation of grades from the tool to any registered platforms.
|
||||
*
|
||||
* @return bool|void
|
||||
*/
|
||||
public function execute() {
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/lib/completionlib.php');
|
||||
require_once($CFG->libdir . '/gradelib.php');
|
||||
require_once($CFG->dirroot . '/grade/querylib.php');
|
||||
|
||||
$resource = $this->get_custom_data();
|
||||
|
||||
mtrace("Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$resource->courseid'.");
|
||||
|
||||
[$usercount, $sendcount] = $this->sync_grades_for_resource($resource);
|
||||
|
||||
mtrace("Completed - Synced grades for tool '$resource->id' in the course '$resource->courseid'. " .
|
||||
"Processed $usercount users; sent $sendcount grades.");
|
||||
mtrace("");
|
||||
|
||||
}
|
||||
}
|
@ -34,115 +34,6 @@ require_once(__DIR__ . '/../lti_advantage_testcase.php');
|
||||
* @coversDefaultClass \enrol_lti\local\ltiadvantage\task\sync_grades
|
||||
*/
|
||||
class sync_grades_test extends \lti_advantage_testcase {
|
||||
/**
|
||||
* Get a task which has a mocked ags instance injected, meaning no real calls will be made to the platform.
|
||||
*
|
||||
* This allows us to test the behaviour of the task (in terms of which users are in scope and which grades are sent)
|
||||
* without needing to deal with any auth.
|
||||
*
|
||||
* @param string $statuscode the HTTP status code to simulate.
|
||||
* @param bool $mockexception whether to simulate an exception during the service call or not.
|
||||
* @return sync_grades instance of the task with a mocked ags instance inside.
|
||||
*/
|
||||
protected function get_task_with_mocked_grade_service($statuscode = '200', $mockexception = false): sync_grades {
|
||||
$mockgradeservice = $this->getMockBuilder(LtiAssignmentsGradesService::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['putGrade'])
|
||||
->getMock();
|
||||
$mockgradeservice->method('putGrade')->willReturnCallback(function() use ($statuscode, $mockexception) {
|
||||
if ($mockexception) {
|
||||
throw new \Exception();
|
||||
}
|
||||
return ['headers' => ['httpstatus' => "HTTP/2 $statuscode OK"], 'body' => '', 'status' => $statuscode];
|
||||
});
|
||||
// Get a mock task, with the method 'get_ags()' mocked to return the mocked AGS instance.
|
||||
$mocktask = $this->getMockBuilder(sync_grades::class)
|
||||
->onlyMethods(['get_ags'])
|
||||
->getMock();
|
||||
$mocktask->method('get_ags')->willReturn($mockgradeservice);
|
||||
return $mocktask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to set a grade for a user.
|
||||
*
|
||||
* @param int $userid the id of the user being graded.
|
||||
* @param float $grade the grade value, out of 100, to set for the user.
|
||||
* @param \stdClass $resource the published resource object.
|
||||
* @return float the fractional grade value expected to be used during sync.
|
||||
*/
|
||||
protected function set_user_grade_for_resource(int $userid, float $grade, \stdClass $resource): float {
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/accesslib.php');
|
||||
require_once($CFG->libdir . '/gradelib.php');
|
||||
$context = \context::instance_by_id($resource->contextid);
|
||||
|
||||
if ($context->contextlevel == CONTEXT_COURSE) {
|
||||
$gi = \grade_item::fetch_course_item($resource->courseid);
|
||||
} else if ($context->contextlevel == CONTEXT_MODULE) {
|
||||
$cm = get_coursemodule_from_id('assign', $context->instanceid);
|
||||
|
||||
$gi = \grade_item::fetch([
|
||||
'itemtype' => 'mod',
|
||||
'itemmodule' => 'assign',
|
||||
'iteminstance' => $cm->instance,
|
||||
'courseid' => $resource->courseid
|
||||
]);
|
||||
}
|
||||
|
||||
if ($ggrade = \grade_grade::fetch(['itemid' => $gi->id, 'userid' => $userid])) {
|
||||
$ggrade->finalgrade = $grade;
|
||||
$ggrade->rawgrade = $grade;
|
||||
$ggrade->update();
|
||||
} else {
|
||||
$ggrade = new \grade_grade();
|
||||
$ggrade->itemid = $gi->id;
|
||||
$ggrade->userid = $userid;
|
||||
$ggrade->rawgrade = $grade;
|
||||
$ggrade->finalgrade = $grade;
|
||||
$ggrade->rawgrademax = 100;
|
||||
$ggrade->rawgrademin = 0;
|
||||
$ggrade->timecreated = time();
|
||||
$ggrade->timemodified = time();
|
||||
$ggrade->insert();
|
||||
}
|
||||
return floatval($ggrade->finalgrade / $gi->grademax);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to set the completion status for published course or module.
|
||||
*
|
||||
* @param \stdClass $resource the resource - either a course or module.
|
||||
* @param int $userid the id of the user to override the completion status for.
|
||||
* @param bool $complete whether the resource is deemed complete or not.
|
||||
*/
|
||||
protected function override_resource_completion_status_for_user(\stdClass $resource, int $userid,
|
||||
bool $complete): void {
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/accesslib.php');
|
||||
require_once($CFG->libdir . '/completionlib.php');
|
||||
require_once($CFG->libdir . '/datalib.php');
|
||||
$this->setAdminUser();
|
||||
$context = \context::instance_by_id($resource->contextid);
|
||||
$completion = new \completion_info(get_course($resource->courseid));
|
||||
if ($context->contextlevel == CONTEXT_COURSE) {
|
||||
$ccompletion = new \completion_completion(['userid' => $userid, 'course' => $resource->courseid]);
|
||||
if ($complete) {
|
||||
$ccompletion->mark_complete();
|
||||
} else {
|
||||
$completion->clear_criteria();
|
||||
}
|
||||
} else if ($context->contextlevel == CONTEXT_MODULE) {
|
||||
$completion->update_state(
|
||||
get_coursemodule_from_id('assign', $context->instanceid),
|
||||
$complete ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE,
|
||||
$userid,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test confirming task name.
|
||||
@ -153,175 +44,6 @@ class sync_grades_test extends \lti_advantage_testcase {
|
||||
$this->assertEquals(get_string('tasksyncgrades', 'enrol_lti'), (new sync_grades())->get_name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the sync grades task during several runs and for a series of grade changes.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_grade_sync_chronological_syncs() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
|
||||
$studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
|
||||
|
||||
$student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]);
|
||||
$student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1]);
|
||||
$student1user = $this->getDataGenerator()->create_user();
|
||||
$student2user = $this->getDataGenerator()->create_user();
|
||||
[$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
|
||||
[$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch);
|
||||
|
||||
// Grade student1 only.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
|
||||
|
||||
// Sync and verify that only student1's grade is sent.
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 1 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Sync again, verifying no grades are sent because nothing has changed.
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Not sent - The grade for the user '$student1id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id' was not sent as the grades are the same.",
|
||||
"Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 0 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Change student1's grade and add a grade for student2.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 68.5, $resource);
|
||||
$expectedstudent2grade = $this->set_user_grade_for_resource($student2id, 44.5, $resource);
|
||||
|
||||
// Sync again, verifying both grade changes are sent.
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Success - The grade '$expectedstudent2grade' for the user '$student2id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 2 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a grade sync when there are more than one resource link for the resource.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_grade_sync_multiple_resource_links() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
|
||||
// Launch the resource first for an instructor using the default resource link in the platform.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Launch again as the instructor, this time from a different resource link in the platform.
|
||||
$teachermocklaunch2 = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], 'RLID-2');
|
||||
$launchservice->user_launches_tool($instructoruser, $teachermocklaunch2);
|
||||
|
||||
// Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
|
||||
$studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
|
||||
|
||||
$student1reslink1launch = $this->get_mock_launch($resource, $studentusers[0]);
|
||||
$student2reslink1launch = $this->get_mock_launch($resource, $studentusers[1]);
|
||||
$student1reslink2launch = $this->get_mock_launch($resource, $studentusers[1], 'RLID-2');
|
||||
$student1user = $this->getDataGenerator()->create_user();
|
||||
$student2user = $this->getDataGenerator()->create_user();
|
||||
[$student1id] = $launchservice->user_launches_tool($student1user, $student1reslink1launch);
|
||||
[$student2id] = $launchservice->user_launches_tool($student2user, $student2reslink1launch);
|
||||
$launchservice->user_launches_tool($student1user, $student1reslink2launch);
|
||||
|
||||
// Grade student1 only.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
|
||||
|
||||
// Sync and verify that only student1's grade is sent but that it's sent for BOTH resource links.
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Found 2 resource link(s) for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id'. Attempting to sync grades for all.",
|
||||
"Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 1 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Verify that the grade was reported as being synced twice - once for each resource link.
|
||||
$expected = "/Found 2 resource link\(s\) for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id'. Attempting to sync grades for all.\n".
|
||||
"Processing resource link '.*'.\n".
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.\n".
|
||||
"Processing resource link '.*'.\n".
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent./";
|
||||
$this->assertMatchesRegularExpression($expected, $ob);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test grade sync when the resource has syncgrades disabled.
|
||||
*
|
||||
@ -338,7 +60,7 @@ class sync_grades_test extends \lti_advantage_testcase {
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
$launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
$task = new \enrol_lti\local\ltiadvantage\task\sync_grades();
|
||||
$this->expectOutputRegex('/Skipping task - There are no resources with grade sync enabled./');
|
||||
$task->execute();
|
||||
}
|
||||
@ -358,7 +80,7 @@ class sync_grades_test extends \lti_advantage_testcase {
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
$launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
$task = new \enrol_lti\local\ltiadvantage\task\sync_grades();
|
||||
$this->expectOutputRegex('/Skipping task - ' .
|
||||
get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti')) . '/');
|
||||
$task->execute();
|
||||
@ -379,488 +101,8 @@ class sync_grades_test extends \lti_advantage_testcase {
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
$launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
$task = new \enrol_lti\local\ltiadvantage\task\sync_grades();
|
||||
$this->expectOutputRegex('/Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti') . '/');
|
||||
$task->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the grade sync task when the launch data doesn't include the AGS support.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_no_service_endpoint() {
|
||||
$this->resetAfterTest();
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0],
|
||||
null, null);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$userid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
$this->expectOutputRegex(
|
||||
"/Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.\n".
|
||||
"Found 1 resource link\(s\) for the user '$userid', for the resource '$resource->id' and the ".
|
||||
"course '$course->id'. Attempting to sync grades for all.\n".
|
||||
"Processing resource link '.*'.\n".
|
||||
"Skipping - No grade service found for the user '$userid', for the resource '$resource->id' and the ".
|
||||
"course '$course->id'.\n".
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. Processed 1 users; ".
|
||||
"sent 0 grades./"
|
||||
);
|
||||
$task->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test syncing grades when the enrolment instance is disabled.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_disabled_instance() {
|
||||
$this->resetAfterTest();
|
||||
global $DB;
|
||||
|
||||
[$course, $resource, $resource2, $resource3] = $this->create_test_environment();
|
||||
|
||||
// Disable resource 1.
|
||||
$enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_DISABLED];
|
||||
$DB->update_record('enrol', $enrol);
|
||||
|
||||
// Delete the activity being shared by resource 2, leaving resource 2 disabled as a result.
|
||||
$modcontext = \context::instance_by_id($resource2->contextid);
|
||||
course_delete_module($modcontext->instanceid);
|
||||
|
||||
// Only the enabled resource 3 should sync grades.
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
$this->expectOutputRegex(
|
||||
"/^Starting - LTI Advantage grade sync for shared resource '$resource3->id' in course '$course->id'.\n".
|
||||
"Completed - Synced grades for tool '$resource3->id' in the course '$course->id'. Processed 0 users; ".
|
||||
"sent 0 grades.\n$/"
|
||||
);
|
||||
$task->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the grade sync when the context has been deleted in between launch and when the grade sync task is run.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_deleted_context() {
|
||||
$this->resetAfterTest();
|
||||
global $DB;
|
||||
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$userid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Delete the activity, then enable the enrolment method (it is disabled during activity deletion).
|
||||
$modcontext = \context::instance_by_id($resource->contextid);
|
||||
course_delete_module($modcontext->instanceid);
|
||||
$enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_ENABLED];
|
||||
$DB->update_record('enrol', $enrol);
|
||||
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
$this->expectOutputRegex(
|
||||
"/Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.\n".
|
||||
"Found 1 resource link\(s\) for the user '$userid', for the resource '$resource->id' and the ".
|
||||
"course '$course->id'. Attempting to sync grades for all.\n".
|
||||
"Processing resource link '.*'.\n".
|
||||
"Failed - Invalid contextid '$resource->contextid' for the resource '$resource->id'.\n".
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. Processed 1 users; ".
|
||||
"sent 0 grades./"
|
||||
);
|
||||
$task->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test grade sync when completion is required for the activity before sync takes place.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_completion_required() {
|
||||
$this->resetAfterTest();
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/completionlib.php');
|
||||
|
||||
[
|
||||
$course,
|
||||
$resource,
|
||||
$resource2,
|
||||
$publishedcourse
|
||||
] = $this->create_test_environment(true, true, false, helper::MEMBER_SYNC_ENROL_AND_UNENROL, true, true);
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$teacherid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
|
||||
$studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
|
||||
$student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]);
|
||||
$student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1]);
|
||||
$student1user = $this->getDataGenerator()->create_user();
|
||||
$student2user = $this->getDataGenerator()->create_user();
|
||||
[$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
|
||||
[$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch);
|
||||
|
||||
// Launch the published course as student2.
|
||||
$student2mockcourselaunch = $this->get_mock_launch($publishedcourse, $studentusers[1], '23456');
|
||||
$launchservice->user_launches_tool($student2user, $student2mockcourselaunch);
|
||||
|
||||
// Grade student1 in the assign resource.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
|
||||
|
||||
// And student2 in the course resource.
|
||||
$expectedstudent2grade = $this->set_user_grade_for_resource($student2id, 55.5, $publishedcourse);
|
||||
|
||||
// Sync and verify that no grades are sent because resource and published course are both not yet complete.
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ".
|
||||
"the course '$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 0 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Complete the resource for student1.
|
||||
$this->override_resource_completion_status_for_user($resource, $student1id, true);
|
||||
|
||||
// Run the sync again, this time confirming the grade for student1 is sent.
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 1 grades.",
|
||||
"Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.",
|
||||
"Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ".
|
||||
"the course '$course->id'.",
|
||||
"Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ".
|
||||
"Processed 1 users; sent 0 grades.",
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Fail completion for student1 and confirm no grade is sent, even despite it being changed.
|
||||
$this->set_user_grade_for_resource($student1id, 33.3, $resource);
|
||||
$this->override_resource_completion_status_for_user($resource, $student1id, false);
|
||||
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 0 grades.",
|
||||
"Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.",
|
||||
"Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ".
|
||||
"the course '$course->id'.",
|
||||
"Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ".
|
||||
"Processed 1 users; sent 0 grades.",
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Complete the course for student2 and verify the grade is now sent.
|
||||
$this->override_resource_completion_status_for_user($publishedcourse, $student2id, true);
|
||||
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 0 grades.",
|
||||
"Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.",
|
||||
"Success - The grade '$expectedstudent2grade' for the user '$student2id', for the resource ".
|
||||
"'$publishedcourse->id' and the course '$course->id' was sent.",
|
||||
"Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ".
|
||||
"Processed 1 users; sent 1 grades.",
|
||||
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Mark the course as in progress again for student2 and verify any new grade changes are not sent.
|
||||
$this->set_user_grade_for_resource($student2id, 78.8, $publishedcourse);
|
||||
$this->override_resource_completion_status_for_user($publishedcourse, $student2id, false);
|
||||
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ".
|
||||
"the course '$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 0 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test grade sync when the attempt to call the service returns an exception or a bad HTTP response code.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_failed_service_call() {
|
||||
$this->resetAfterTest();
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$task = $this->get_task_with_mocked_grade_service('200', true);
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$teacherid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Launch the resource for a student, creating the enrolment and allowing grading to take place.
|
||||
$studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
|
||||
$student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]);
|
||||
$student1user = $this->getDataGenerator()->create_user();
|
||||
[$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
|
||||
|
||||
// Grade student1 in the assign resource.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
|
||||
|
||||
// Run the sync, verifying that the response error causes a 'Failed' trace but that the task completes.
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Failed - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' failed to send.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 2 users; sent 0 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Now run the sync again, this time with a bad http response code.
|
||||
$task = $this->get_task_with_mocked_grade_service('400');
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Failed - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' failed to send.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 2 users; sent 0 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the sync when only the lineitem URL is provided and when lineitem creation/query isn't expected.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_coupled_lineitem() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
|
||||
// The launches use a coupled line item. Only scores can be posted. Line items and results cannot be created or queried.
|
||||
$agsclaim = [
|
||||
"scope" => ["https://purl.imsglobal.org/spec/lti-ags/scope/score"],
|
||||
"lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
|
||||
];
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null,
|
||||
$agsclaim);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
|
||||
$studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
|
||||
|
||||
$student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim);
|
||||
$student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim);
|
||||
$student1user = $this->getDataGenerator()->create_user();
|
||||
$student2user = $this->getDataGenerator()->create_user();
|
||||
[$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
|
||||
[$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch);
|
||||
|
||||
// Grade student1 only.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
|
||||
|
||||
// Mock task, asserting that score posting to an existing line item takes place, via a mock grade service object.
|
||||
$mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class);
|
||||
$mockgradeservice->method('putGrade')->willReturnCallback(function() {
|
||||
return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200];
|
||||
});
|
||||
$mockgradeservice->expects($this->never())
|
||||
->method('findOrCreateLineitem');
|
||||
$mockgradeservice->expects($this->once())
|
||||
->method('putGrade')
|
||||
->with($this->isInstanceOf(LtiGrade::class));
|
||||
$mocktask = $this->getMockBuilder(sync_grades::class)
|
||||
->onlyMethods(['get_ags'])
|
||||
->getMock();
|
||||
$mocktask->method('get_ags')->willReturn($mockgradeservice);
|
||||
|
||||
// Sync and verify that only student1's grade is sent.
|
||||
ob_start();
|
||||
$mocktask->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 1 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the sync when only the lineitems URL is provided and when line item creation/query is expected.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_none_or_many_lineitems() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
|
||||
// The launches omit the 'lineitem' claim, meaning the item may have none (or many) line items.
|
||||
$agsclaim = [
|
||||
"scope" => [
|
||||
"https://purl.imsglobal.org/spec/lti-ags/scope/score",
|
||||
"https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
|
||||
],
|
||||
"lineitems" => "https://platform.example.com/10/lineitems"
|
||||
];
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null,
|
||||
$agsclaim);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
|
||||
$studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
|
||||
|
||||
$student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim);
|
||||
$student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim);
|
||||
$student1user = $this->getDataGenerator()->create_user();
|
||||
$student2user = $this->getDataGenerator()->create_user();
|
||||
[$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
|
||||
[$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch);
|
||||
|
||||
// Grade student1 only.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
|
||||
|
||||
// Mock task, asserting that line item creation takes place via a mock grade service object.
|
||||
$mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class);
|
||||
$mockgradeservice->method('putGrade')->willReturnCallback(function() {
|
||||
return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200];
|
||||
});
|
||||
$mockgradeservice->expects($this->once())
|
||||
->method('findOrCreateLineitem');
|
||||
$mockgradeservice->expects($this->once())
|
||||
->method('putGrade')
|
||||
->with($this->isInstanceOf(LtiGrade::class), $this->isInstanceOf(LtiLineitem::class));
|
||||
$mocktask = $this->getMockBuilder(sync_grades::class)
|
||||
->onlyMethods(['get_ags'])
|
||||
->getMock();
|
||||
$mocktask->method('get_ags')->willReturn($mockgradeservice);
|
||||
|
||||
// Sync and verify that only student1's grade is sent.
|
||||
ob_start();
|
||||
$mocktask->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 1 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,827 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace enrol_lti\local\ltiadvantage\task;
|
||||
|
||||
use enrol_lti\helper;
|
||||
use Packback\Lti1p3\LtiAssignmentsGradesService;
|
||||
use Packback\Lti1p3\LtiGrade;
|
||||
use Packback\Lti1p3\LtiLineitem;
|
||||
use core\task\manager;
|
||||
use phpunit_util;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
|
||||
require_once(__DIR__ . '/../lti_advantage_testcase.php');
|
||||
|
||||
/**
|
||||
* Tests for the enrol_lti\local\ltiadvantage\task\sync_tool_grades adhoc task.
|
||||
*
|
||||
* @package enrol_lti
|
||||
* @copyright 2023 David Pesce <david.pesce@exputo.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @coversDefaultClass \enrol_lti\local\ltiadvantage\task\sync_tool_grades
|
||||
*/
|
||||
class sync_tool_grades_test extends \lti_advantage_testcase {
|
||||
/**
|
||||
* Get a task which has a mocked ags instance injected, meaning no real calls will be made to the platform.
|
||||
*
|
||||
* This allows us to test the behaviour of the task (in terms of which users are in scope and which grades are sent)
|
||||
* without needing to deal with any auth.
|
||||
*
|
||||
* @param string $statuscode the HTTP status code to simulate.
|
||||
* @param bool $mockexception whether to simulate an exception during the service call or not.
|
||||
* @return sync_grades instance of the task with a mocked ags instance inside.
|
||||
*/
|
||||
protected function get_task_with_mocked_grade_service($statuscode = '200', $mockexception = false): sync_tool_grades {
|
||||
$mockgradeservice = $this->getMockBuilder(LtiAssignmentsGradesService::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['putGrade'])
|
||||
->getMock();
|
||||
$mockgradeservice->method('putGrade')->willReturnCallback(function() use ($statuscode, $mockexception) {
|
||||
if ($mockexception) {
|
||||
throw new \Exception();
|
||||
}
|
||||
return ['headers' => ['httpstatus' => "HTTP/2 $statuscode OK"], 'body' => '', 'status' => $statuscode];
|
||||
});
|
||||
// Get a mock task, with the method 'get_ags()' mocked to return the mocked AGS instance.
|
||||
$mocktask = $this->getMockBuilder(sync_tool_grades::class)
|
||||
->onlyMethods(['get_ags'])
|
||||
->getMock();
|
||||
$mocktask->method('get_ags')->willReturn($mockgradeservice);
|
||||
return $mocktask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to set a grade for a user.
|
||||
*
|
||||
* @param int $userid the id of the user being graded.
|
||||
* @param float $grade the grade value, out of 100, to set for the user.
|
||||
* @param \stdClass $resource the published resource object.
|
||||
* @return float the fractional grade value expected to be used during sync.
|
||||
*/
|
||||
protected function set_user_grade_for_resource(int $userid, float $grade, \stdClass $resource): float {
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/accesslib.php');
|
||||
require_once($CFG->libdir . '/gradelib.php');
|
||||
$context = \context::instance_by_id($resource->contextid);
|
||||
|
||||
if ($context->contextlevel == CONTEXT_COURSE) {
|
||||
$gi = \grade_item::fetch_course_item($resource->courseid);
|
||||
} else if ($context->contextlevel == CONTEXT_MODULE) {
|
||||
$cm = get_coursemodule_from_id('assign', $context->instanceid);
|
||||
|
||||
$gi = \grade_item::fetch([
|
||||
'itemtype' => 'mod',
|
||||
'itemmodule' => 'assign',
|
||||
'iteminstance' => $cm->instance,
|
||||
'courseid' => $resource->courseid
|
||||
]);
|
||||
}
|
||||
|
||||
if ($ggrade = \grade_grade::fetch(['itemid' => $gi->id, 'userid' => $userid])) {
|
||||
$ggrade->finalgrade = $grade;
|
||||
$ggrade->rawgrade = $grade;
|
||||
$ggrade->update();
|
||||
} else {
|
||||
$ggrade = new \grade_grade();
|
||||
$ggrade->itemid = $gi->id;
|
||||
$ggrade->userid = $userid;
|
||||
$ggrade->rawgrade = $grade;
|
||||
$ggrade->finalgrade = $grade;
|
||||
$ggrade->rawgrademax = 100;
|
||||
$ggrade->rawgrademin = 0;
|
||||
$ggrade->timecreated = time();
|
||||
$ggrade->timemodified = time();
|
||||
$ggrade->insert();
|
||||
}
|
||||
return floatval($ggrade->finalgrade / $gi->grademax);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to set the completion status for published course or module.
|
||||
*
|
||||
* @param \stdClass $resource the resource - either a course or module.
|
||||
* @param int $userid the id of the user to override the completion status for.
|
||||
* @param bool $complete whether the resource is deemed complete or not.
|
||||
*/
|
||||
protected function override_resource_completion_status_for_user(\stdClass $resource, int $userid,
|
||||
bool $complete): void {
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/accesslib.php');
|
||||
require_once($CFG->libdir . '/completionlib.php');
|
||||
require_once($CFG->libdir . '/datalib.php');
|
||||
$this->setAdminUser();
|
||||
$context = \context::instance_by_id($resource->contextid);
|
||||
$completion = new \completion_info(get_course($resource->courseid));
|
||||
if ($context->contextlevel == CONTEXT_COURSE) {
|
||||
$ccompletion = new \completion_completion(['userid' => $userid, 'course' => $resource->courseid]);
|
||||
if ($complete) {
|
||||
$ccompletion->mark_complete();
|
||||
} else {
|
||||
$completion->clear_criteria();
|
||||
}
|
||||
} else if ($context->contextlevel == CONTEXT_MODULE) {
|
||||
$completion->update_state(
|
||||
get_coursemodule_from_id('assign', $context->instanceid),
|
||||
$complete ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE,
|
||||
$userid,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the sync grades task during several runs and for a series of grade changes.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_grade_sync_chronological_syncs() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
|
||||
$studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
|
||||
|
||||
$student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]);
|
||||
$student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1]);
|
||||
$student1user = $this->getDataGenerator()->create_user();
|
||||
$student2user = $this->getDataGenerator()->create_user();
|
||||
[$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
|
||||
[$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch);
|
||||
|
||||
// Grade student1 only.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
|
||||
|
||||
// Sync and verify that only student1's grade is sent.
|
||||
ob_start();
|
||||
$task->set_custom_data($resource);
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 1 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Sync again, verifying no grades are sent because nothing has changed.
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Not sent - The grade for the user '$student1id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id' was not sent as the grades are the same.",
|
||||
"Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 0 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Change student1's grade and add a grade for student2.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 68.5, $resource);
|
||||
$expectedstudent2grade = $this->set_user_grade_for_resource($student2id, 44.5, $resource);
|
||||
|
||||
// Sync again, verifying both grade changes are sent.
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Success - The grade '$expectedstudent2grade' for the user '$student2id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 2 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a grade sync when there are more than one resource link for the resource.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_grade_sync_multiple_resource_links() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
|
||||
// Launch the resource first for an instructor using the default resource link in the platform.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Launch again as the instructor, this time from a different resource link in the platform.
|
||||
$teachermocklaunch2 = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], 'RLID-2');
|
||||
$launchservice->user_launches_tool($instructoruser, $teachermocklaunch2);
|
||||
|
||||
// Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
|
||||
$studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
|
||||
|
||||
$student1reslink1launch = $this->get_mock_launch($resource, $studentusers[0]);
|
||||
$student2reslink1launch = $this->get_mock_launch($resource, $studentusers[1]);
|
||||
$student1reslink2launch = $this->get_mock_launch($resource, $studentusers[1], 'RLID-2');
|
||||
$student1user = $this->getDataGenerator()->create_user();
|
||||
$student2user = $this->getDataGenerator()->create_user();
|
||||
[$student1id] = $launchservice->user_launches_tool($student1user, $student1reslink1launch);
|
||||
[$student2id] = $launchservice->user_launches_tool($student2user, $student2reslink1launch);
|
||||
$launchservice->user_launches_tool($student1user, $student1reslink2launch);
|
||||
|
||||
// Grade student1 only.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
|
||||
|
||||
// Sync and verify that only student1's grade is sent but that it's sent for BOTH resource links.
|
||||
ob_start();
|
||||
$task->set_custom_data($resource);
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Found 2 resource link(s) for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id'. Attempting to sync grades for all.",
|
||||
"Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 1 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Verify that the grade was reported as being synced twice - once for each resource link.
|
||||
$expected = "/Found 2 resource link\(s\) for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id'. Attempting to sync grades for all.\n".
|
||||
"Processing resource link '.*'.\n".
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.\n".
|
||||
"Processing resource link '.*'.\n".
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent./";
|
||||
$this->assertMatchesRegularExpression($expected, $ob);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the grade sync task when the launch data doesn't include the AGS support.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_no_service_endpoint() {
|
||||
$this->resetAfterTest();
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0],
|
||||
null, null);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$userid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
$task->set_custom_data($resource);
|
||||
$this->expectOutputRegex(
|
||||
"/Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.\n".
|
||||
"Found 1 resource link\(s\) for the user '$userid', for the resource '$resource->id' and the ".
|
||||
"course '$course->id'. Attempting to sync grades for all.\n".
|
||||
"Processing resource link '.*'.\n".
|
||||
"Skipping - No grade service found for the user '$userid', for the resource '$resource->id' and the ".
|
||||
"course '$course->id'.\n".
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. Processed 1 users; ".
|
||||
"sent 0 grades./"
|
||||
);
|
||||
$task->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test syncing grades when the enrolment instance is disabled.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_disabled_instance() {
|
||||
$this->resetAfterTest();
|
||||
global $DB;
|
||||
|
||||
[$course, $resource, $resource2, $resource3] = $this->create_test_environment();
|
||||
|
||||
// Disable resource 1.
|
||||
$enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_DISABLED];
|
||||
$DB->update_record('enrol', $enrol);
|
||||
|
||||
// Delete the activity being shared by resource 2, leaving resource 2 disabled as a result.
|
||||
$modcontext = \context::instance_by_id($resource2->contextid);
|
||||
course_delete_module($modcontext->instanceid);
|
||||
|
||||
// Only the enabled resource 3 should sync grades.
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
$task->set_custom_data($resource3);
|
||||
$this->expectOutputRegex(
|
||||
"/^Starting - LTI Advantage grade sync for shared resource '$resource3->id' in course '$course->id'.\n".
|
||||
"Completed - Synced grades for tool '$resource3->id' in the course '$course->id'. Processed 0 users; ".
|
||||
"sent 0 grades.\n$/"
|
||||
);
|
||||
$task->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the grade sync when the context has been deleted in between launch and when the grade sync task is run.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_deleted_context() {
|
||||
$this->resetAfterTest();
|
||||
global $DB;
|
||||
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$userid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Delete the activity, then enable the enrolment method (it is disabled during activity deletion).
|
||||
$modcontext = \context::instance_by_id($resource->contextid);
|
||||
course_delete_module($modcontext->instanceid);
|
||||
$enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_ENABLED];
|
||||
$DB->update_record('enrol', $enrol);
|
||||
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
$task->set_custom_data($resource);
|
||||
$this->expectOutputRegex(
|
||||
"/Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.\n".
|
||||
"Found 1 resource link\(s\) for the user '$userid', for the resource '$resource->id' and the ".
|
||||
"course '$course->id'. Attempting to sync grades for all.\n".
|
||||
"Processing resource link '.*'.\n".
|
||||
"Failed - Invalid contextid '$resource->contextid' for the resource '$resource->id'.\n".
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. Processed 1 users; ".
|
||||
"sent 0 grades./"
|
||||
);
|
||||
$task->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test grade sync when completion is required for the activity before sync takes place.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_completion_required() {
|
||||
$this->resetAfterTest();
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/completionlib.php');
|
||||
|
||||
[
|
||||
$course,
|
||||
$resource,
|
||||
$resource2,
|
||||
$publishedcourse
|
||||
] = $this->create_test_environment(true, true, false, helper::MEMBER_SYNC_ENROL_AND_UNENROL, true, true);
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$task = $this->get_task_with_mocked_grade_service();
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$teacherid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
|
||||
$studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
|
||||
$student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]);
|
||||
$student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1]);
|
||||
$student1user = $this->getDataGenerator()->create_user();
|
||||
$student2user = $this->getDataGenerator()->create_user();
|
||||
[$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
|
||||
[$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch);
|
||||
|
||||
// Launch the published course as student2.
|
||||
$student2mockcourselaunch = $this->get_mock_launch($publishedcourse, $studentusers[1], '23456');
|
||||
$launchservice->user_launches_tool($student2user, $student2mockcourselaunch);
|
||||
|
||||
// Grade student1 in the assign resource.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
|
||||
|
||||
// And student2 in the course resource.
|
||||
$expectedstudent2grade = $this->set_user_grade_for_resource($student2id, 55.5, $publishedcourse);
|
||||
|
||||
// Since adhoc tasks are queued via sync_grades scheduled task, we need to create a queue.
|
||||
$allitems = array($resource, $resource2, $publishedcourse);
|
||||
|
||||
// Sync and verify that no grades are sent because resource and published course are both not yet complete.
|
||||
ob_start();
|
||||
foreach ($allitems as $item) {
|
||||
$task->set_custom_data($item);
|
||||
$task->execute();
|
||||
}
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ".
|
||||
"the course '$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 0 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Complete the resource for student1.
|
||||
$this->override_resource_completion_status_for_user($resource, $student1id, true);
|
||||
|
||||
// Run the sync again, this time confirming the grade for student1 is sent.
|
||||
ob_start();
|
||||
foreach ($allitems as $item) {
|
||||
$task->set_custom_data($item);
|
||||
$task->execute();
|
||||
}
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 1 grades.",
|
||||
"Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.",
|
||||
"Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ".
|
||||
"the course '$course->id'.",
|
||||
"Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ".
|
||||
"Processed 1 users; sent 0 grades.",
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Fail completion for student1 and confirm no grade is sent, even despite it being changed.
|
||||
$this->set_user_grade_for_resource($student1id, 33.3, $resource);
|
||||
$this->override_resource_completion_status_for_user($resource, $student1id, false);
|
||||
|
||||
ob_start();
|
||||
foreach ($allitems as $item) {
|
||||
$task->set_custom_data($item);
|
||||
$task->execute();
|
||||
}
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 0 grades.",
|
||||
"Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.",
|
||||
"Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ".
|
||||
"the course '$course->id'.",
|
||||
"Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ".
|
||||
"Processed 1 users; sent 0 grades.",
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Complete the course for student2 and verify the grade is now sent.
|
||||
$this->override_resource_completion_status_for_user($publishedcourse, $student2id, true);
|
||||
|
||||
ob_start();
|
||||
foreach ($allitems as $item) {
|
||||
$task->set_custom_data($item);
|
||||
$task->execute();
|
||||
}
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 0 grades.",
|
||||
"Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.",
|
||||
"Success - The grade '$expectedstudent2grade' for the user '$student2id', for the resource ".
|
||||
"'$publishedcourse->id' and the course '$course->id' was sent.",
|
||||
"Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ".
|
||||
"Processed 1 users; sent 1 grades.",
|
||||
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Mark the course as in progress again for student2 and verify any new grade changes are not sent.
|
||||
$this->set_user_grade_for_resource($student2id, 78.8, $publishedcourse);
|
||||
$this->override_resource_completion_status_for_user($publishedcourse, $student2id, false);
|
||||
|
||||
ob_start();
|
||||
foreach ($allitems as $item) {
|
||||
$task->set_custom_data($item);
|
||||
$task->execute();
|
||||
}
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ".
|
||||
"the course '$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 0 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test grade sync when the attempt to call the service returns an exception or a bad HTTP response code.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_failed_service_call() {
|
||||
$this->resetAfterTest();
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$task = $this->get_task_with_mocked_grade_service('200', true);
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$teacherid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Launch the resource for a student, creating the enrolment and allowing grading to take place.
|
||||
$studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
|
||||
$student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]);
|
||||
$student1user = $this->getDataGenerator()->create_user();
|
||||
[$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
|
||||
|
||||
// Grade student1 in the assign resource.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
|
||||
|
||||
// Run the sync, verifying that the response error causes a 'Failed' trace but that the task completes.
|
||||
ob_start();
|
||||
$task->set_custom_data($resource);
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Failed - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' failed to send.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 2 users; sent 0 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
|
||||
// Now run the sync again, this time with a bad http response code.
|
||||
$task = $this->get_task_with_mocked_grade_service('400');
|
||||
ob_start();
|
||||
$task->set_custom_data($resource);
|
||||
$task->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Failed - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' failed to send.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 2 users; sent 0 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the sync when only the lineitem URL is provided and when lineitem creation/query isn't expected.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_coupled_lineitem() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
|
||||
// The launches use a coupled line item. Only scores can be posted. Line items and results cannot be created or queried.
|
||||
$agsclaim = [
|
||||
"scope" => ["https://purl.imsglobal.org/spec/lti-ags/scope/score"],
|
||||
"lineitem" => "https://platform.example.com/10/lineitems/45/lineitem"
|
||||
];
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null,
|
||||
$agsclaim);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
|
||||
$studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
|
||||
|
||||
$student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim);
|
||||
$student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim);
|
||||
$student1user = $this->getDataGenerator()->create_user();
|
||||
$student2user = $this->getDataGenerator()->create_user();
|
||||
[$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
|
||||
[$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch);
|
||||
|
||||
// Grade student1 only.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
|
||||
|
||||
// Mock task, asserting that score posting to an existing line item takes place, via a mock grade service object.
|
||||
$mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class);
|
||||
$mockgradeservice->method('putGrade')->willReturnCallback(function() {
|
||||
return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200];
|
||||
});
|
||||
$mockgradeservice->expects($this->never())
|
||||
->method('findOrCreateLineitem');
|
||||
$mockgradeservice->expects($this->once())
|
||||
->method('putGrade')
|
||||
->with($this->isInstanceOf(LtiGrade::class));
|
||||
$mocktask = $this->getMockBuilder(sync_tool_grades::class)
|
||||
->onlyMethods(['get_ags'])
|
||||
->getMock();
|
||||
$mocktask->method('get_ags')->willReturn($mockgradeservice);
|
||||
|
||||
// Sync and verify that only student1's grade is sent.
|
||||
ob_start();
|
||||
$mocktask->set_custom_data($resource);
|
||||
$mocktask->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 1 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the sync when only the lineitems URL is provided and when line item creation/query is expected.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_sync_grades_none_or_many_lineitems() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
[$course, $resource] = $this->create_test_environment();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
|
||||
// The launches omit the 'lineitem' claim, meaning the item may have none (or many) line items.
|
||||
$agsclaim = [
|
||||
"scope" => [
|
||||
"https://purl.imsglobal.org/spec/lti-ags/scope/score",
|
||||
"https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
|
||||
],
|
||||
"lineitems" => "https://platform.example.com/10/lineitems"
|
||||
];
|
||||
|
||||
// Launch the resource for an instructor which will create the domain objects needed for service calls.
|
||||
$teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null,
|
||||
$agsclaim);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
[$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch);
|
||||
|
||||
// Launch the resource for a few more users, creating those enrolments and allowing grading to take place.
|
||||
$studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner');
|
||||
|
||||
$student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim);
|
||||
$student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim);
|
||||
$student1user = $this->getDataGenerator()->create_user();
|
||||
$student2user = $this->getDataGenerator()->create_user();
|
||||
[$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch);
|
||||
[$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch);
|
||||
|
||||
// Grade student1 only.
|
||||
$expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource);
|
||||
|
||||
// Mock task, asserting that line item creation takes place via a mock grade service object.
|
||||
$mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class);
|
||||
$mockgradeservice->method('putGrade')->willReturnCallback(function() {
|
||||
return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200];
|
||||
});
|
||||
$mockgradeservice->expects($this->once())
|
||||
->method('findOrCreateLineitem');
|
||||
$mockgradeservice->expects($this->once())
|
||||
->method('putGrade')
|
||||
->with($this->isInstanceOf(LtiGrade::class), $this->isInstanceOf(LtiLineitem::class));
|
||||
$mocktask = $this->getMockBuilder(sync_tool_grades::class)
|
||||
->onlyMethods(['get_ags'])
|
||||
->getMock();
|
||||
$mocktask->method('get_ags')->willReturn($mockgradeservice);
|
||||
|
||||
// Sync and verify that only student1's grade is sent.
|
||||
ob_start();
|
||||
$mocktask->set_custom_data($resource);
|
||||
$mocktask->execute();
|
||||
$ob = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$expectedtraces = [
|
||||
"Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.",
|
||||
"Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ".
|
||||
"'$resource->id' and the course '$course->id' was sent.",
|
||||
"Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ".
|
||||
"'$course->id'.",
|
||||
"Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ".
|
||||
"Processed 3 users; sent 1 grades."
|
||||
];
|
||||
foreach ($expectedtraces as $expectedtrace) {
|
||||
$this->assertStringContainsString($expectedtrace, $ob);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user