<?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\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.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sync_grades extends scheduled_task {
/**
* Get a descriptive name for this task.
*
* @return string
*/
public function get_name() {
return get_string('tasksyncgrades', 'enrol_lti');
}
/**
< * 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')));
return true;
}
if (!enrol_is_enabled('lti')) {
mtrace('Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti'));
return true;
}
$resources = helper::get_lti_tools([
'status' => ENROL_INSTANCE_ENABLED,
'gradesync' => 1,
'ltiversion' => 'LTI-1p3'
]);
if (empty($resources)) {
mtrace('Skipping task - There are no resources with grade sync enabled.');
return true;
}
foreach ($resources as $resource) {
< 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("");
> $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);
}
>
}
> mtrace('Spawned ' . count($resources) . ' adhoc tasks to sync grades.');
}