1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace enrol_lti\local\ltiadvantage\task; 18 19 use core\http_client; 20 use core\task\adhoc_task; 21 use enrol_lti\local\ltiadvantage\lib\issuer_database; 22 use enrol_lti\local\ltiadvantage\lib\launch_cache_session; 23 use enrol_lti\local\ltiadvantage\repository\application_registration_repository; 24 use enrol_lti\local\ltiadvantage\repository\deployment_repository; 25 use enrol_lti\local\ltiadvantage\repository\resource_link_repository; 26 use enrol_lti\local\ltiadvantage\repository\user_repository; 27 use Packback\Lti1p3\LtiAssignmentsGradesService; 28 use Packback\Lti1p3\LtiGrade; 29 use Packback\Lti1p3\LtiLineitem; 30 use Packback\Lti1p3\LtiRegistration; 31 use Packback\Lti1p3\LtiServiceConnector; 32 33 /** 34 * LTI Advantage task responsible for pushing grades to tool platforms. 35 * 36 * @package enrol_lti 37 * @copyright 2023 David Pesce <david.pesce@exputo.com> 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 class sync_tool_grades extends adhoc_task { 41 42 /** 43 * Sync grades to the platform using the Assignment and Grade Services (AGS). 44 * 45 * @param \stdClass $resource the enrol_lti_tools data record for the shared resource. 46 * @return array an array containing the 47 */ 48 protected function sync_grades_for_resource($resource): array { 49 $usercount = 0; 50 $sendcount = 0; 51 $userrepo = new user_repository(); 52 $resourcelinkrepo = new resource_link_repository(); 53 $appregistrationrepo = new application_registration_repository(); 54 $issuerdb = new issuer_database($appregistrationrepo, new deployment_repository()); 55 56 if ($users = $userrepo->find_by_resource($resource->id)) { 57 $completion = new \completion_info(get_course($resource->courseid)); 58 $syncedusergrades = []; // Keep track of those users who have had their grade synced during this run. 59 foreach ($users as $user) { 60 $mtracecontent = "for the user '{$user->get_localid()}', for the resource '$resource->id' and the course " . 61 "'$resource->courseid'"; 62 $usercount++; 63 64 // Check if we do not have a grade service endpoint in either of the resource links. 65 // Remember, not all launches need to support grade services. 66 $userresourcelinks = $resourcelinkrepo->find_by_resource_and_user($resource->id, $user->get_id()); 67 $userlastgrade = $user->get_lastgrade(); 68 mtrace("Found ".count($userresourcelinks)." resource link(s) $mtracecontent. Attempting to sync grades for all."); 69 70 foreach ($userresourcelinks as $userresourcelink) { 71 mtrace("Processing resource link '{$userresourcelink->get_resourcelinkid()}'."); 72 if (!$gradeservice = $userresourcelink->get_grade_service()) { 73 mtrace("Skipping - No grade service found $mtracecontent."); 74 continue; 75 } 76 77 if (!$context = \context::instance_by_id($resource->contextid, IGNORE_MISSING)) { 78 mtrace("Failed - Invalid contextid '$resource->contextid' for the resource '$resource->id'."); 79 continue; 80 } 81 82 $grade = false; 83 $dategraded = false; 84 if ($context->contextlevel == CONTEXT_COURSE) { 85 if ($resource->gradesynccompletion && !$completion->is_course_complete($user->get_localid())) { 86 mtrace("Skipping - Course not completed $mtracecontent."); 87 continue; 88 } 89 90 // Get the grade. 91 if ($grade = grade_get_course_grade($user->get_localid(), $resource->courseid)) { 92 $grademax = floatval($grade->item->grademax); 93 $dategraded = $grade->dategraded; 94 $grade = $grade->grade; 95 } 96 } else if ($context->contextlevel == CONTEXT_MODULE) { 97 $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST); 98 99 if ($resource->gradesynccompletion) { 100 $data = $completion->get_data($cm, false, $user->get_localid()); 101 if (!in_array($data->completionstate, [COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE])) { 102 mtrace("Skipping - Activity not completed $mtracecontent."); 103 continue; 104 } 105 } 106 107 $grades = grade_get_grades($cm->course, 'mod', $cm->modname, $cm->instance, 108 $user->get_localid()); 109 if (!empty($grades->items[0]->grades)) { 110 $grade = reset($grades->items[0]->grades); 111 if (!empty($grade->item)) { 112 $grademax = floatval($grade->item->grademax); 113 } else { 114 $grademax = floatval($grades->items[0]->grademax); 115 } 116 $dategraded = $grade->dategraded; 117 $grade = $grade->grade; 118 } 119 } 120 121 if ($grade === false || $grade === null || strlen($grade) < 1) { 122 mtrace("Skipping - Invalid grade $mtracecontent."); 123 continue; 124 } 125 126 if (empty($grademax)) { 127 mtrace("Skipping - Invalid grademax $mtracecontent."); 128 continue; 129 } 130 131 if (!grade_floats_different($grade, $userlastgrade)) { 132 mtrace("Not sent - The grade $mtracecontent was not sent as the grades are the same."); 133 continue; 134 } 135 $floatgrade = $grade / $grademax; 136 137 try { 138 // Get an AGS instance for the corresponding application registration and service data. 139 $appregistration = $appregistrationrepo->find_by_deployment( 140 $userresourcelink->get_deploymentid() 141 ); 142 $registration = $issuerdb->findRegistrationByIssuer( 143 $appregistration->get_platformid()->out(false), 144 $appregistration->get_clientid() 145 ); 146 global $CFG; 147 require_once($CFG->libdir . '/filelib.php'); 148 $sc = new LtiServiceConnector(new launch_cache_session(), new http_client()); 149 150 $lineitemurl = $gradeservice->get_lineitemurl(); 151 $lineitemsurl = $gradeservice->get_lineitemsurl(); 152 $servicedata = [ 153 'lineitems' => $lineitemsurl ? $lineitemsurl->out(false) : null, 154 'lineitem' => $lineitemurl ? $lineitemurl->out(false) : null, 155 'scope' => $gradeservice->get_scopes(), 156 ]; 157 158 $ags = $this->get_ags($sc, $registration, $servicedata); 159 $ltigrade = LtiGrade::new() 160 ->setScoreGiven($grade) 161 ->setScoreMaximum($grademax) 162 ->setUserId($user->get_sourceid()) 163 ->setTimestamp(date(\DateTimeInterface::ISO8601, $dategraded)) 164 ->setActivityProgress('Completed') 165 ->setGradingProgress('FullyGraded'); 166 167 if (empty($servicedata['lineitem'])) { 168 // The launch did not include a couple lineitem, so find or create the line item for grading. 169 $lineitem = $ags->findOrCreateLineitem(new LtiLineitem([ 170 'label' => $this->get_line_item_label($resource, $context), 171 'scoreMaximum' => $grademax, 172 'tag' => 'grade', 173 'resourceId' => $userresourcelink->get_resourceid(), 174 'resourceLinkId' => $userresourcelink->get_resourcelinkid() 175 ])); 176 $response = $ags->putGrade($ltigrade, $lineitem); 177 } else { 178 // Let AGS find the coupled line item. 179 $response = $ags->putGrade($ltigrade); 180 } 181 182 } catch (\Exception $e) { 183 mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send."); 184 mtrace($e->getMessage()); 185 continue; 186 } 187 188 $successresponses = [200, 201, 202, 204]; 189 if (in_array($response['status'], $successresponses)) { 190 $user->set_lastgrade(grade_floatval($grade)); 191 $syncedusergrades[$user->get_id()] = $user; 192 mtrace("Success - The grade '$floatgrade' $mtracecontent was sent."); 193 } else { 194 mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send."); 195 mtrace("Header: {$response['headers']['httpstatus']}"); 196 } 197 } 198 } 199 // Update the lastgrade value for any users who had a grade synced. Allows skipping on future runs if not changed. 200 // Update the count of total users having their grades synced, not the total number of grade sync calls made. 201 foreach ($syncedusergrades as $ltiuser) { 202 $userrepo->save($ltiuser); 203 $sendcount = $sendcount + 1; 204 } 205 } 206 return [$usercount, $sendcount]; 207 } 208 209 /** 210 * Get the string label for the line item associated with the resource, based on the course or module name. 211 * 212 * @param \stdClass $resource the enrol_lti_tools record. 213 * @param \context $context the context of the resource - either course or module. 214 * @return string the label to use in the line item. 215 */ 216 protected function get_line_item_label(\stdClass $resource, \context $context): string { 217 $resourcename = 'default'; 218 if ($context->contextlevel == CONTEXT_COURSE) { 219 global $DB; 220 $coursenamesql = "SELECT c.fullname 221 FROM {enrol_lti_tools} t 222 JOIN {enrol} e 223 ON (e.id = t.enrolid) 224 JOIN {course} c 225 ON (c.id = e.courseid) 226 WHERE t.id = :resourceid"; 227 $coursename = $DB->get_field_sql($coursenamesql, ['resourceid' => $resource->id]); 228 $resourcename = format_string($coursename, true, ['context' => $context->id]); 229 } else if ($context->contextlevel == CONTEXT_MODULE) { 230 foreach (get_fast_modinfo($resource->courseid)->get_cms() as $mod) { 231 if ($mod->context->id == $context->id) { 232 $resourcename = $mod->name; 233 } 234 } 235 } 236 return $resourcename; 237 } 238 239 /** 240 * Get an Assignment and Grade Services (AGS) instance to make the call to the platform. 241 * 242 * @param LtiServiceConnector $sc a service connector instance. 243 * @param LtiRegistration $registration the registration instance. 244 * @param array $sd the service data. 245 * @return LtiAssignmentsGradesService 246 */ 247 protected function get_ags(LtiServiceConnector $sc, LtiRegistration $registration, array $sd): LtiAssignmentsGradesService { 248 return new LtiAssignmentsGradesService($sc, $registration, $sd); 249 } 250 251 /** 252 * Performs the synchronisation of grades from the tool to any registered platforms. 253 * 254 * @return bool|void 255 */ 256 public function execute() { 257 global $CFG; 258 259 require_once($CFG->dirroot . '/lib/completionlib.php'); 260 require_once($CFG->libdir . '/gradelib.php'); 261 require_once($CFG->dirroot . '/grade/querylib.php'); 262 263 $resource = $this->get_custom_data(); 264 265 mtrace("Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$resource->courseid'."); 266 267 [$usercount, $sendcount] = $this->sync_grades_for_resource($resource); 268 269 mtrace("Completed - Synced grades for tool '$resource->id' in the course '$resource->courseid'. " . 270 "Processed $usercount users; sent $sendcount grades."); 271 mtrace(""); 272 273 } 274 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body