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