Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   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  }