Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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  }