See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
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 /** 18 * This file contains a class definition for the LTI Gradebook Services 19 * 20 * @package ltiservice_gradebookservices 21 * @copyright 2017 Cengage Learning http://www.cengage.com 22 * @author Dirk Singels, Diego del Blanco, Claude Vervoort 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace ltiservice_gradebookservices\local\service; 27 28 use ltiservice_gradebookservices\local\resources\lineitem; 29 use ltiservice_gradebookservices\local\resources\lineitems; 30 use ltiservice_gradebookservices\local\resources\results; 31 use ltiservice_gradebookservices\local\resources\scores; 32 use mod_lti\local\ltiservice\resource_base; 33 use mod_lti\local\ltiservice\service_base; 34 use moodle_url; 35 36 defined('MOODLE_INTERNAL') || die(); 37 38 global $CFG; 39 require_once($CFG->dirroot . '/mod/lti/locallib.php'); 40 41 /** 42 * A service implementing LTI Gradebook Services. 43 * 44 * @package ltiservice_gradebookservices 45 * @copyright 2017 Cengage Learning http://www.cengage.com 46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 47 */ 48 class gradebookservices extends service_base { 49 50 /** Read-only access to Gradebook services */ 51 const GRADEBOOKSERVICES_READ = 1; 52 /** Full access to Gradebook services */ 53 const GRADEBOOKSERVICES_FULL = 2; 54 /** Scope for full access to Lineitem service */ 55 const SCOPE_GRADEBOOKSERVICES_LINEITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'; 56 /** Scope for full access to Lineitem service */ 57 const SCOPE_GRADEBOOKSERVICES_LINEITEM_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly'; 58 /** Scope for access to Result service */ 59 const SCOPE_GRADEBOOKSERVICES_RESULT_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly'; 60 /** Scope for access to Score service */ 61 const SCOPE_GRADEBOOKSERVICES_SCORE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score'; 62 63 64 /** 65 * Class constructor. 66 */ 67 public function __construct() { 68 69 parent::__construct(); 70 $this->id = 'gradebookservices'; 71 $this->name = get_string($this->get_component_id(), $this->get_component_id()); 72 73 } 74 75 /** 76 * Get the resources for this service. 77 * 78 * @return resource_base[] 79 */ 80 public function get_resources() { 81 82 // The containers should be ordered in the array after their elements. 83 // Lineitems should be after lineitem. 84 if (empty($this->resources)) { 85 $this->resources = array(); 86 $this->resources[] = new lineitem($this); 87 $this->resources[] = new lineitems($this); 88 $this->resources[] = new results($this); 89 $this->resources[] = new scores($this); 90 } 91 92 return $this->resources; 93 } 94 95 /** 96 * Get the scope(s) permitted for this service. 97 * 98 * @return array 99 */ 100 public function get_permitted_scopes() { 101 102 $scopes = array(); 103 $ok = !empty($this->get_type()); 104 if ($ok && isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) { 105 if (!empty($setting = $this->get_typeconfig()['ltiservice_gradesynchronization'])) { 106 $scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ; 107 $scopes[] = self::SCOPE_GRADEBOOKSERVICES_RESULT_READ; 108 $scopes[] = self::SCOPE_GRADEBOOKSERVICES_SCORE; 109 if ($setting == self::GRADEBOOKSERVICES_FULL) { 110 $scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM; 111 } 112 } 113 } 114 115 return $scopes; 116 117 } 118 119 /** 120 * Get the scopes defined by this service. 121 * 122 * @return array 123 */ 124 public function get_scopes() { 125 return [self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ, self::SCOPE_GRADEBOOKSERVICES_RESULT_READ, 126 self::SCOPE_GRADEBOOKSERVICES_SCORE, self::SCOPE_GRADEBOOKSERVICES_LINEITEM]; 127 } 128 129 /** 130 * Adds form elements for gradebook sync add/edit page. 131 * 132 * @param \MoodleQuickForm $mform Moodle quickform object definition 133 */ 134 public function get_configuration_options(&$mform) { 135 136 $selectelementname = 'ltiservice_gradesynchronization'; 137 $identifier = 'grade_synchronization'; 138 $options = [ 139 get_string('nevergs', $this->get_component_id()), 140 get_string('partialgs', $this->get_component_id()), 141 get_string('alwaysgs', $this->get_component_id()) 142 ]; 143 144 $mform->addElement('select', $selectelementname, get_string($identifier, $this->get_component_id()), $options); 145 $mform->setType($selectelementname, 'int'); 146 $mform->setDefault($selectelementname, 0); 147 $mform->addHelpButton($selectelementname, $identifier, $this->get_component_id()); 148 } 149 150 /** 151 * For submission review, if there is a dedicated URL, use it as the target link. 152 * 153 * @param string $messagetype message type for this launch 154 * @param string $targetlinkuri current target link uri 155 * @param string|null $customstr concatenated list of custom parameters 156 * @param int $courseid 157 * @param null|object $lti LTI Instance. 158 * 159 * @return array containing the target link URL and the custom params string to use. 160 */ 161 public function override_endpoint(string $messagetype, string $targetlinkuri, ?string $customstr, int $courseid, 162 ?object $lti = null): array { 163 global $DB; 164 if ($messagetype == 'LtiSubmissionReviewRequest' && isset($lti->id)) { 165 $conditions = array('courseid' => $courseid, 'ltilinkid' => $lti->id); 166 $coupledlineitems = $DB->get_records('ltiservice_gradebookservices', $conditions); 167 if (count($coupledlineitems) == 1) { 168 $item = reset($coupledlineitems); 169 $url = $item->subreviewurl; 170 $subreviewparams = $item->subreviewparams; 171 if (!empty($url) && $url != 'DEFAULT') { 172 $targetlinkuri = $url; 173 } 174 if (!empty($subreviewparams)) { 175 if (!empty($customstr)) { 176 $customstr .= "\n{$subreviewparams}"; 177 } else { 178 $customstr = $subreviewparams; 179 } 180 } 181 } 182 } 183 return [$targetlinkuri, $customstr]; 184 } 185 186 /** 187 * Return an array of key/claim mapping allowing LTI 1.1 custom parameters 188 * to be transformed to LTI 1.3 claims. 189 * 190 * @return array Key/value pairs of params to claim mapping. 191 */ 192 public function get_jwt_claim_mappings(): array { 193 return [ 194 'custom_gradebookservices_scope' => [ 195 'suffix' => 'ags', 196 'group' => 'endpoint', 197 'claim' => 'scope', 198 'isarray' => true 199 ], 200 'custom_lineitems_url' => [ 201 'suffix' => 'ags', 202 'group' => 'endpoint', 203 'claim' => 'lineitems', 204 'isarray' => false 205 ], 206 'custom_lineitem_url' => [ 207 'suffix' => 'ags', 208 'group' => 'endpoint', 209 'claim' => 'lineitem', 210 'isarray' => false 211 ], 212 'custom_results_url' => [ 213 'suffix' => 'ags', 214 'group' => 'endpoint', 215 'claim' => 'results', 216 'isarray' => false 217 ], 218 'custom_result_url' => [ 219 'suffix' => 'ags', 220 'group' => 'endpoint', 221 'claim' => 'result', 222 'isarray' => false 223 ], 224 'custom_scores_url' => [ 225 'suffix' => 'ags', 226 'group' => 'endpoint', 227 'claim' => 'scores', 228 'isarray' => false 229 ], 230 'custom_score_url' => [ 231 'suffix' => 'ags', 232 'group' => 'endpoint', 233 'claim' => 'score', 234 'isarray' => false 235 ] 236 ]; 237 } 238 239 /** 240 * Return an array of key/values to add to the launch parameters. 241 * 242 * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'. 243 * @param string $courseid the course id. 244 * @param object $user The user id. 245 * @param string $typeid The tool lti type id. 246 * @param string $modlti The id of the lti activity. 247 * 248 * The type is passed to check the configuration 249 * and not return parameters for services not used. 250 * 251 * @return array of key/value pairs to add as launch parameters. 252 */ 253 public function get_launch_parameters($messagetype, $courseid, $user, $typeid, $modlti = null) { 254 global $DB; 255 $launchparameters = array(); 256 $this->set_type(lti_get_type($typeid)); 257 $this->set_typeconfig(lti_get_type_config($typeid)); 258 // Only inject parameters if the service is enabled for this tool. 259 if (isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) { 260 if ($this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_READ || 261 $this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_FULL) { 262 // Check for used in context is only needed because there is no explicit site tool - course relation. 263 if ($this->is_allowed_in_context($typeid, $courseid)) { 264 $id = null; 265 if (!is_null($modlti)) { 266 $conditions = array('courseid' => $courseid, 'itemtype' => 'mod', 267 'itemmodule' => 'lti', 'iteminstance' => $modlti); 268 269 $coupledlineitems = $DB->get_records('grade_items', $conditions); 270 $conditionsgbs = array('courseid' => $courseid, 'ltilinkid' => $modlti); 271 $lineitemsgbs = $DB->get_records('ltiservice_gradebookservices', $conditionsgbs); 272 // If a link has more that one attached grade items, per spec we do not populate line item url. 273 if (count($lineitemsgbs) == 1) { 274 $id = reset($lineitemsgbs)->gradeitemid; 275 } 276 if (count($lineitemsgbs) < 2 && count($coupledlineitems) == 1) { 277 $coupledid = reset($coupledlineitems)->id; 278 if (!is_null($id) && $id != $coupledid) { 279 $id = null; 280 } else { 281 $id = $coupledid; 282 } 283 } 284 } 285 $launchparameters['gradebookservices_scope'] = implode(',', $this->get_permitted_scopes()); 286 $launchparameters['lineitems_url'] = '$LineItems.url'; 287 if (!is_null($id)) { 288 $launchparameters['lineitem_url'] = '$LineItem.url'; 289 } 290 } 291 } 292 } 293 return $launchparameters; 294 } 295 296 /** 297 * Fetch the lineitem instances. 298 * 299 * @param string $courseid ID of course 300 * @param string $resourceid Resource identifier used for filtering, may be null 301 * @param string $ltilinkid Resource Link identifier used for filtering, may be null 302 * @param string $tag 303 * @param int $limitfrom Offset for the first line item to include in a paged set 304 * @param int $limitnum Maximum number of line items to include in the paged set 305 * @param string $typeid 306 * 307 * @return array 308 * @throws \Exception 309 */ 310 public function get_lineitems($courseid, $resourceid, $ltilinkid, $tag, $limitfrom, $limitnum, $typeid) { 311 global $DB; 312 313 // Select all lti potential linetiems in site. 314 $params = array('courseid' => $courseid); 315 316 $sql = "SELECT i.* 317 FROM {grade_items} i 318 WHERE (i.courseid = :courseid) 319 ORDER BY i.id"; 320 $lineitems = $DB->get_records_sql($sql, $params); 321 322 // For each one, check the gbs id, and check that toolproxy matches. If so, add the 323 // tag to the result and add it to a final results array. 324 $lineitemstoreturn = array(); 325 $lineitemsandtotalcount = array(); 326 if ($lineitems) { 327 foreach ($lineitems as $lineitem) { 328 $gbs = $this->find_ltiservice_gradebookservice_for_lineitem($lineitem->id); 329 if ($gbs && (!isset($tag) || (isset($tag) && $gbs->tag == $tag)) 330 && (!isset($ltilinkid) || (isset($ltilinkid) && $gbs->ltilinkid == $ltilinkid)) 331 && (!isset($resourceid) || (isset($resourceid) && $gbs->resourceid == $resourceid))) { 332 if (is_null($typeid)) { 333 if ($this->get_tool_proxy()->id == $gbs->toolproxyid) { 334 array_push($lineitemstoreturn, $lineitem); 335 } 336 } else { 337 if ($typeid == $gbs->typeid) { 338 array_push($lineitemstoreturn, $lineitem); 339 } 340 } 341 } else if (($lineitem->itemtype == 'mod' && $lineitem->itemmodule == 'lti' 342 && !isset($resourceid) && !isset($tag) 343 && (!isset($ltilinkid) || (isset($ltilinkid) 344 && $lineitem->iteminstance == $ltilinkid)))) { 345 // We will need to check if the activity related belongs to our tool proxy. 346 $ltiactivity = $DB->get_record('lti', array('id' => $lineitem->iteminstance)); 347 if (($ltiactivity) && (isset($ltiactivity->typeid))) { 348 if ($ltiactivity->typeid != 0) { 349 $tool = $DB->get_record('lti_types', array('id' => $ltiactivity->typeid)); 350 } else { 351 $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $courseid); 352 if (!$tool) { 353 $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $courseid); 354 } 355 } 356 if (is_null($typeid)) { 357 if (($tool) && ($this->get_tool_proxy()->id == $tool->toolproxyid)) { 358 array_push($lineitemstoreturn, $lineitem); 359 } 360 } else { 361 if (($tool) && ($tool->id == $typeid)) { 362 array_push($lineitemstoreturn, $lineitem); 363 } 364 } 365 } 366 } 367 } 368 $lineitemsandtotalcount = array(); 369 array_push($lineitemsandtotalcount, count($lineitemstoreturn)); 370 // Return the right array based in the paging parameters limit and from. 371 if (($limitnum) && ($limitnum > 0)) { 372 $lineitemstoreturn = array_slice($lineitemstoreturn, $limitfrom, $limitnum); 373 } 374 array_push($lineitemsandtotalcount, $lineitemstoreturn); 375 } 376 return $lineitemsandtotalcount; 377 } 378 379 /** 380 * Fetch a lineitem instance. 381 * 382 * Returns the lineitem instance if found, otherwise false. 383 * 384 * @param string $courseid ID of course 385 * @param string $itemid ID of lineitem 386 * @param string $typeid 387 * 388 * @return \ltiservice_gradebookservices\local\resources\lineitem|bool 389 */ 390 public function get_lineitem($courseid, $itemid, $typeid) { 391 global $DB, $CFG; 392 393 require_once($CFG->libdir . '/gradelib.php'); 394 $lineitem = \grade_item::fetch(array('id' => $itemid)); 395 if ($lineitem) { 396 $gbs = $this->find_ltiservice_gradebookservice_for_lineitem($itemid); 397 if (!$gbs) { 398 // We will need to check if the activity related belongs to our tool proxy. 399 $ltiactivity = $DB->get_record('lti', array('id' => $lineitem->iteminstance)); 400 if (($ltiactivity) && (isset($ltiactivity->typeid))) { 401 if ($ltiactivity->typeid != 0) { 402 $tool = $DB->get_record('lti_types', array('id' => $ltiactivity->typeid)); 403 } else { 404 $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $courseid); 405 if (!$tool) { 406 $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $courseid); 407 } 408 } 409 if (is_null($typeid)) { 410 if (!(($tool) && ($this->get_tool_proxy()->id == $tool->toolproxyid))) { 411 return false; 412 } 413 } else { 414 if (!(($tool) && ($tool->id == $typeid))) { 415 return false; 416 } 417 } 418 } else { 419 return false; 420 } 421 } 422 } 423 return $lineitem; 424 } 425 426 /** 427 * Adds a decoupled (standalone) line item. 428 * Decoupled line items are not directly attached to 429 * an lti instance activity. They are recorded in 430 * the gradebook as manual activities and the 431 * gradebookservices is used to associate that manual column 432 * with the tool in addition to storing the LTI related 433 * metadata (resource id, tag). 434 * 435 * @param string $courseid ID of course 436 * @param string $label label of lineitem 437 * @param float $maximumscore maximum score of lineitem 438 * @param string $baseurl 439 * @param int|null $ltilinkid id of lti instance this line item is associated with 440 * @param string|null $resourceid resource id of lineitem 441 * @param string|null $tag tag of lineitem 442 * @param int $typeid lti type to which this line item is associated with 443 * @param int|null $toolproxyid lti2 tool proxy to which this lineitem is associated to 444 * 445 * @return int id of the created gradeitem 446 */ 447 public function add_standalone_lineitem(string $courseid, string $label, float $maximumscore, 448 string $baseurl, ?int $ltilinkid, ?string $resourceid, ?string $tag, int $typeid, 449 int $toolproxyid = null) : int { 450 global $DB; 451 $params = array(); 452 $params['itemname'] = $label; 453 $params['gradetype'] = GRADE_TYPE_VALUE; 454 $params['grademax'] = $maximumscore; 455 $params['grademin'] = 0; 456 $item = new \grade_item(array('id' => 0, 'courseid' => $courseid)); 457 \grade_item::set_properties($item, $params); 458 $item->itemtype = 'manual'; 459 $item->grademax = $maximumscore; 460 $id = $item->insert('mod/ltiservice_gradebookservices'); 461 $DB->insert_record('ltiservice_gradebookservices', (object)array( 462 'gradeitemid' => $id, 463 'courseid' => $courseid, 464 'toolproxyid' => $toolproxyid, 465 'typeid' => $typeid, 466 'baseurl' => $baseurl, 467 'ltilinkid' => $ltilinkid, 468 'resourceid' => $resourceid, 469 'tag' => $tag 470 )); 471 return $id; 472 } 473 474 /** 475 * Set a grade item. 476 * 477 * @param object $gradeitem Grade Item record 478 * @param object $score Result object 479 * @param int $userid User ID 480 * 481 * @throws \Exception 482 * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. 483 * @see gradebookservices::save_grade_item($gradeitem, $score, $userid) 484 */ 485 public static function save_score($gradeitem, $score, $userid) { 486 $service = new gradebookservices(); 487 $service->save_grade_item($gradeitem, $score, $userid); 488 } 489 490 /** 491 * Saves a score received from the LTI tool. 492 * 493 * @param object $gradeitem Grade Item record 494 * @param object $score Result object 495 * @param int $userid User ID 496 * 497 * @throws \Exception 498 */ 499 public function save_grade_item($gradeitem, $score, $userid) { 500 global $DB, $CFG; 501 $source = 'mod' . $this->get_component_id(); 502 if ($DB->get_record('user', array('id' => $userid)) === false) { 503 throw new \Exception(null, 400); 504 } 505 require_once($CFG->libdir . '/gradelib.php'); 506 $finalgrade = null; 507 $timemodified = null; 508 if (isset($score->scoreGiven)) { 509 $finalgrade = grade_floatval($score->scoreGiven); 510 $max = 1; 511 if (isset($score->scoreMaximum)) { 512 $max = $score->scoreMaximum; 513 } 514 if (!is_null($max) && grade_floats_different($max, $gradeitem->grademax) && grade_floats_different($max, 0.0)) { 515 // Rescale to match the grade item maximum. 516 $finalgrade = grade_floatval($finalgrade * $gradeitem->grademax / $max); 517 } 518 if (isset($score->timestamp)) { 519 $timemodified = strtotime($score->timestamp); 520 } else { 521 $timemodified = time(); 522 } 523 } 524 $feedbackformat = FORMAT_MOODLE; 525 $feedback = null; 526 if (!empty($score->comment)) { 527 $feedback = $score->comment; 528 $feedbackformat = FORMAT_PLAIN; 529 } 530 531 if ($gradeitem->is_manual_item()) { 532 $result = $gradeitem->update_final_grade($userid, $finalgrade, null, $feedback, FORMAT_PLAIN, null, $timemodified); 533 } else { 534 if (!$grade = \grade_grade::fetch(array('itemid' => $gradeitem->id, 'userid' => $userid))) { 535 $grade = new \grade_grade(); 536 $grade->userid = $userid; 537 $grade->itemid = $gradeitem->id; 538 } 539 $grade->rawgrademax = $score->scoreMaximum; 540 $grade->timemodified = $timemodified; 541 $grade->feedbackformat = $feedbackformat; 542 $grade->feedback = $feedback; 543 $grade->rawgrade = $finalgrade; 544 $status = grade_update($source, $gradeitem->courseid, 545 $gradeitem->itemtype, $gradeitem->itemmodule, 546 $gradeitem->iteminstance, $gradeitem->itemnumber, $grade); 547 548 $result = ($status == GRADE_UPDATE_OK); 549 } 550 if (!$result) { 551 debugging("failed to save score for item ".$gradeitem->id." and user ".$grade->userid); 552 throw new \Exception(null, 500); 553 } 554 555 } 556 557 /** 558 * Get the json object representation of the grade item 559 * 560 * @param object $item Grade Item record 561 * @param string $endpoint Endpoint for lineitems container request 562 * @param string $typeid 563 * 564 * @return object 565 */ 566 public static function item_for_json($item, $endpoint, $typeid) { 567 568 $lineitem = new \stdClass(); 569 if (is_null($typeid)) { 570 $typeidstring = ""; 571 } else { 572 $typeidstring = "?type_id={$typeid}"; 573 } 574 $lineitem->id = "{$endpoint}/{$item->id}/lineitem" . $typeidstring; 575 $lineitem->label = $item->itemname; 576 $lineitem->scoreMaximum = floatval($item->grademax); 577 $gbs = self::find_ltiservice_gradebookservice_for_lineitem($item->id); 578 if ($gbs) { 579 $lineitem->resourceId = (!empty($gbs->resourceid)) ? $gbs->resourceid : ''; 580 $lineitem->tag = (!empty($gbs->tag)) ? $gbs->tag : ''; 581 if (isset($gbs->ltilinkid)) { 582 $lineitem->resourceLinkId = strval($gbs->ltilinkid); 583 $lineitem->ltiLinkId = strval($gbs->ltilinkid); 584 } 585 if (!empty($gbs->subreviewurl)) { 586 $submissionreview = new \stdClass(); 587 if ($gbs->subreviewurl != 'DEFAULT') { 588 $submissionreview->url = $gbs->subreviewurl; 589 } 590 if (!empty($gbs->subreviewparams)) { 591 $submissionreview->custom = lti_split_parameters($gbs->subreviewparams); 592 } 593 $lineitem->submissionReview = $submissionreview; 594 } 595 } else { 596 $lineitem->tag = ''; 597 if (isset($item->iteminstance)) { 598 $lineitem->resourceLinkId = strval($item->iteminstance); 599 $lineitem->ltiLinkId = strval($item->iteminstance); 600 } 601 } 602 603 return $lineitem; 604 605 } 606 607 /** 608 * Get the object matching the JSON representation of the result. 609 * 610 * @param object $grade Grade record 611 * @param string $endpoint Endpoint for lineitem 612 * @param int $typeid The id of the type to include in the result url. 613 * 614 * @return object 615 */ 616 public static function result_for_json($grade, $endpoint, $typeid) { 617 618 if (is_null($typeid)) { 619 $id = "{$endpoint}/results?user_id={$grade->userid}"; 620 } else { 621 $id = "{$endpoint}/results?type_id={$typeid}&user_id={$grade->userid}"; 622 } 623 $result = new \stdClass(); 624 $result->id = $id; 625 $result->userId = $grade->userid; 626 if (!empty($grade->finalgrade)) { 627 $result->resultScore = floatval($grade->finalgrade); 628 $result->resultMaximum = floatval($grade->rawgrademax); 629 if (!empty($grade->feedback)) { 630 $result->comment = $grade->feedback; 631 } 632 if (is_null($typeid)) { 633 $result->scoreOf = $endpoint; 634 } else { 635 $result->scoreOf = "{$endpoint}?type_id={$typeid}"; 636 } 637 $result->timestamp = date('c', $grade->timemodified); 638 } 639 return $result; 640 } 641 642 /** 643 * Check if an LTI id is valid. 644 * 645 * @param string $linkid The lti id 646 * @param string $course The course 647 * @param string $toolproxy The tool proxy id 648 * 649 * @return boolean 650 */ 651 public static function check_lti_id($linkid, $course, $toolproxy) { 652 global $DB; 653 // Check if lti type is zero or not (comes from a backup). 654 $sqlparams1 = array(); 655 $sqlparams1['linkid'] = $linkid; 656 $sqlparams1['course'] = $course; 657 $ltiactivity = $DB->get_record('lti', array('id' => $linkid, 'course' => $course)); 658 if ($ltiactivity->typeid == 0) { 659 $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $course); 660 if (!$tool) { 661 $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $course); 662 } 663 return (($tool) && ($toolproxy == $tool->toolproxyid)); 664 } else { 665 $sqlparams2 = array(); 666 $sqlparams2['linkid'] = $linkid; 667 $sqlparams2['course'] = $course; 668 $sqlparams2['toolproxy'] = $toolproxy; 669 $sql = 'SELECT lti.* 670 FROM {lti} lti 671 INNER JOIN {lti_types} typ ON lti.typeid = typ.id 672 WHERE lti.id = ? 673 AND lti.course = ? 674 AND typ.toolproxyid = ?'; 675 return $DB->record_exists_sql($sql, $sqlparams2); 676 } 677 } 678 679 /** 680 * Check if an LTI id is valid when we are in a LTI 1.x case 681 * 682 * @param string $linkid The lti id 683 * @param string $course The course 684 * @param string $typeid The lti type id 685 * 686 * @return boolean 687 */ 688 public static function check_lti_1x_id($linkid, $course, $typeid) { 689 global $DB; 690 // Check if lti type is zero or not (comes from a backup). 691 $sqlparams1 = array(); 692 $sqlparams1['linkid'] = $linkid; 693 $sqlparams1['course'] = $course; 694 $ltiactivity = $DB->get_record('lti', array('id' => $linkid, 'course' => $course)); 695 if ($ltiactivity) { 696 if ($ltiactivity->typeid == 0) { 697 $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $course); 698 if (!$tool) { 699 $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $course); 700 } 701 return (($tool) && ($typeid == $tool->id)); 702 } else { 703 $sqlparams2 = array(); 704 $sqlparams2['linkid'] = $linkid; 705 $sqlparams2['course'] = $course; 706 $sqlparams2['typeid'] = $typeid; 707 $sql = 'SELECT lti.* 708 FROM {lti} lti 709 INNER JOIN {lti_types} typ ON lti.typeid = typ.id 710 WHERE lti.id = ? 711 AND lti.course = ? 712 AND typ.id = ?'; 713 return $DB->record_exists_sql($sql, $sqlparams2); 714 } 715 } else { 716 return false; 717 } 718 } 719 720 /** 721 * Updates the tag, resourceid and submission review values for a grade item coupled to an lti link instance. 722 * 723 * @param object $ltiinstance The lti instance to which the grade item is coupled to 724 * @param string|null $resourceid The resourceid to apply to the lineitem. If empty string which will be stored as null. 725 * @param string|null $tag The tag to apply to the lineitem. If empty string which will be stored as null. 726 * @param moodle_url|null $subreviewurl The submission review target link URL 727 * @param string|null $subreviewparams The submission review custom parameters. 728 * 729 */ 730 public static function update_coupled_gradebookservices(object $ltiinstance, 731 ?string $resourceid, ?string $tag, ?\moodle_url $subreviewurl, ?string $subreviewparams) : void { 732 global $DB; 733 734 if ($ltiinstance && $ltiinstance->typeid) { 735 $gradeitem = $DB->get_record('grade_items', array('itemmodule' => 'lti', 'iteminstance' => $ltiinstance->id)); 736 if ($gradeitem) { 737 $resourceid = (isset($resourceid) && empty(trim($resourceid))) ? null : $resourceid; 738 $subreviewurlstr = $subreviewurl ? $subreviewurl->out(false) : null; 739 $tag = (isset($tag) && empty(trim($tag))) ? null : $tag; 740 $gbs = self::find_ltiservice_gradebookservice_for_lineitem($gradeitem->id); 741 if ($gbs) { 742 $gbs->resourceid = $resourceid; 743 $gbs->tag = $tag; 744 $gbs->subreviewurl = $subreviewurlstr; 745 $gbs->subreviewparams = $subreviewparams; 746 $DB->update_record('ltiservice_gradebookservices', $gbs); 747 } else { 748 $baseurl = lti_get_type_type_config($ltiinstance->typeid)->lti_toolurl; 749 $DB->insert_record('ltiservice_gradebookservices', (object)array( 750 'gradeitemid' => $gradeitem->id, 751 'courseid' => $gradeitem->courseid, 752 'typeid' => $ltiinstance->typeid, 753 'baseurl' => $baseurl, 754 'ltilinkid' => $ltiinstance->id, 755 'resourceid' => $resourceid, 756 'tag' => $tag, 757 'subreviewurl' => $subreviewurlstr, 758 'subreviewparams' => $subreviewparams 759 )); 760 } 761 } 762 } 763 } 764 765 /** 766 * Called when a new LTI Instance is added. 767 * 768 * @param object $lti LTI Instance. 769 */ 770 public function instance_added(object $lti): void { 771 self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null, 772 isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null, 773 $lti->lineitemsubreviewparams ?? null); 774 } 775 776 /** 777 * Called when a new LTI Instance is updated. 778 * 779 * @param object $lti LTI Instance. 780 */ 781 public function instance_updated(object $lti): void { 782 self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null, 783 isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null, 784 $lti->lineitemsubreviewparams ?? null); 785 } 786 787 /** 788 * Set the form data when displaying the LTI Instance form. 789 * 790 * @param object $defaultvalues Default form values. 791 */ 792 public function set_instance_form_values(object $defaultvalues): void { 793 $defaultvalues->lineitemresourceid = ''; 794 $defaultvalues->lineitemtag = ''; 795 $defaultvalues->subreviewurl = ''; 796 $defaultvalues->subreviewparams = ''; 797 if (is_object($defaultvalues) && $defaultvalues->instance) { 798 $gbs = self::find_ltiservice_gradebookservice_for_lti($defaultvalues->instance); 799 if ($gbs) { 800 $defaultvalues->lineitemresourceid = $gbs->resourceid; 801 $defaultvalues->lineitemtag = $gbs->tag; 802 $defaultvalues->lineitemsubreviewurl = $gbs->subreviewurl; 803 $defaultvalues->lineitemsubreviewparams = $gbs->subreviewparams; 804 } 805 } 806 } 807 808 /** 809 * Deletes orphaned rows from the 'ltiservice_gradebookservices' table. 810 * 811 * Sometimes, if a gradebook entry is deleted and it was a lineitem 812 * the row in the table ltiservice_gradebookservices can become an orphan 813 * This method will clean these orphans. It will happens based on a task 814 * because it is not urgent and we don't want to slow the service 815 */ 816 public static function delete_orphans_ltiservice_gradebookservices_rows() { 817 global $DB; 818 819 $sql = "DELETE 820 FROM {ltiservice_gradebookservices} 821 WHERE gradeitemid NOT IN (SELECT id 822 FROM {grade_items} gi)"; 823 $DB->execute($sql); 824 } 825 826 /** 827 * Check if a user can be graded in a course 828 * 829 * @param int $courseid The course 830 * @param int $userid The user 831 * @return bool 832 */ 833 public static function is_user_gradable_in_course($courseid, $userid) { 834 global $CFG; 835 836 $gradableuser = false; 837 $coursecontext = \context_course::instance($courseid); 838 if (is_enrolled($coursecontext, $userid, '', false)) { 839 $roles = get_user_roles($coursecontext, $userid); 840 $gradebookroles = explode(',', $CFG->gradebookroles); 841 foreach ($roles as $role) { 842 foreach ($gradebookroles as $gradebookrole) { 843 if ($role->roleid === $gradebookrole) { 844 $gradableuser = true; 845 } 846 } 847 } 848 } 849 850 return $gradableuser; 851 } 852 853 /** 854 * Find the right element in the ltiservice_gradebookservice table for an lti instance 855 * 856 * @param string $instanceid The LTI module instance id 857 * @return object gradebookservice for this line item 858 */ 859 public static function find_ltiservice_gradebookservice_for_lti($instanceid) { 860 global $DB; 861 862 if ($instanceid) { 863 $gradeitem = $DB->get_record('grade_items', array('itemmodule' => 'lti', 'iteminstance' => $instanceid)); 864 if ($gradeitem) { 865 return self::find_ltiservice_gradebookservice_for_lineitem($gradeitem->id); 866 } 867 } 868 } 869 870 /** 871 * Find the right element in the ltiservice_gradebookservice table for a lineitem 872 * 873 * @param string $lineitemid The lineitem (gradeitem) id 874 * @return object gradebookservice if it exists 875 */ 876 public static function find_ltiservice_gradebookservice_for_lineitem($lineitemid) { 877 global $DB; 878 if ($lineitemid) { 879 return $DB->get_record('ltiservice_gradebookservices', 880 array('gradeitemid' => $lineitemid)); 881 } 882 } 883 884 /** 885 * Validates specific ISO 8601 format of the timestamps. 886 * 887 * @param string $date The timestamp to check. 888 * @return boolean true or false if the date matches the format. 889 * 890 */ 891 public static function validate_iso8601_date($date) { 892 if (preg_match('/^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])' . 893 '(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))' . 894 '([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)' . 895 '?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/', $date) > 0) { 896 return true; 897 } else { 898 return false; 899 } 900 } 901 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body