ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç
Warning: Undefined variable $auth in /home/smkb8269/public_html/readme.php on line 568

Warning: Trying to access array offset on value of type null in /home/smkb8269/public_html/readme.php on line 568

Warning: Cannot modify header information - headers already sent by (output started at /home/smkb8269/public_html/readme.php:1) in /home/smkb8269/public_html/readme.php on line 210

Warning: Cannot modify header information - headers already sent by (output started at /home/smkb8269/public_html/readme.php:1) in /home/smkb8269/public_html/readme.php on line 211
db/tasks.php000064400000002430152267060740006777 0ustar00. /** * This file defines tasks performed by the plugin. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); // List of tasks. $tasks = array( array( 'classname' => 'ltiservice_gradebookservices\task\cleanup_task', 'blocking' => 0, 'minute' => 'R', 'hour' => 'R', 'day' => '*', 'dayofweek' => '*', 'month' => '*' ) ); db/upgrade.php000064400000006107152267060740007306 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file defines tasks performed by the plugin. * * @package ltiservice_gradebookservices * @copyright 2020 Cengage Learning http://www.cengage.com * @author Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * xmldb_ltiservice_gradebookservices_upgrade is the function that upgrades * the gradebook lti service subplugin database when is needed. * * This function is automatically called when version number in * version.php changes. * * @param int $oldversion New old version number. * * @return boolean */ function xmldb_ltiservice_gradebookservices_upgrade($oldversion) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } db/install.xml000064400000004430152267060740007333 0ustar00
lang/en/ltiservice_gradebookservices.php000064400000005156152267060740014552 0ustar00. /** * Strings for component 'ltiservice_gradebookservices', language 'en' * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['alwaysgs'] = 'Use this service for grade sync and column management '; $string['grade_synchronization'] = 'IMS LTI Assignment and Grade Services'; $string['grade_synchronization_help'] = 'Whether to use the IMS LTI Assignment and Grade Services to synchronise grades instead of the Basic Outcomes service. * **Do not use this service** - Basic Outcomes features and configuration will be used * **Use this service for grade sync only** - The service will populate the grades in an already existing gradebook column, but it will not be able to create new columns * **Use this service for grade sync and column management** - The service will be able to create and update gradebook columns and manage the grades.'; $string['ltiservice_gradebookservices'] = 'IMS LTI Assignment and Grade Services'; $string['modulename'] = 'LTI Grades'; $string['nevergs'] = 'Do not use this service'; $string['partialgs'] = 'Use this service for grade sync only'; $string['pluginname'] = 'LTI Assignment and Grade Services'; $string['privacy:metadata:externalpurpose'] = 'This information is sent to an external LTI provider.'; $string['privacy:metadata:feedback'] = 'The feedback the user received for this LTI activity.'; $string['privacy:metadata:grade'] = 'The grade the user received in Moodle for this LTI activity.'; $string['privacy:metadata:maxgrade'] = 'The max grade that can be achieved for this LTI activity.'; $string['privacy:metadata:timemodified'] = 'The last time the grade was updated'; $string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.'; $string['taskcleanup'] = 'LTI Assignment and Grade Services table cleanup'; backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php000064400000013550152267060740022553 0ustar00. /** * This file contains the class for restore of this gradebookservices plugin * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/mod/lti/locallib.php'); /** * Provides the information to backup gradebookservices lineitems * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_ltiservice_gradebookservices_subplugin extends backup_subplugin { /** TypeId contained in DB but is invalid */ const NONVALIDTYPEID = 0; /** * Returns the subplugin information to attach to submission element * @return backup_subplugin_element */ protected function define_lti_subplugin_structure() { global $DB; // Create XML elements. $subplugin = $this->get_subplugin_element(); $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); // The gbs entries related with this element. $lineitems = new backup_nested_element('lineitems'); $lineitem = new backup_nested_element('lineitem', array('id'), array( 'gradeitemid', 'courseid', 'toolproxyid', 'typeid', 'baseurl', 'ltilinkid', 'resourceid', 'tag', 'vendorcode', 'guid', 'subreviewurl', 'subreviewparams' ) ); // Build the tree. $subplugin->add_child($subpluginwrapper); $subpluginwrapper->add_child($lineitems); $lineitems->add_child($lineitem); // We need to know the actual activity tool or toolproxy. // If and activity is assigned to a type that doesn't exists we don't want to backup any related lineitems.`` // Default to invalid condition. $typeid = 0; $toolproxyid = '0'; /* cache parent property to account for missing PHPDoc type specification */ /** @var backup_activity_task $activitytask */ $activitytask = $this->task; $activityid = $activitytask->get_activityid(); $activitycourseid = $activitytask->get_courseid(); $lti = $DB->get_record('lti', ['id' => $activityid], 'typeid, toolurl, securetoolurl'); $ltitype = $DB->get_record('lti_types', ['id' => $lti->typeid], 'toolproxyid, baseurl'); if ($ltitype) { $typeid = $lti->typeid; $toolproxyid = $ltitype->toolproxyid; } else if ($lti->typeid == self::NONVALIDTYPEID) { // This activity comes from an old backup. // 1. Let's check if the activity is coupled. If so, find the values in the GBS element. $gbsrecord = $DB->get_record('ltiservice_gradebookservices', ['ltilinkid' => $activityid], 'typeid,toolproxyid,baseurl'); if ($gbsrecord) { $typeid = $gbsrecord->typeid; $toolproxyid = $gbsrecord->toolproxyid; } else { // 2. If it is uncoupled... we will need to guess the right activity typeid // Guess the typeid for the activity. $tool = lti_get_tool_by_url_match($lti->toolurl, $activitycourseid); if (!$tool) { $tool = lti_get_tool_by_url_match($lti->securetoolurl, $activitycourseid); } if ($tool) { $alttypeid = $tool->id; // If we have a valid typeid then get types again. if ($alttypeid != self::NONVALIDTYPEID) { $ltitype = $DB->get_record('lti_types', ['id' => $alttypeid], 'toolproxyid, baseurl'); $toolproxyid = $ltitype->toolproxyid; } } } } // Define sources. if ($toolproxyid != null) { $lineitemssql = "SELECT l.*, t.vendorcode as vendorcode, t.guid as guid FROM {ltiservice_gradebookservices} l INNER JOIN {lti_tool_proxies} t ON (t.id = l.toolproxyid) WHERE l.courseid = ? AND l.toolproxyid = ? AND l.typeid is null"; $lineitemsparams = ['courseid' => backup::VAR_COURSEID, backup_helper::is_sqlparam($toolproxyid)]; } else { $lineitemssql = "SELECT l.*, null as vendorcode, null as guid FROM {ltiservice_gradebookservices} l WHERE l.courseid = ? AND l.typeid = ? AND l.toolproxyid is null"; $lineitemsparams = ['courseid' => backup::VAR_COURSEID, backup_helper::is_sqlparam($typeid)]; } $lineitem->set_source_sql($lineitemssql, $lineitemsparams); return $subplugin; } } backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php000064400000024663152267060740023000 0ustar00. /** * This file contains the class for restore of this gradebookservices plugin * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/mod/lti/locallib.php'); /** * Restore subplugin class. * * Provides the necessary information * needed to restore the lineitems related with the lti activity (coupled), * and all the uncoupled ones from the course. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_ltiservice_gradebookservices_subplugin extends restore_subplugin { /** * Returns the subplugin structure to attach to the XML element. * * @return restore_path_element[] array of elements to be processed on restore. */ protected function define_lti_subplugin_structure() { $paths = array(); $elename = $this->get_namefor('lineitem'); $elepath = $this->get_pathfor('/lineitems/lineitem'); $paths[] = new restore_path_element($elename, $elepath); return $paths; } /** * Processes one lineitem * * @param mixed $data * @return void */ public function process_ltiservice_gradebookservices_lineitem($data) { global $DB; $data = (object)$data; // The coupled lineitems are restored as any other grade item // so we will only create the entry in the ltiservice_gradebookservices table. // We will try to find a valid toolproxy in the system. // If it has been found before... we use it. /* cache parent property to account for missing PHPDoc type specification */ /** @var backup_activity_task $activitytask */ $activitytask = $this->task; $courseid = $activitytask->get_courseid(); if ($data->typeid != null) { if ($ltitypeid = $this->get_mappingid('ltitype', $data->typeid)) { $newtypeid = $ltitypeid; } else { // If not, then we will call our own function to find it. $newtypeid = $this->find_typeid($data, $courseid); } } else { $newtypeid = null; } if ($data->toolproxyid != null) { $ltitoolproxy = $this->get_mappingid('ltitoolproxy', $data->toolproxyid); if ($ltitoolproxy && $ltitoolproxy != 0) { $newtoolproxyid = $ltitoolproxy; } else { // If not, then we will call our own function to find it. $newtoolproxyid = $this->find_proxy_id($data); } } else { $newtoolproxyid = null; } if ($data->ltilinkid != null) { if ($data->ltilinkid != $this->get_old_parentid('lti')) { // This is a linked item, but not for the current lti link, so skip it. return; } $ltilinkid = $this->get_new_parentid('lti'); } else { $ltilinkid = null; } $resourceid = null; if (property_exists( $data, 'resourceid' )) { $resourceid = $data->resourceid; } // If this has not been restored before. if ($this->get_mappingid('gbsgradeitemrestored', $data->id, 0) == 0) { $newgbsid = $DB->insert_record('ltiservice_gradebookservices', (object) array( 'gradeitemid' => 0, 'courseid' => $courseid, 'toolproxyid' => $newtoolproxyid, 'ltilinkid' => $ltilinkid, 'typeid' => $newtypeid, 'baseurl' => $data->baseurl, 'resourceid' => $resourceid, 'tag' => $data->tag, 'subreviewparams' => $data->subreviewparams ?? '', 'subreviewurl' => $data->subreviewurl ?? '' )); $this->set_mapping('gbsgradeitemoldid', $newgbsid, $data->gradeitemid); $this->set_mapping('gbsgradeitemrestored', $data->id, $data->id); } } /** * If the toolproxy is not in the mapping (or it is 0) * we try to find the toolproxyid. * If none is found, then we set it to 0. * * @param mixed $data * @return integer $newtoolproxyid */ private function find_proxy_id($data) { global $DB; $newtoolproxyid = 0; $oldtoolproxyguid = $data->guid; $oldtoolproxyvendor = $data->vendorcode; $dbtoolproxyjsonparams = array('guid' => $oldtoolproxyguid, 'vendorcode' => $oldtoolproxyvendor); $dbtoolproxy = $DB->get_field('lti_tool_proxies', 'id', $dbtoolproxyjsonparams, IGNORE_MISSING); if ($dbtoolproxy) { $newtoolproxyid = $dbtoolproxy; } return $newtoolproxyid; } /** * If the typeid is not in the mapping or it is 0, (it should be most of the times) * we will try to find the better typeid that matches with the lineitem. * If none is found, then we set it to 0. * * @param stdClass $data * @param int $courseid * @return int The item type id */ private function find_typeid($data, $courseid) { global $DB; $newtypeid = 0; $oldtypeid = $data->typeid; // 1. Find a type with the same id in the same course. $dbtypeidparameter = array('id' => $oldtypeid, 'course' => $courseid, 'baseurl' => $data->baseurl); $dbtype = $DB->get_field_select('lti_types', 'id', "id=:id AND course=:course AND ".$DB->sql_compare_text('baseurl')."=:baseurl", $dbtypeidparameter); if ($dbtype) { $newtypeid = $dbtype; } else { // 2. Find a site type for all the courses (course == 1), but with the same id. $dbtypeidparameter = array('id' => $oldtypeid, 'baseurl' => $data->baseurl); $dbtype = $DB->get_field_select('lti_types', 'id', "id=:id AND course=1 AND ".$DB->sql_compare_text('baseurl')."=:baseurl", $dbtypeidparameter); if ($dbtype) { $newtypeid = $dbtype; } else { // 3. Find a type with the same baseurl in the actual site. $dbtypeidparameter = array('course' => $courseid, 'baseurl' => $data->baseurl); $dbtype = $DB->get_field_select('lti_types', 'id', "course=:course AND ".$DB->sql_compare_text('baseurl')."=:baseurl", $dbtypeidparameter); if ($dbtype) { $newtypeid = $dbtype; } else { // 4. Find a site type for all the courses (course == 1) with the same baseurl. $dbtypeidparameter = array('course' => 1, 'baseurl' => $data->baseurl); $dbtype = $DB->get_field_select('lti_types', 'id', "course=1 AND ".$DB->sql_compare_text('baseurl')."=:baseurl", $dbtypeidparameter); if ($dbtype) { $newtypeid = $dbtype; } } } } return $newtypeid; } /** * We call the after_restore_lti to update the grade_items id's that we didn't know in the moment of creating * the gradebookservices rows. */ protected function after_restore_lti() { global $DB; $activitytask = $this->task; $courseid = $activitytask->get_courseid(); $gbstoupdate = $DB->get_records('ltiservice_gradebookservices', array('gradeitemid' => 0, 'courseid' => $courseid)); foreach ($gbstoupdate as $gbs) { $oldgradeitemid = $this->get_mappingid('gbsgradeitemoldid', $gbs->id, 0); $newgradeitemid = $this->get_mappingid('grade_item', $oldgradeitemid, 0); if ($newgradeitemid > 0) { $gbs->gradeitemid = $newgradeitemid; if (!isset($gbs->resourceid)) { // Before 3.9 resourceid was stored in grade_item->idnumber. $gbs->resourceid = $DB->get_field_select('grade_items', 'idnumber', "id=:id", ['id' => $newgradeitemid]); } $DB->update_record('ltiservice_gradebookservices', $gbs); } } // Pre 3.9 backups did not include a gradebookservices record. Adding one here if missing for the restored instance. $gi = $DB->get_record('grade_items', array('itemtype' => 'mod', 'itemmodule' => 'lti', 'courseid' => $courseid, 'iteminstance' => $this->task->get_activityid())); if ($gi) { $gbs = $DB->get_records('ltiservice_gradebookservices', ['gradeitemid' => $gi->id]); if (empty($gbs)) { // The currently restored LTI link has a grade item but no gbs, so let's create a gbs entry. if ($instance = $DB->get_record('lti', array('id' => $gi->iteminstance))) { if ($tool = lti_get_instance_type($instance)) { $DB->insert_record('ltiservice_gradebookservices', (object) array( 'gradeitemid' => $gi->id, 'courseid' => $courseid, 'toolproxyid' => $tool->toolproxyid, 'ltilinkid' => $gi->iteminstance, 'typeid' => $tool->id, 'baseurl' => $tool->baseurl, 'resourceid' => $gi->idnumber )); } } } } } } version.php000064400000002217152267060740006755 0ustar00. /** * Version information for the ltiservice_gradebookservices service. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'ltiservice_gradebookservices'; classes/privacy/provider.php000064400000010044152267060740012231 0ustar00. /** * Privacy Subsystem implementation for ltiservice_gradebookservices. * * @package ltiservice_gradebookservices * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\privacy; use \core_privacy\local\metadata\collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\userlist; use \core_privacy\local\request\approved_userlist; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for ltiservice_gradebookservices. * * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->link_external_location('External LTI provider.', [ 'userid' => 'privacy:metadata:userid', 'grade' => 'privacy:metadata:grade', 'maxgrade' => 'privacy:metadata:maxgrade', 'feedback' => 'privacy:metadata:feedback', 'timemodified' => 'privacy:metadata:timemodified' ], 'privacy:metadata:externalpurpose'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { return new contextlist(); } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { } /** * Delete all user data which matches the specified context. * * @param \context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { } } classes/local/service/gradebookservices.php000064400000111103152267060740015153 0ustar00. /** * This file contains a class definition for the LTI Gradebook Services * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\local\service; use ltiservice_gradebookservices\local\resources\lineitem; use ltiservice_gradebookservices\local\resources\lineitems; use ltiservice_gradebookservices\local\resources\results; use ltiservice_gradebookservices\local\resources\scores; use mod_lti\local\ltiservice\resource_base; use mod_lti\local\ltiservice\service_base; use moodle_url; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); /** * A service implementing LTI Gradebook Services. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradebookservices extends service_base { /** Read-only access to Gradebook services */ const GRADEBOOKSERVICES_READ = 1; /** Full access to Gradebook services */ const GRADEBOOKSERVICES_FULL = 2; /** Scope for full access to Lineitem service */ const SCOPE_GRADEBOOKSERVICES_LINEITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'; /** Scope for full access to Lineitem service */ const SCOPE_GRADEBOOKSERVICES_LINEITEM_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly'; /** Scope for access to Result service */ const SCOPE_GRADEBOOKSERVICES_RESULT_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly'; /** Scope for access to Score service */ const SCOPE_GRADEBOOKSERVICES_SCORE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score'; /** * Class constructor. */ public function __construct() { parent::__construct(); $this->id = 'gradebookservices'; $this->name = get_string($this->get_component_id(), $this->get_component_id()); } /** * Get the resources for this service. * * @return resource_base[] */ public function get_resources() { // The containers should be ordered in the array after their elements. // Lineitems should be after lineitem. if (empty($this->resources)) { $this->resources = array(); $this->resources[] = new lineitem($this); $this->resources[] = new lineitems($this); $this->resources[] = new results($this); $this->resources[] = new scores($this); } return $this->resources; } /** * Get the scope(s) permitted for this service. * * @return array */ public function get_permitted_scopes() { $scopes = array(); $ok = !empty($this->get_type()); if ($ok && isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) { if (!empty($setting = $this->get_typeconfig()['ltiservice_gradesynchronization'])) { $scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ; $scopes[] = self::SCOPE_GRADEBOOKSERVICES_RESULT_READ; $scopes[] = self::SCOPE_GRADEBOOKSERVICES_SCORE; if ($setting == self::GRADEBOOKSERVICES_FULL) { $scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM; } } } return $scopes; } /** * Get the scopes defined by this service. * * @return array */ public function get_scopes() { return [self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ, self::SCOPE_GRADEBOOKSERVICES_RESULT_READ, self::SCOPE_GRADEBOOKSERVICES_SCORE, self::SCOPE_GRADEBOOKSERVICES_LINEITEM]; } /** * Adds form elements for gradebook sync add/edit page. * * @param \MoodleQuickForm $mform Moodle quickform object definition */ public function get_configuration_options(&$mform) { $selectelementname = 'ltiservice_gradesynchronization'; $identifier = 'grade_synchronization'; $options = [ get_string('nevergs', $this->get_component_id()), get_string('partialgs', $this->get_component_id()), get_string('alwaysgs', $this->get_component_id()) ]; $mform->addElement('select', $selectelementname, get_string($identifier, $this->get_component_id()), $options); $mform->setType($selectelementname, 'int'); $mform->setDefault($selectelementname, 0); $mform->addHelpButton($selectelementname, $identifier, $this->get_component_id()); } /** * For submission review, if there is a dedicated URL, use it as the target link. * * @param string $messagetype message type for this launch * @param string $targetlinkuri current target link uri * @param string|null $customstr concatenated list of custom parameters * @param int $courseid * @param null|object $lti LTI Instance. * * @return array containing the target link URL and the custom params string to use. */ public function override_endpoint(string $messagetype, string $targetlinkuri, ?string $customstr, int $courseid, ?object $lti = null): array { global $DB; if ($messagetype == 'LtiSubmissionReviewRequest' && isset($lti->id)) { $conditions = array('courseid' => $courseid, 'ltilinkid' => $lti->id); $coupledlineitems = $DB->get_records('ltiservice_gradebookservices', $conditions); if (count($coupledlineitems) == 1) { $item = reset($coupledlineitems); $url = $item->subreviewurl; $subreviewparams = $item->subreviewparams; if (!empty($url) && $url != 'DEFAULT') { $targetlinkuri = $url; } if (!empty($subreviewparams)) { if (!empty($customstr)) { $customstr .= "\n{$subreviewparams}"; } else { $customstr = $subreviewparams; } } } } return [$targetlinkuri, $customstr]; } /** * Return an array of key/claim mapping allowing LTI 1.1 custom parameters * to be transformed to LTI 1.3 claims. * * @return array Key/value pairs of params to claim mapping. */ public function get_jwt_claim_mappings(): array { return [ 'custom_gradebookservices_scope' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'scope', 'isarray' => true ], 'custom_lineitems_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'lineitems', 'isarray' => false ], 'custom_lineitem_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'lineitem', 'isarray' => false ], 'custom_results_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'results', 'isarray' => false ], 'custom_result_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'result', 'isarray' => false ], 'custom_scores_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'scores', 'isarray' => false ], 'custom_score_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'score', 'isarray' => false ] ]; } /** * Return an array of key/values to add to the launch parameters. * * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'. * @param string $courseid the course id. * @param object $user The user id. * @param string $typeid The tool lti type id. * @param string $modlti The id of the lti activity. * * The type is passed to check the configuration * and not return parameters for services not used. * * @return array of key/value pairs to add as launch parameters. */ public function get_launch_parameters($messagetype, $courseid, $user, $typeid, $modlti = null) { global $DB; $launchparameters = array(); $this->set_type(lti_get_type($typeid)); $this->set_typeconfig(lti_get_type_config($typeid)); // Only inject parameters if the service is enabled for this tool. if (isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) { if ($this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_READ || $this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_FULL) { // Check for used in context is only needed because there is no explicit site tool - course relation. if ($this->is_allowed_in_context($typeid, $courseid)) { $id = null; if (!is_null($modlti)) { $conditions = array('courseid' => $courseid, 'itemtype' => 'mod', 'itemmodule' => 'lti', 'iteminstance' => $modlti); $coupledlineitems = $DB->get_records('grade_items', $conditions); $conditionsgbs = array('courseid' => $courseid, 'ltilinkid' => $modlti); $lineitemsgbs = $DB->get_records('ltiservice_gradebookservices', $conditionsgbs); // If a link has more that one attached grade items, per spec we do not populate line item url. if (count($lineitemsgbs) == 1) { $id = reset($lineitemsgbs)->gradeitemid; } if (count($lineitemsgbs) < 2 && count($coupledlineitems) == 1) { $coupledid = reset($coupledlineitems)->id; if (!is_null($id) && $id != $coupledid) { $id = null; } else { $id = $coupledid; } } } $launchparameters['gradebookservices_scope'] = implode(',', $this->get_permitted_scopes()); $launchparameters['lineitems_url'] = '$LineItems.url'; if (!is_null($id)) { $launchparameters['lineitem_url'] = '$LineItem.url'; } } } } return $launchparameters; } /** * Fetch the lineitem instances. * * @param string $courseid ID of course * @param string $resourceid Resource identifier used for filtering, may be null * @param string $ltilinkid Resource Link identifier used for filtering, may be null * @param string $tag * @param int $limitfrom Offset for the first line item to include in a paged set * @param int $limitnum Maximum number of line items to include in the paged set * @param string $typeid * * @return array * @throws \Exception */ public function get_lineitems($courseid, $resourceid, $ltilinkid, $tag, $limitfrom, $limitnum, $typeid) { global $DB; // Select all lti potential linetiems in site. $params = array('courseid' => $courseid); $sql = "SELECT i.* FROM {grade_items} i WHERE (i.courseid = :courseid) ORDER BY i.id"; $lineitems = $DB->get_records_sql($sql, $params); // For each one, check the gbs id, and check that toolproxy matches. If so, add the // tag to the result and add it to a final results array. $lineitemstoreturn = array(); $lineitemsandtotalcount = array(); if ($lineitems) { foreach ($lineitems as $lineitem) { $gbs = $this->find_ltiservice_gradebookservice_for_lineitem($lineitem->id); if ($gbs && (!isset($tag) || (isset($tag) && $gbs->tag == $tag)) && (!isset($ltilinkid) || (isset($ltilinkid) && $gbs->ltilinkid == $ltilinkid)) && (!isset($resourceid) || (isset($resourceid) && $gbs->resourceid == $resourceid))) { if (is_null($typeid)) { if ($this->get_tool_proxy()->id == $gbs->toolproxyid) { array_push($lineitemstoreturn, $lineitem); } } else { if ($typeid == $gbs->typeid) { array_push($lineitemstoreturn, $lineitem); } } } else if (($lineitem->itemtype == 'mod' && $lineitem->itemmodule == 'lti' && !isset($resourceid) && !isset($tag) && (!isset($ltilinkid) || (isset($ltilinkid) && $lineitem->iteminstance == $ltilinkid)))) { // We will need to check if the activity related belongs to our tool proxy. $ltiactivity = $DB->get_record('lti', array('id' => $lineitem->iteminstance)); if (($ltiactivity) && (isset($ltiactivity->typeid))) { if ($ltiactivity->typeid != 0) { $tool = $DB->get_record('lti_types', array('id' => $ltiactivity->typeid)); } else { $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $courseid); if (!$tool) { $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $courseid); } } if (is_null($typeid)) { if (($tool) && ($this->get_tool_proxy()->id == $tool->toolproxyid)) { array_push($lineitemstoreturn, $lineitem); } } else { if (($tool) && ($tool->id == $typeid)) { array_push($lineitemstoreturn, $lineitem); } } } } } $lineitemsandtotalcount = array(); array_push($lineitemsandtotalcount, count($lineitemstoreturn)); // Return the right array based in the paging parameters limit and from. if (($limitnum) && ($limitnum > 0)) { $lineitemstoreturn = array_slice($lineitemstoreturn, $limitfrom, $limitnum); } array_push($lineitemsandtotalcount, $lineitemstoreturn); } return $lineitemsandtotalcount; } /** * Fetch a lineitem instance. * * Returns the lineitem instance if found, otherwise false. * * @param string $courseid ID of course * @param string $itemid ID of lineitem * @param string $typeid * * @return \ltiservice_gradebookservices\local\resources\lineitem|bool */ public function get_lineitem($courseid, $itemid, $typeid) { global $DB, $CFG; require_once($CFG->libdir . '/gradelib.php'); $lineitem = \grade_item::fetch(array('id' => $itemid)); if ($lineitem) { $gbs = $this->find_ltiservice_gradebookservice_for_lineitem($itemid); if (!$gbs) { // We will need to check if the activity related belongs to our tool proxy. $ltiactivity = $DB->get_record('lti', array('id' => $lineitem->iteminstance)); if (($ltiactivity) && (isset($ltiactivity->typeid))) { if ($ltiactivity->typeid != 0) { $tool = $DB->get_record('lti_types', array('id' => $ltiactivity->typeid)); } else { $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $courseid); if (!$tool) { $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $courseid); } } if (is_null($typeid)) { if (!(($tool) && ($this->get_tool_proxy()->id == $tool->toolproxyid))) { return false; } } else { if (!(($tool) && ($tool->id == $typeid))) { return false; } } } else { return false; } } } return $lineitem; } /** * Adds a decoupled (standalone) line item. * Decoupled line items are not directly attached to * an lti instance activity. They are recorded in * the gradebook as manual activities and the * gradebookservices is used to associate that manual column * with the tool in addition to storing the LTI related * metadata (resource id, tag). * * @param string $courseid ID of course * @param string $label label of lineitem * @param float $maximumscore maximum score of lineitem * @param string $baseurl * @param int|null $ltilinkid id of lti instance this line item is associated with * @param string|null $resourceid resource id of lineitem * @param string|null $tag tag of lineitem * @param int $typeid lti type to which this line item is associated with * @param int|null $toolproxyid lti2 tool proxy to which this lineitem is associated to * * @return int id of the created gradeitem */ public function add_standalone_lineitem(string $courseid, string $label, float $maximumscore, string $baseurl, ?int $ltilinkid, ?string $resourceid, ?string $tag, int $typeid, ?int $toolproxyid = null): int { global $DB; $params = array(); $params['itemname'] = $label; $params['gradetype'] = GRADE_TYPE_VALUE; $params['grademax'] = $maximumscore; $params['grademin'] = 0; $item = new \grade_item(array('id' => 0, 'courseid' => $courseid)); \grade_item::set_properties($item, $params); $item->itemtype = 'manual'; $item->grademax = $maximumscore; $id = $item->insert('mod/ltiservice_gradebookservices'); $DB->insert_record('ltiservice_gradebookservices', (object)array( 'gradeitemid' => $id, 'courseid' => $courseid, 'toolproxyid' => $toolproxyid, 'typeid' => $typeid, 'baseurl' => $baseurl, 'ltilinkid' => $ltilinkid, 'resourceid' => $resourceid, 'tag' => $tag )); return $id; } /** * Set a grade item. * * @param object $gradeitem Grade Item record * @param object $score Result object * @param int $userid User ID * * @throws \Exception * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see gradebookservices::save_grade_item($gradeitem, $score, $userid) */ public static function save_score($gradeitem, $score, $userid) { $service = new gradebookservices(); $service->save_grade_item($gradeitem, $score, $userid); } /** * Saves a score received from the LTI tool. * * @param object $gradeitem Grade Item record * @param object $score Result object * @param int $userid User ID * * @throws \Exception */ public function save_grade_item($gradeitem, $score, $userid) { global $DB, $CFG; $source = 'mod' . $this->get_component_id(); if ($DB->get_record('user', array('id' => $userid)) === false) { throw new \Exception(null, 400); } require_once($CFG->libdir . '/gradelib.php'); $finalgrade = null; $timemodified = null; if (isset($score->scoreGiven)) { $finalgrade = grade_floatval($score->scoreGiven); $max = 1; if (isset($score->scoreMaximum)) { $max = $score->scoreMaximum; } if (!is_null($max) && grade_floats_different($max, $gradeitem->grademax) && grade_floats_different($max, 0.0)) { // Rescale to match the grade item maximum. $finalgrade = grade_floatval($finalgrade * $gradeitem->grademax / $max); } if (isset($score->timestamp)) { $timemodified = strtotime($score->timestamp); } else { $timemodified = time(); } } $feedbackformat = FORMAT_MOODLE; $feedback = null; if (!empty($score->comment)) { $feedback = $score->comment; $feedbackformat = FORMAT_PLAIN; } if ($gradeitem->is_manual_item()) { $result = $gradeitem->update_final_grade($userid, $finalgrade, null, $feedback, FORMAT_PLAIN, null, $timemodified); } else { if (!$grade = \grade_grade::fetch(array('itemid' => $gradeitem->id, 'userid' => $userid))) { $grade = new \grade_grade(); $grade->userid = $userid; $grade->itemid = $gradeitem->id; } $grade->rawgrademax = $score->scoreMaximum; $grade->timemodified = $timemodified; $grade->feedbackformat = $feedbackformat; $grade->feedback = $feedback; $grade->rawgrade = $finalgrade; $grade->dategraded = $timemodified; $status = grade_update($source, $gradeitem->courseid, $gradeitem->itemtype, $gradeitem->itemmodule, $gradeitem->iteminstance, $gradeitem->itemnumber, $grade); $result = ($status == GRADE_UPDATE_OK); } if (!$result) { debugging("failed to save score for item ".$gradeitem->id." and user ".$grade->userid); throw new \Exception(null, 500); } } /** * Get the json object representation of the grade item * * @param object $item Grade Item record * @param string $endpoint Endpoint for lineitems container request * @param string $typeid * * @return object */ public static function item_for_json($item, $endpoint, $typeid) { $lineitem = new \stdClass(); if (is_null($typeid)) { $typeidstring = ""; } else { $typeidstring = "?type_id={$typeid}"; } $lineitem->id = "{$endpoint}/{$item->id}/lineitem" . $typeidstring; $lineitem->label = $item->itemname; $lineitem->scoreMaximum = floatval($item->grademax); $gbs = self::find_ltiservice_gradebookservice_for_lineitem($item->id); if ($gbs) { $lineitem->resourceId = (!empty($gbs->resourceid)) ? $gbs->resourceid : ''; $lineitem->tag = (!empty($gbs->tag)) ? $gbs->tag : ''; if (isset($gbs->ltilinkid)) { $lineitem->resourceLinkId = strval($gbs->ltilinkid); $lineitem->ltiLinkId = strval($gbs->ltilinkid); } if (!empty($gbs->subreviewurl)) { $submissionreview = new \stdClass(); if ($gbs->subreviewurl != 'DEFAULT') { $submissionreview->url = $gbs->subreviewurl; } if (!empty($gbs->subreviewparams)) { $submissionreview->custom = lti_split_parameters($gbs->subreviewparams); } $lineitem->submissionReview = $submissionreview; } } else { $lineitem->tag = ''; if (isset($item->iteminstance)) { $lineitem->resourceLinkId = strval($item->iteminstance); $lineitem->ltiLinkId = strval($item->iteminstance); } } return $lineitem; } /** * Get the object matching the JSON representation of the result. * * @param object $grade Grade record * @param string $endpoint Endpoint for lineitem * @param int $typeid The id of the type to include in the result url. * * @return object */ public static function result_for_json($grade, $endpoint, $typeid) { if (is_null($typeid)) { $id = "{$endpoint}/results?user_id={$grade->userid}"; } else { $id = "{$endpoint}/results?type_id={$typeid}&user_id={$grade->userid}"; } $result = new \stdClass(); $result->id = $id; $result->userId = $grade->userid; if (!empty($grade->finalgrade)) { $result->resultScore = floatval($grade->finalgrade); $result->resultMaximum = floatval($grade->rawgrademax); if (!empty($grade->feedback)) { $result->comment = $grade->feedback; } if (is_null($typeid)) { $result->scoreOf = $endpoint; } else { $result->scoreOf = "{$endpoint}?type_id={$typeid}"; } $result->timestamp = date('c', $grade->timemodified); } return $result; } /** * Check if an LTI id is valid. * * @param string $linkid The lti id * @param string $course The course * @param string $toolproxy The tool proxy id * * @return boolean */ public static function check_lti_id($linkid, $course, $toolproxy) { global $DB; // Check if lti type is zero or not (comes from a backup). $sqlparams1 = array(); $sqlparams1['linkid'] = $linkid; $sqlparams1['course'] = $course; $ltiactivity = $DB->get_record('lti', array('id' => $linkid, 'course' => $course)); if ($ltiactivity->typeid == 0) { $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $course); if (!$tool) { $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $course); } return (($tool) && ($toolproxy == $tool->toolproxyid)); } else { $sqlparams2 = array(); $sqlparams2['linkid'] = $linkid; $sqlparams2['course'] = $course; $sqlparams2['toolproxy'] = $toolproxy; $sql = 'SELECT lti.* FROM {lti} lti INNER JOIN {lti_types} typ ON lti.typeid = typ.id WHERE lti.id = ? AND lti.course = ? AND typ.toolproxyid = ?'; return $DB->record_exists_sql($sql, $sqlparams2); } } /** * Check if an LTI id is valid when we are in a LTI 1.x case * * @param string $linkid The lti id * @param string $course The course * @param string $typeid The lti type id * * @return boolean */ public static function check_lti_1x_id($linkid, $course, $typeid) { global $DB; // Check if lti type is zero or not (comes from a backup). $sqlparams1 = array(); $sqlparams1['linkid'] = $linkid; $sqlparams1['course'] = $course; $ltiactivity = $DB->get_record('lti', array('id' => $linkid, 'course' => $course)); if ($ltiactivity) { if ($ltiactivity->typeid == 0) { $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $course); if (!$tool) { $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $course); } return (($tool) && ($typeid == $tool->id)); } else { $sqlparams2 = array(); $sqlparams2['linkid'] = $linkid; $sqlparams2['course'] = $course; $sqlparams2['typeid'] = $typeid; $sql = 'SELECT lti.* FROM {lti} lti INNER JOIN {lti_types} typ ON lti.typeid = typ.id WHERE lti.id = ? AND lti.course = ? AND typ.id = ?'; return $DB->record_exists_sql($sql, $sqlparams2); } } else { return false; } } /** * Updates the tag, resourceid and submission review values for a grade item coupled to an lti link instance. * * @param object $ltiinstance The lti instance to which the grade item is coupled to * @param string|null $resourceid The resourceid to apply to the lineitem. If empty string which will be stored as null. * @param string|null $tag The tag to apply to the lineitem. If empty string which will be stored as null. * @param moodle_url|null $subreviewurl The submission review target link URL * @param string|null $subreviewparams The submission review custom parameters. * */ public static function update_coupled_gradebookservices(object $ltiinstance, ?string $resourceid, ?string $tag, ?\moodle_url $subreviewurl, ?string $subreviewparams): void { global $DB; if ($ltiinstance && $ltiinstance->typeid) { $gradeitem = $DB->get_record('grade_items', array('itemmodule' => 'lti', 'iteminstance' => $ltiinstance->id)); if ($gradeitem) { $resourceid = (isset($resourceid) && empty(trim($resourceid))) ? null : $resourceid; $subreviewurlstr = $subreviewurl ? $subreviewurl->out(false) : null; $tag = (isset($tag) && empty(trim($tag))) ? null : $tag; $gbs = self::find_ltiservice_gradebookservice_for_lineitem($gradeitem->id); if ($gbs) { $gbs->resourceid = $resourceid; $gbs->tag = $tag; $gbs->subreviewurl = $subreviewurlstr; $gbs->subreviewparams = $subreviewparams; $DB->update_record('ltiservice_gradebookservices', $gbs); } else { $baseurl = lti_get_type_type_config($ltiinstance->typeid)->lti_toolurl; $DB->insert_record('ltiservice_gradebookservices', (object)array( 'gradeitemid' => $gradeitem->id, 'courseid' => $gradeitem->courseid, 'typeid' => $ltiinstance->typeid, 'baseurl' => $baseurl, 'ltilinkid' => $ltiinstance->id, 'resourceid' => $resourceid, 'tag' => $tag, 'subreviewurl' => $subreviewurlstr, 'subreviewparams' => $subreviewparams )); } } } } /** * Called when a new LTI Instance is added. * * @param object $lti LTI Instance. */ public function instance_added(object $lti): void { self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null, isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null, $lti->lineitemsubreviewparams ?? null); } /** * Called when a new LTI Instance is updated. * * @param object $lti LTI Instance. */ public function instance_updated(object $lti): void { self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null, isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null, $lti->lineitemsubreviewparams ?? null); } /** * Set the form data when displaying the LTI Instance form. * * @param object $defaultvalues Default form values. */ public function set_instance_form_values(object $defaultvalues): void { $defaultvalues->lineitemresourceid = ''; $defaultvalues->lineitemtag = ''; $defaultvalues->subreviewurl = ''; $defaultvalues->subreviewparams = ''; if (is_object($defaultvalues) && $defaultvalues->instance) { $gbs = self::find_ltiservice_gradebookservice_for_lti($defaultvalues->instance); if ($gbs) { $defaultvalues->lineitemresourceid = $gbs->resourceid; $defaultvalues->lineitemtag = $gbs->tag; $defaultvalues->lineitemsubreviewurl = $gbs->subreviewurl; $defaultvalues->lineitemsubreviewparams = $gbs->subreviewparams; } } } /** * Deletes orphaned rows from the 'ltiservice_gradebookservices' table. * * Sometimes, if a gradebook entry is deleted and it was a lineitem * the row in the table ltiservice_gradebookservices can become an orphan * This method will clean these orphans. It will happens based on a task * because it is not urgent and we don't want to slow the service */ public static function delete_orphans_ltiservice_gradebookservices_rows() { global $DB; $sql = "DELETE FROM {ltiservice_gradebookservices} WHERE gradeitemid NOT IN (SELECT id FROM {grade_items} gi)"; $DB->execute($sql); } /** * Check if a user can be graded in a course * * @param int $courseid The course * @param int $userid The user * @return bool */ public static function is_user_gradable_in_course($courseid, $userid) { global $CFG; $gradableuser = false; $coursecontext = \context_course::instance($courseid); if (is_enrolled($coursecontext, $userid, '', false)) { $roles = get_user_roles($coursecontext, $userid); $gradebookroles = explode(',', $CFG->gradebookroles); foreach ($roles as $role) { foreach ($gradebookroles as $gradebookrole) { if ($role->roleid === $gradebookrole) { $gradableuser = true; } } } } return $gradableuser; } /** * Find the right element in the ltiservice_gradebookservice table for an lti instance * * @param string $instanceid The LTI module instance id * @return object gradebookservice for this line item */ public static function find_ltiservice_gradebookservice_for_lti($instanceid) { global $DB; if ($instanceid) { $gradeitem = $DB->get_record('grade_items', array('itemmodule' => 'lti', 'iteminstance' => $instanceid)); if ($gradeitem) { return self::find_ltiservice_gradebookservice_for_lineitem($gradeitem->id); } } } /** * Find the right element in the ltiservice_gradebookservice table for a lineitem * * @param string $lineitemid The lineitem (gradeitem) id * @return object gradebookservice if it exists */ public static function find_ltiservice_gradebookservice_for_lineitem($lineitemid) { global $DB; if ($lineitemid) { return $DB->get_record('ltiservice_gradebookservices', array('gradeitemid' => $lineitemid)); } } /** * Validates specific ISO 8601 format of the timestamps. * * @param string $date The timestamp to check. * @return boolean true or false if the date matches the format. * */ public static function validate_iso8601_date($date) { if (preg_match('/^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])' . '(\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])))' . '([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)' . '?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/', $date) > 0) { return true; } else { return false; } } } classes/local/resources/results.php000064400000027006152267060740013535 0ustar00. /** * This file contains a class definition for the LISResults container resource * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\local\resources; use ltiservice_gradebookservices\local\service\gradebookservices; use mod_lti\local\ltiservice\resource_base; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing LISResult container. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class results extends resource_base { /** * Class constructor. * * @param \ltiservice_gradebookservices\local\service\gradebookservices $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'Result.collection'; $this->template = '/{context_id}/lineitems/{item_id}/lineitem/results'; $this->variables[] = 'Results.url'; $this->formats[] = 'application/vnd.ims.lis.v2.resultcontainer+json'; $this->methods[] = 'GET'; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { global $CFG, $DB; $params = $this->parse_template(); $contextid = $params['context_id']; $itemid = $params['item_id']; $isget = $response->get_request_method() === self::HTTP_GET; // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2. $typeid = optional_param('type_id', null, PARAM_INT); $scope = gradebookservices::SCOPE_GRADEBOOKSERVICES_RESULT_READ; try { if (!$this->check_tool($typeid, $response->get_request_data(), array($scope))) { throw new \Exception(null, 401); } $typeid = $this->get_service()->get_type()->id; if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) { throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404); } if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) { throw new \Exception('Not allowed in context', 403); } if (!$DB->record_exists('grade_items', array('id' => $itemid))) { throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404); } $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid); if ($item === false) { throw new \Exception('Line item does not exist', 404); } $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid); $ltilinkid = null; if (isset($item->iteminstance)) { $ltilinkid = $item->iteminstance; } else if ($gbs && isset($gbs->ltilinkid)) { $ltilinkid = $gbs->ltilinkid; } if ($ltilinkid != null) { if (is_null($typeid)) { if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid, $this->get_service()->get_tool_proxy()->id))) { $response->set_code(403); $response->set_reason("Invalid LTI id supplied."); return; } } else { if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid, $typeid))) { $response->set_code(403); $response->set_reason("Invalid LTI id supplied."); return; } } } require_once($CFG->libdir.'/gradelib.php'); switch ($response->get_request_method()) { case 'GET': $useridfilter = optional_param('user_id', 0, PARAM_INT); $limitnum = optional_param('limit', 0, PARAM_INT); $limitfrom = optional_param('from', 0, PARAM_INT); $typeid = optional_param('type_id', null, PARAM_TEXT); $json = $this->get_json_for_get_request($item->id, $limitfrom, $limitnum, $useridfilter, $typeid, $response); $response->set_content_type($this->formats[0]); $response->set_body($json); break; default: // Should not be possible. $response->set_code(405); $response->set_reason("Invalid request method specified."); return; } $response->set_body($json); } catch (\Exception $e) { $response->set_code($e->getCode()); $response->set_reason($e->getMessage()); } } /** * Generate the JSON for a GET request. * * @param int $itemid Grade item instance ID * @param int $limitfrom Offset for the first result to include in this paged set * @param int $limitnum Maximum number of results to include in the response, ignored if zero * @param int $useridfilter The user id to filter the results. * @param int $typeid Lti tool typeid (or null) * @param \mod_lti\local\ltiservice\response $response The response element needed to add a header. * * @return string */ private function get_json_for_get_request($itemid, $limitfrom, $limitnum, $useridfilter, $typeid, $response) { if ($useridfilter > 0) { $grades = \grade_grade::fetch_all(array('itemid' => $itemid, 'userid' => $useridfilter)); } else { $grades = \grade_grade::fetch_all(array('itemid' => $itemid)); } $firstpage = null; $nextpage = null; $prevpage = null; $lastpage = null; if ($grades && isset($limitnum) && $limitnum > 0) { // Since we only display grades that have been modified, we need to filter first in order to support // paging. $resultgrades = array_filter($grades, function ($grade) { return !empty($grade->timemodified); }); // We save the total count to calculate the last page. $totalcount = count($resultgrades); // We slice to the requested item offset to insure proper item is always first, and we always return // first pageset of any remaining items. $grades = array_slice($resultgrades, $limitfrom); if (count($grades) > 0) { $pagedgrades = array_chunk($grades, $limitnum); $pageset = 0; $grades = $pagedgrades[$pageset]; } if ($limitfrom >= $totalcount || $limitfrom < 0) { $outofrange = true; } else { $outofrange = false; } $limitprev = $limitfrom - $limitnum >= 0 ? $limitfrom - $limitnum : 0; $limitcurrent = $limitfrom; $limitlast = $totalcount - $limitnum + 1 >= 0 ? $totalcount - $limitnum + 1 : 0; $limitfrom += $limitnum; $baseurl = new \moodle_url($this->get_endpoint()); if (is_null($typeid)) { $baseurl->param('limit', $limitnum); if (($limitfrom <= $totalcount - 1) && (!$outofrange)) { $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]); } $firstpage = new \moodle_url($baseurl, ['from' => 0]); $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]); $lastpage = new \moodle_url($baseurl, ['from' => $limitlast]); if (($limitcurrent > 0) && (!$outofrange)) { $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]); } } else { $baseurl->params(['type_id' => $typeid, 'limit' => $limitnum]); if (($limitfrom <= $totalcount - 1) && (!$outofrange)) { $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]); } $firstpage = new \moodle_url($baseurl, ['from' => 0]); $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]); if (($limitcurrent > 0) && (!$outofrange)) { $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]); } } } $jsonresults = []; $lineitem = new lineitem($this->get_service()); $endpoint = $lineitem->get_endpoint(); if ($grades) { foreach ($grades as $grade) { if (!empty($grade->timemodified)) { array_push($jsonresults, gradebookservices::result_for_json($grade, $endpoint, $typeid)); } } } if (isset($canonicalpage) && ($canonicalpage)) { $links = 'Link: <' . $firstpage->out() . '>; rel=“first”'; if (!is_null($prevpage)) { $links .= ', <' . $prevpage->out() . '>; rel=“prev”'; } $links .= ', <' . $canonicalpage->out() . '>; rel=“canonical”'; if (!is_null($nextpage)) { $links .= ', <' . $nextpage->out() . '>; rel=“next”'; } $links .= ', <' . $lastpage->out() . '>; rel=“last”'; $response->add_additional_header($links); } return json_encode($jsonresults); } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { global $COURSE, $CFG; if (strpos($value, '$Results.url') !== false) { require_once($CFG->libdir . '/gradelib.php'); $resolved = ''; $this->params['context_id'] = $COURSE->id; $id = optional_param('id', 0, PARAM_INT); // Course Module ID. if (!empty($id)) { $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); $id = $cm->instance; $item = grade_get_grades($COURSE->id, 'mod', 'lti', $id); if ($item && $item->items) { $this->params['item_id'] = $item->items[0]->id; $resolved = parent::get_endpoint(); } } $value = str_replace('$Results.url', $resolved, $value); } return $value; } } classes/local/resources/lineitems.php000064400000030553152267060740014026 0ustar00. /** * This file contains a class definition for the LineItem container resource * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\local\resources; use ltiservice_gradebookservices\local\service\gradebookservices; use mod_lti\local\ltiservice\resource_base; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing LineItem container. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class lineitems extends resource_base { /** * Class constructor. * * @param \ltiservice_gradebookservices\local\service\gradebookservices $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'LineItem.collection'; $this->template = '/{context_id}/lineitems'; $this->variables[] = 'LineItems.url'; $this->formats[] = 'application/vnd.ims.lis.v2.lineitemcontainer+json'; $this->formats[] = 'application/vnd.ims.lis.v2.lineitem+json'; $this->methods[] = self::HTTP_GET; $this->methods[] = self::HTTP_POST; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { global $DB; $params = $this->parse_template(); $contextid = $params['context_id']; $isget = $response->get_request_method() === self::HTTP_GET; if ($isget) { $contenttype = $response->get_accept(); } else { $contenttype = $response->get_content_type(); } $container = empty($contenttype) || ($contenttype === $this->formats[0]); // We will receive typeid when working with LTI 1.x, if not then we are in LTI 2. $typeid = optional_param('type_id', null, PARAM_INT); $scopes = array(gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM); if ($response->get_request_method() === self::HTTP_GET) { $scopes[] = gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ; } try { if (!$this->check_tool($typeid, $response->get_request_data(), $scopes)) { throw new \Exception(null, 401); } $typeid = $this->get_service()->get_type()->id; if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_POST)) || (!empty($contenttype) && !in_array($contenttype, $this->formats))) { throw new \Exception('No context or unsupported content type', 400); } if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) { throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404); } if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) { throw new \Exception('Not allowed in context', 403); } if ($response->get_request_method() !== self::HTTP_POST) { $resourceid = optional_param('resource_id', null, PARAM_TEXT); $ltilinkid = optional_param('resource_link_id', null, PARAM_TEXT); if (is_null($ltilinkid)) { $ltilinkid = optional_param('lti_link_id', null, PARAM_TEXT); } $tag = optional_param('tag', null, PARAM_TEXT); $limitnum = optional_param('limit', 0, PARAM_INT); $limitfrom = optional_param('from', 0, PARAM_INT); $itemsandcount = $this->get_service()->get_lineitems($contextid, $resourceid, $ltilinkid, $tag, $limitfrom, $limitnum, $typeid); $items = $itemsandcount[1]; $totalcount = $itemsandcount[0]; $json = $this->get_json_for_get_request($items, $resourceid, $ltilinkid, $tag, $limitfrom, $limitnum, $totalcount, $typeid, $response); $response->set_content_type($this->formats[0]); } else { $json = $this->get_json_for_post_request($response->get_request_data(), $contextid, $typeid); $response->set_code(201); $response->set_content_type($this->formats[1]); } $response->set_body($json); } catch (\Exception $e) { $response->set_code($e->getCode()); $response->set_reason($e->getMessage()); } } /** * Generate the JSON for a GET request. * * @param array $items Array of lineitems * @param string $resourceid Resource identifier used for filtering, may be null * @param string $ltilinkid Resource Link identifier used for filtering, may be null * @param string $tag Tag identifier used for filtering, may be null * @param int $limitfrom Offset of the first line item to return * @param int $limitnum Maximum number of line items to return, ignored if zero or less * @param int $totalcount Number of total lineitems before filtering for paging * @param int $typeid Maximum number of line items to return, ignored if zero or less * @param \mod_lti\local\ltiservice\response $response * @return string */ private function get_json_for_get_request($items, $resourceid, $ltilinkid, $tag, $limitfrom, $limitnum, $totalcount, $typeid, $response) { $firstpage = null; $nextpage = null; $prevpage = null; $lastpage = null; if (isset($limitnum) && $limitnum > 0) { if ($limitfrom >= $totalcount || $limitfrom < 0) { $outofrange = true; } else { $outofrange = false; } $limitprev = $limitfrom - $limitnum >= 0 ? $limitfrom - $limitnum : 0; $limitcurrent = $limitfrom; $limitlast = $totalcount - $limitnum + 1 >= 0 ? $totalcount - $limitnum + 1 : 0; $limitfrom += $limitnum; $baseurl = new \moodle_url($this->get_endpoint()); if (isset($resourceid)) { $baseurl->param('resource_id', $resourceid); } if (isset($ltilinkid)) { $baseurl->param('resource_link_id', $ltilinkid); } if (isset($tag)) { $baseurl->param('tag', $tag); } if (is_null($typeid)) { $baseurl->param('limit', $limitnum); if (($limitfrom <= $totalcount - 1) && (!$outofrange)) { $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]); } $firstpage = new \moodle_url($baseurl, ['from' => 0]); $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]); $lastpage = new \moodle_url($baseurl, ['from' > $limitlast]); if (($limitcurrent > 0) && (!$outofrange)) { $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]); } } else { $baseurl->params(['type_id' => $typeid, 'limit' => $limitnum]); if (($limitfrom <= $totalcount - 1) && (!$outofrange)) { $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]); } $firstpage = new \moodle_url($baseurl, ['from' => 0]); $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]); $lastpage = new \moodle_url($baseurl, ['from' => $limitlast]); if (($limitcurrent > 0) && (!$outofrange)) { $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]); } } } $jsonitems = []; $endpoint = parent::get_endpoint(); foreach ($items as $item) { array_push($jsonitems, gradebookservices::item_for_json($item, $endpoint, $typeid)); } if (isset($canonicalpage) && ($canonicalpage)) { $links = 'Link: <' . $firstpage->out() . '>; rel=“first”'; if (!is_null($prevpage)) { $links .= ', <' . $prevpage->out() . '>; rel=“prev”'; } $links .= ', <' . $canonicalpage->out(). '>; rel=“canonical”'; if (!is_null($nextpage)) { $links .= ', <' . $nextpage->out() . '>; rel=“next”'; } $links .= ', <' . $lastpage->out() . '>; rel=“last”'; $response->add_additional_header($links); } return json_encode($jsonitems); } /** * Generate the JSON for a POST request. * * @param string $body POST body * @param string $contextid Course ID * @param string $typeid * * @return string * @throws \Exception */ private function get_json_for_post_request($body, $contextid, $typeid) { global $CFG, $DB; $json = json_decode($body); if (empty($json) || !isset($json->scoreMaximum) || !isset($json->label)) { throw new \Exception('No label or Score Maximum', 400); } if (is_numeric($json->scoreMaximum)) { $max = $json->scoreMaximum; } else { throw new \Exception(null, 400); } require_once($CFG->libdir.'/gradelib.php'); $resourceid = (isset($json->resourceId)) ? $json->resourceId : ''; $ltilinkid = (isset($json->resourceLinkId)) ? $json->resourceLinkId : null; if ($ltilinkid == null) { $ltilinkid = (isset($json->ltiLinkId)) ? $json->ltiLinkId : null; } if ($ltilinkid != null) { if (is_null($typeid)) { if (!gradebookservices::check_lti_id($ltilinkid, $contextid, $this->get_service()->get_tool_proxy()->id)) { throw new \Exception(null, 403); } } else { if (!gradebookservices::check_lti_1x_id($ltilinkid, $contextid, $typeid)) { throw new \Exception(null, 403); } } } $tag = (isset($json->tag)) ? $json->tag : ''; if (is_null($typeid)) { $toolproxyid = $this->get_service()->get_tool_proxy()->id; $baseurl = null; } else { $toolproxyid = null; $baseurl = lti_get_type_type_config($typeid)->lti_toolurl; } $gradebookservices = new gradebookservices(); $id = $gradebookservices->add_standalone_lineitem($contextid, $json->label, $max, $baseurl, $ltilinkid, $resourceid, $tag, $typeid, $toolproxyid); if (is_null($typeid)) { $json->id = parent::get_endpoint() . "/{$id}/lineitem"; } else { $json->id = parent::get_endpoint() . "/{$id}/lineitem?type_id={$typeid}"; } return json_encode($json, JSON_UNESCAPED_SLASHES); } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { global $COURSE; if (strpos($value, '$LineItems.url') !== false) { $this->params['context_id'] = $COURSE->id; $query = ''; if (($tool = $this->get_service()->get_type())) { $query = "?type_id={$tool->id}"; } $value = str_replace('$LineItems.url', parent::get_endpoint() . $query, $value); } return $value; } } classes/local/resources/lineitem.php000064400000033201152267060740013634 0ustar00. /** * This file contains a class definition for the LineItem resource * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\local\resources; use ltiservice_gradebookservices\local\service\gradebookservices; use mod_lti\local\ltiservice\resource_base; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing LineItem. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class lineitem extends resource_base { /** * Class constructor. * * @param gradebookservices $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'LineItem.item'; $this->template = '/{context_id}/lineitems/{item_id}/lineitem'; $this->variables[] = 'LineItem.url'; $this->formats[] = 'application/vnd.ims.lis.v2.lineitem+json'; $this->methods[] = self::HTTP_GET; $this->methods[] = self::HTTP_PUT; $this->methods[] = self::HTTP_DELETE; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { global $CFG, $DB; $params = $this->parse_template(); $contextid = $params['context_id']; $itemid = $params['item_id']; $isget = $response->get_request_method() === self::HTTP_GET; // We will receive typeid when working with LTI 1.x, if not then we are in LTI 2. $typeid = optional_param('type_id', null, PARAM_INT); $scopes = array(gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM); if ($response->get_request_method() === self::HTTP_GET) { $scopes[] = gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ; } try { if (!$this->check_tool($typeid, $response->get_request_data(), $scopes)) { throw new \Exception(null, 401); } $typeid = $this->get_service()->get_type()->id; if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) { throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404); } if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) { throw new \Exception('Not allowed in context', 403); } if (!$DB->record_exists('grade_items', array('id' => $itemid))) { throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404); } $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid); if ($item === false) { throw new \Exception('Line item does not exist', 404); } require_once($CFG->libdir.'/gradelib.php'); switch ($response->get_request_method()) { case self::HTTP_GET: $this->get_request($response, $item, $typeid); break; case self::HTTP_PUT: $json = $this->process_put_request($response->get_request_data(), $item, $typeid); $response->set_body($json); $response->set_code(200); break; case self::HTTP_DELETE: $this->process_delete_request($item); $response->set_code(204); break; } } catch (\Exception $e) { $response->set_code($e->getCode()); $response->set_reason($e->getMessage()); } } /** * Process a GET request. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. * @param object $item Grade item instance. * @param string $typeid Tool Type Id */ private function get_request($response, $item, $typeid) { $response->set_content_type($this->formats[0]); $lineitem = gradebookservices::item_for_json($item, substr(parent::get_endpoint(), 0, strrpos(parent::get_endpoint(), "/", -10)), $typeid); $response->set_body(json_encode($lineitem)); } /** * Process a PUT request. * * @param string $body PUT body * @param \ltiservice_gradebookservices\local\resources\lineitem $olditem Grade item instance * @param string $typeid Tool Type Id * * @return string * @throws \Exception */ private function process_put_request($body, $olditem, $typeid) { global $DB; $json = json_decode($body); if (empty($json) || !isset($json->scoreMaximum) || !isset($json->label)) { throw new \Exception(null, 400); } $item = \grade_item::fetch(array('id' => $olditem->id, 'courseid' => $olditem->courseid)); $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($olditem->id); $updategradeitem = false; $rescalegrades = false; $oldgrademax = grade_floatval($item->grademax); $upgradegradebookservices = false; if ($item->itemname !== $json->label) { $updategradeitem = true; } $item->itemname = $json->label; if (!is_numeric($json->scoreMaximum)) { throw new \Exception(null, 400); } else { if (grade_floats_different($oldgrademax, grade_floatval($json->scoreMaximum))) { $updategradeitem = true; $rescalegrades = true; } $item->grademax = grade_floatval($json->scoreMaximum); } if ($gbs) { $resourceid = (isset($json->resourceId)) ? $json->resourceId : ''; $tag = (isset($json->tag)) ? $json->tag : ''; if ($gbs->tag !== $tag || $gbs->resourceid !== $resourceid) { $upgradegradebookservices = true; } $gbs->tag = $tag; $gbs->resourceid = $resourceid; $incomingurl = null; $incomingparams = null; if (isset($json->submissionReview)) { $incomingurl = $json->submissionReview->url ?? 'DEFAULT'; if (isset($json->submissionReview->custom)) { $incomingparams = params_to_string($json->submissionReview->custom); } } if ($gbs->subreviewurl ?? null !== $incomingurl || $gbs->subreviewparams ?? null !== $incomingparams) { $upgradegradebookservices = true; $gbs->subreviewurl = $incomingurl; $gbs->subreviewparams = $incomingparams; } } $ltilinkid = null; if (isset($json->resourceLinkId)) { if (is_numeric($json->resourceLinkId)) { $ltilinkid = $json->resourceLinkId; if ($gbs) { if (intval($gbs->ltilinkid) !== intval($json->resourceLinkId)) { $gbs->ltilinkid = $json->resourceLinkId; $upgradegradebookservices = true; } } else { if (intval($item->iteminstance) !== intval($json->resourceLinkId)) { $item->iteminstance = intval($json->resourceLinkId); $updategradeitem = true; } } } else { throw new \Exception(null, 400); } } else if (isset($json->ltiLinkId)) { if (is_numeric($json->ltiLinkId)) { $ltilinkid = $json->ltiLinkId; if ($gbs) { if (intval($gbs->ltilinkid) !== intval($json->ltiLinkId)) { $gbs->ltilinkid = $json->ltiLinkId; $upgradegradebookservices = true; } } else { if (intval($item->iteminstance) !== intval($json->ltiLinkId)) { $item->iteminstance = intval($json->ltiLinkId); $updategradeitem = true; } } } else { throw new \Exception(null, 400); } } if ($ltilinkid != null) { if (is_null($typeid)) { if (!gradebookservices::check_lti_id($ltilinkid, $item->courseid, $this->get_service()->get_tool_proxy()->id)) { throw new \Exception(null, 403); } } else { if (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid, $typeid)) { throw new \Exception(null, 403); } } } if ($updategradeitem) { if (!$item->update('mod/ltiservice_gradebookservices')) { throw new \Exception(null, 500); } if ($rescalegrades) { $item->rescale_grades_keep_percentage(0, $oldgrademax, 0, $item->grademax); } } $lineitem = new lineitem($this->get_service()); $endpoint = $lineitem->get_endpoint(); if ($upgradegradebookservices) { if (is_null($typeid)) { $toolproxyid = $this->get_service()->get_tool_proxy()->id; $baseurl = null; } else { $toolproxyid = null; $baseurl = lti_get_type_type_config($typeid)->lti_toolurl; } $DB->update_record('ltiservice_gradebookservices', (object)array( 'id' => $gbs->id, 'gradeitemid' => $gbs->gradeitemid, 'courseid' => $gbs->courseid, 'toolproxyid' => $toolproxyid, 'typeid' => $typeid, 'baseurl' => $baseurl, 'ltilinkid' => $ltilinkid, 'resourceid' => $resourceid, 'tag' => $gbs->tag, 'subreviewurl' => $gbs->subreviewurl, 'subreviewparams' => $gbs->subreviewparams )); } if (is_null($typeid)) { $id = "{$endpoint}"; $json->id = $id; } else { $id = "{$endpoint}?type_id={$typeid}"; $json->id = $id; } return json_encode($json, JSON_UNESCAPED_SLASHES); } /** * Process a DELETE request. * * @param \ltiservice_gradebookservices\local\resources\lineitem $item Grade item instance * @throws \Exception */ private function process_delete_request($item) { global $DB; $gradeitem = \grade_item::fetch(array('id' => $item->id)); if (($gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($item->id)) == false) { throw new \Exception(null, 403); } if (!$gradeitem->delete('mod/ltiservice_gradebookservices')) { throw new \Exception(null, 500); } else { $sqlparams = array(); $sqlparams['id'] = $gbs->id; if (!$DB->delete_records('ltiservice_gradebookservices', $sqlparams)) { throw new \Exception(null, 500); } } } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { global $COURSE, $CFG; if (strpos($value, '$LineItem.url') !== false) { $resolved = ''; require_once($CFG->libdir . '/gradelib.php'); $this->params['context_id'] = $COURSE->id; if ($tool = $this->get_service()->get_type()) { $this->params['tool_code'] = $tool->id; } $id = optional_param('id', 0, PARAM_INT); // Course Module ID. if (empty($id)) { $hint = optional_param('lti_message_hint', "", PARAM_TEXT); if ($hint) { $hintdec = json_decode($hint); if (isset($hintdec->cmid)) { $id = $hintdec->cmid; } } } if (!empty($id)) { $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); $id = $cm->instance; $item = grade_get_grades($COURSE->id, 'mod', 'lti', $id); if ($item && $item->items) { $this->params['item_id'] = $item->items[0]->id; $resolved = parent::get_endpoint(); $resolved .= "?type_id={$tool->id}"; } } $value = str_replace('$LineItem.url', $resolved, $value); } return $value; } } classes/local/resources/scores.php000064400000023407152267060740013333 0ustar00. /** * This file contains a class definition for the LISResult container resource * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\local\resources; use ltiservice_gradebookservices\local\service\gradebookservices; use mod_lti\local\ltiservice\resource_base; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing LISResult container. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class scores extends resource_base { /** * Class constructor. * * @param \ltiservice_gradebookservices\local\service\gradebookservices $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'Score.collection'; $this->template = '/{context_id}/lineitems/{item_id}/lineitem/scores'; $this->variables[] = 'Scores.url'; $this->formats[] = 'application/vnd.ims.lis.v1.scorecontainer+json'; $this->formats[] = 'application/vnd.ims.lis.v1.score+json'; $this->methods[] = 'POST'; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { global $CFG, $DB; $params = $this->parse_template(); $contextid = $params['context_id']; $itemid = $params['item_id']; // GET is disabled by the moment, but we have the code ready // for a future implementation. $isget = $response->get_request_method() === 'GET'; if ($isget) { $contenttype = $response->get_accept(); } else { $contenttype = $response->get_content_type(); } $container = empty($contenttype) || ($contenttype === $this->formats[0]); // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2. $typeid = optional_param('type_id', null, PARAM_ALPHANUM); $scope = gradebookservices::SCOPE_GRADEBOOKSERVICES_SCORE; try { if (!$this->check_tool($typeid, $response->get_request_data(), array($scope))) { throw new \Exception(null, 401); } $typeid = $this->get_service()->get_type()->id; if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_POST)) || (!empty($contenttype) && !in_array($contenttype, $this->formats))) { throw new \Exception('No context or unsupported content type', 400); } if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) { throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404); } if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) { throw new \Exception('Not allowed in context', 403); } if (!$DB->record_exists('grade_items', array('id' => $itemid))) { throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404); } $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid); if ($item === false) { throw new \Exception('Line item does not exist', 404); } $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid); $ltilinkid = null; if (isset($item->iteminstance)) { $ltilinkid = $item->iteminstance; } else if ($gbs && isset($gbs->ltilinkid)) { $ltilinkid = $gbs->ltilinkid; } if ($ltilinkid != null) { if (is_null($typeid)) { if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid, $this->get_service()->get_tool_proxy()->id))) { $response->set_code(403); $response->set_reason("Invalid LTI id supplied."); return; } } else { if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid, $typeid))) { $response->set_code(403); $response->set_reason("Invalid LTI id supplied."); return; } } } $json = '[]'; require_once($CFG->libdir.'/gradelib.php'); switch ($response->get_request_method()) { case 'GET': $response->set_code(405); $response->set_reason("GET requests are not allowed."); break; case 'POST': try { $json = $this->get_json_for_post_request($response, $response->get_request_data(), $item, $contextid, $typeid); $response->set_content_type($this->formats[1]); } catch (\Exception $e) { $response->set_code($e->getCode()); $response->set_reason($e->getMessage()); } break; default: // Should not be possible. $response->set_code(405); $response->set_reason("Invalid request method specified."); return; } $response->set_body($json); } catch (\Exception $e) { $response->set_code($e->getCode()); $response->set_reason($e->getMessage()); } } /** * Generate the JSON for a POST request. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. * @param string $body POST body * @param object $item Grade item instance * @param string $contextid * @param string $typeid * * @throws \Exception */ private function get_json_for_post_request($response, $body, $item, $contextid, $typeid) { $score = json_decode($body); if (empty($score) || !isset($score->userId) || !isset($score->timestamp) || !isset($score->gradingProgress) || !isset($score->activityProgress) || !isset($score->timestamp) || isset($score->timestamp) && !gradebookservices::validate_iso8601_date($score->timestamp) || (isset($score->scoreGiven) && !is_numeric($score->scoreGiven)) || (isset($score->scoreGiven) && !isset($score->scoreMaximum)) || (isset($score->scoreMaximum) && !is_numeric($score->scoreMaximum)) || (!gradebookservices::is_user_gradable_in_course($contextid, $score->userId)) ) { throw new \Exception('Incorrect score received' . $body, 400); } $score->timemodified = intval($score->timestamp); if (!isset($score->scoreMaximum)) { $score->scoreMaximum = 1; } $response->set_code(200); $grade = \grade_grade::fetch(array('itemid' => $item->id, 'userid' => $score->userId)); if ($grade && !empty($grade->timemodified)) { if ($grade->timemodified >= strtotime($score->timestamp)) { $exmsg = "Refusing score with an earlier timestamp for item " . $item->id . " and user " . $score->userId; throw new \Exception($exmsg, 409); } } if (isset($score->scoreGiven)) { if ($score->gradingProgress != 'FullyGraded') { $score->scoreGiven = null; } } $this->get_service()->save_grade_item($item, $score, $score->userId); } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { global $COURSE, $CFG; if (strpos($value, '$Scores.url') !== false) { require_once($CFG->libdir . '/gradelib.php'); $resolved = ''; $this->params['context_id'] = $COURSE->id; $id = optional_param('id', 0, PARAM_INT); // Course Module ID. if (!empty($id)) { $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); $id = $cm->instance; $item = grade_get_grades($COURSE->id, 'mod', 'lti', $id); if ($item && $item->items) { $this->params['item_id'] = $item->items[0]->id; $resolved = parent::get_endpoint(); } } $value = str_replace('$Scores.url', $resolved, $value); } return $value; } } classes/task/cleanup_task.php000064400000003556152267060740012347 0ustar00. /** * A scheduled task for gradebookservices. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\task; use core\task\scheduled_task; use ltiservice_gradebookservices\local\service\gradebookservices; defined('MOODLE_INTERNAL') || die(); /** * Class containing the scheduled task for gradebookservices. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cleanup_task extends scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('taskcleanup', 'ltiservice_gradebookservices'); } /** * Run forum cron. */ public function execute() { gradebookservices::delete_orphans_ltiservice_gradebookservices_rows(); } } upgrade.txt000064400000000557152267060740006754 0ustar00=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in the lti code. === 4.1 === * The update_coupled_gradebookservices function now accept 2 additional optional parameters to update the newly added properties related to submission review support (url and custom params). tests/privacy/provider_test.php000064400000003453152267060740013003 0ustar00. /** * Unit tests for ltiservice_gradebookservices privacy provider. * * @package ltiservice_gradebookservices * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\tests\provider_testcase; /** * Unit tests for ltiservice_gradebookservices privacy provider. * * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { /** * Basic setup for these tests. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Test getting the context for the user ID related to this plugin. */ public function test_get_contexts_for_userid(): void { $user = $this->getDataGenerator()->create_user(); $contextlist = \ltiservice_gradebookservices\privacy\provider::get_contexts_for_userid($user->id); $this->assertEmpty($contextlist); } } tests/lineitem_test.php000064400000036300152267060740011277 0ustar00. namespace ltiservice_gradebookservices; use ltiservice_gradebookservices\local\resources\lineitem; use ltiservice_gradebookservices\local\resources\results; use ltiservice_gradebookservices\local\resources\scores; use ltiservice_gradebookservices\local\service\gradebookservices; /** * Unit tests for lti lineitem. * * @package ltiservice_gradebookservices * @category test * @copyright 2022 Cengage Group * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_lti\service\gradebookservices\local\resources\lineitem */ final class lineitem_test extends \advanced_testcase { /** * @covers ::execute * * Test updating the line item with submission review. */ public function test_execute_put_nosubreview(): void { global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); $this->resetAfterTest(); $this->setAdminUser(); $resourceid = 'test-resource-id'; $tag = 'tag'; $course = $this->getDataGenerator()->create_course(); $typeid = $this->create_type(); // The 1st item in the array is the items count. $gbservice = new gradebookservices(); $gbservice->set_type(lti_get_type($typeid)); $this->create_graded_lti($typeid, $course, $resourceid, $tag); $gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertFalse(isset($lineitem->submissionReview)); $lineitemresource = new lineitem($gbservice); $this->set_server_for_put($course, $typeid, $lineitem); $response = new \mod_lti\local\ltiservice\response(); $lineitem->resourceId = $resourceid.'modified'; $lineitem->tag = $tag.'modified'; $response->set_request_data(json_encode($lineitem)); $lineitemresource->execute($response); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertFalse(isset($lineitem->submissionReview)); $this->assertEquals($resourceid.'modified', $lineitem->resourceId); $this->assertEquals($tag.'modified', $lineitem->tag); $responseitem = json_decode($response->get_body()); $this->assertEquals($resourceid.'modified', $responseitem->resourceId); } /** * @covers ::execute * * Test updating the line item with submission review. */ public function test_execute_put_withsubreview(): void { global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); $this->resetAfterTest(); $this->setAdminUser(); $resourceid = 'test-resource-id'; $tag = 'tag'; $subreviewurl = 'https://subreview.example.com'; $subreviewparams = 'a=2'; $course = $this->getDataGenerator()->create_course(); $typeid = $this->create_type(); // The 1st item in the array is the items count. $gbservice = new gradebookservices(); $gbservice->set_type(lti_get_type($typeid)); $this->create_graded_lti($typeid, $course, $resourceid, $tag, $subreviewurl, $subreviewparams); $gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertTrue(isset($lineitem->submissionReview)); $lineitemresource = new lineitem($gbservice); $this->set_server_for_put($course, $typeid, $lineitem); $response = new \mod_lti\local\ltiservice\response(); $lineitem->resourceId = $resourceid.'modified'; $lineitem->tag = $tag.'modified'; $lineitem->submissionReview->url = $subreviewurl.'modified'; $lineitem->submissionReview->custom = ['a' => '3']; $response->set_request_data(json_encode($lineitem)); $lineitemresource->execute($response); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertEquals($resourceid.'modified', $lineitem->resourceId); $this->assertEquals($subreviewurl.'modified', $lineitem->submissionReview->url); $custom = $lineitem->submissionReview->custom; $this->assertEquals('a=3', join("\n", array_map(fn($k) => $k.'='.$custom[$k], array_keys($custom)))); $responseitem = json_decode($response->get_body()); $this->assertEquals($resourceid.'modified', $responseitem->resourceId); $this->assertEquals($subreviewurl.'modified', $responseitem->submissionReview->url); } /** * @covers ::execute * * Test updating the line item with submission review. */ public function test_execute_put_addsubreview(): void { global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); $this->resetAfterTest(); $this->setAdminUser(); $resourceid = 'test-resource-id'; $tag = 'tag'; $subreviewurl = 'https://subreview.example.com'; $course = $this->getDataGenerator()->create_course(); $typeid = $this->create_type(); // The 1st item in the array is the items count. $gbservice = new gradebookservices(); $gbservice->set_type(lti_get_type($typeid)); $this->create_graded_lti($typeid, $course, $resourceid, $tag); $gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertFalse(isset($lineitem->submissionReview)); $lineitemresource = new lineitem($gbservice); $this->set_server_for_put($course, $typeid, $lineitem); $response = new \mod_lti\local\ltiservice\response(); $lineitem->resourceId = $resourceid.'modified'; $lineitem->tag = $tag.'modified'; $lineitem->submissionReview = ['url' => $subreviewurl]; $response->set_request_data(json_encode($lineitem)); $lineitemresource->execute($response); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertEquals($resourceid.'modified', $lineitem->resourceId); $this->assertEquals($subreviewurl, $lineitem->submissionReview->url); $this->assertFalse(isset($lineitem->submissionReview->custom)); $responseitem = json_decode($response->get_body()); $this->assertEquals($resourceid.'modified', $responseitem->resourceId); $this->assertEquals($subreviewurl, $responseitem->submissionReview->url); $this->assertFalse(isset($responseitem->submissionReview->custom)); } /** * Test running a series of score updates, highlighting problems with the score posting logic. * * @covers ::execute * * @return void */ public function test_sequential_score_posts(): void { global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); $this->resetAfterTest(); $resourceid = 'test-resource-id'; $tag = 'tag'; $course = $this->getDataGenerator()->create_course(); $typeid = $this->create_type(); $user = $this->getDataGenerator()->create_and_enrol($course); // Create mod instance with line item - nothing pushed via services yet. $gbservice = new gradebookservices(); $gbservice->set_type(lti_get_type($typeid)); $modinstance = $this->create_graded_lti($typeid, $course, $resourceid, $tag); $gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); // The 1st item in the array is the items count. // Post a score so that there's at least one grade present at the time of lineitem update. $score = new scores($gbservice); $_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_POST; $_SERVER['PATH_INFO'] = "/$course->id/lineitems/{$gradeitems[1][0]->id}/lineitem/scores?type_id=$typeid"; $token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/score']); $_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token; $_GET['type_id'] = (string)$typeid; $requestdata = [ 'scoreGiven' => "8.0", 'scoreMaximum' => "10.0", 'activityProgress' => "Completed", 'timestamp' => "2024-08-07T18:54:36.736+00:00", 'gradingProgress' => "FullyGraded", 'userId' => $user->id, ]; $response = new \mod_lti\local\ltiservice\response(); $response->set_content_type('application/vnd.ims.lis.v1.score+json'); $response->set_request_data(json_encode($requestdata)); $score->execute($response); // The grade in Moodle should reflect the score->timestamp, not the time of the score post. $grades = grade_get_grades($course->id, 'mod', 'lti', $modinstance->id, $user->id); $studentgrade = array_shift($grades->items[0]->grades); $this->assertEquals(strtotime($requestdata['timestamp']), $studentgrade->dategraded); // Read the results via the service. This should also return the correct dategraded time (timemodified in LTI terms). $_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_GET; $_SERVER['PATH_INFO'] = "/$course->id/lineitems/{$gradeitems[1][0]->id}/lineitem/results?type_id=$typeid"; $token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly']); $_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token; $_GET['type_id'] = (string)$typeid; $result = new results($gbservice); $response = new \mod_lti\local\ltiservice\response(); $response->set_content_type('application/vnd.ims.lis.v2.resultcontainer+json'); $result->execute($response); $body = json_decode($response->get_body()); $result = array_shift($body); $this->assertEquals(strtotime($requestdata['timestamp']), strtotime($result->timestamp)); // Now, try to post a newer score using a timestamp that is greater than the one originally sent, but less than the time at // which the score was last posted. This should be valid since the timestamp is greater than the original posted score. $_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_POST; $_SERVER['PATH_INFO'] = "/$course->id/lineitems/{$gradeitems[1][0]->id}/lineitem/scores?type_id=$typeid"; $token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/score']); $_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token; $_GET['type_id'] = (string)$typeid; $requestdata['scoreGiven'] = "14"; $requestdata['timestamp'] = "2024-08-08T18:54:36.736+00:00"; $response = new \mod_lti\local\ltiservice\response(); $response->set_content_type('application/vnd.ims.lis.v1.score+json'); $response->set_request_data(json_encode($requestdata)); $score->execute($response); $this->assertEquals(200, json_decode($response->get_code())); // Finally, post a score that's just been updated (i.e. score->timestamp = now). $_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_POST; $_SERVER['PATH_INFO'] = "/$course->id/lineitems/{$gradeitems[1][0]->id}/lineitem/scores?type_id=$typeid"; $token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/score']); $_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token; $_GET['type_id'] = (string)$typeid; $requestdata['scoreGiven'] = "15"; $requestdata['timestamp'] = date('c', time()); $response = new \mod_lti\local\ltiservice\response(); $response->set_content_type('application/vnd.ims.lis.v1.score+json'); $response->set_request_data(json_encode($requestdata)); $score->execute($response); $this->assertEquals(200, json_decode($response->get_code())); } /** * Inserts a graded lti instance, which should create a grade_item and gradebookservices record. * * @param int $typeid Type ID of the LTI Tool. * @param object $course course where to add the lti instance. * @param string|null $resourceid resource id * @param string|null $tag tag * @param string|null $subreviewurl submission review url * @param string|null $subreviewparams submission review custom params * * @return object lti instance created */ private function create_graded_lti(int $typeid, object $course, ?string $resourceid, ?string $tag, ?string $subreviewurl = null, ?string $subreviewparams = null): object { $lti = ['course' => $course->id, 'typeid' => $typeid, 'instructorchoiceacceptgrades' => LTI_SETTING_ALWAYS, 'grade' => 10, 'lineitemresourceid' => $resourceid, 'lineitemtag' => $tag, 'lineitemsubreviewurl' => $subreviewurl, 'lineitemsubreviewparams' => $subreviewparams]; return $this->getDataGenerator()->create_module('lti', $lti, array()); } /** * Creates a new LTI Tool Type. */ private function create_type() { $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->clientid = "Test client ID"; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $config = new \stdClass(); $config->ltiservice_gradesynchronization = 2; return lti_add_type($type, $config); } /** * Sets the server info and get to be configured for a PUT operation, * including having a proper auth token attached. * * @param object $course course where to add the lti instance. * @param int $typeid * @param object $lineitem */ private function set_server_for_put(object $course, int $typeid, object $lineitem) { $_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_PUT; $_SERVER['PATH_INFO'] = "/$course->id/lineitems$lineitem->id"; $token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/lineitem']); $_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token; $_GET['type_id'] = (string)$typeid; } } tests/task/cleanup_test.php000064400000010571152267060740012064 0ustar00. namespace ltiservice_gradebookservices\task; /** * Tests cleaning up the gradebook services task. * * @package ltiservice_gradebookservices * @category test * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class cleanup_test extends \advanced_testcase { /** * Test set up. * * This is executed before running any test in this file. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Test the cleanup task. */ public function test_cleanup_task(): void { global $DB; // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a few LTI items. $lti = $this->getDataGenerator()->create_module('lti', ['course' => $course->id]); $lti2 = $this->getDataGenerator()->create_module('lti', ['course' => $course->id]); $conditions = [ 'courseid' => $course->id, 'itemtype' => 'mod', 'itemmodule' => 'lti', 'iteminstance' => $lti->id ]; // Get the grade items. $gradeitem = $DB->get_record('grade_items', $conditions); $conditions['iteminstance'] = $lti2->id; $gradeitem2 = $DB->get_record('grade_items', $conditions); // Insert these into the 'ltiservice_gradebookservices' table. $data = new \stdClass(); $data->gradeitemid = $gradeitem->id; $data->courseid = $course->id; $DB->insert_record('ltiservice_gradebookservices', $data); $data->gradeitemid = $gradeitem2->id; $DB->insert_record('ltiservice_gradebookservices', $data); $task = new cleanup_task(); $task->execute(); // Check they both still exist. $this->assertEquals(2, $DB->count_records('ltiservice_gradebookservices')); // Delete the first LTI activity. course_delete_module($lti->cmid); // Run the task again. $task = new cleanup_task(); $task->execute(); // Check only the second grade item exists. $gradebookserviceitems = $DB->get_records('ltiservice_gradebookservices'); $this->assertCount(1, $gradebookserviceitems); $gradebookserviceitem = reset($gradebookserviceitems); $this->assertEquals($gradeitem2->id, $gradebookserviceitem->gradeitemid); } /** * Test the cleanup task with a manual grade item. */ public function test_cleanup_task_with_manual_item(): void { global $CFG, $DB; // This is required when running the unit test in isolation. require_once($CFG->libdir . '/gradelib.php'); // Create a manual grade item for a course. $course = $this->getDataGenerator()->create_course(); $params = [ 'courseid' => $course->id, 'itemtype' => 'manual' ]; $gradeitem = new \grade_item($params); $gradeitem->insert(); // Insert it into the 'ltiservice_gradebookservices' table. $data = new \stdClass(); $data->gradeitemid = $gradeitem->id; $data->courseid = $course->id; $DB->insert_record('ltiservice_gradebookservices', $data); // Run the task. $task = new cleanup_task(); $task->execute(); // Check it still exist. $this->assertEquals(1, $DB->count_records('ltiservice_gradebookservices')); // Delete the manual item. $gradeitem->delete(); // Run the task again. $task = new cleanup_task(); $task->execute(); // Check it has been removed. $this->assertEquals(0, $DB->count_records('ltiservice_gradebookservices')); } } tests/gradebookservices_test.php000064400000040724152267060740013177 0ustar00. namespace ltiservice_gradebookservices; use ltiservice_gradebookservices\local\service\gradebookservices; /** * Unit tests for lti gradebookservices. * * @package ltiservice_gradebookservices * @category test * @copyright 2020 Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_lti\service\gradebookservices\local\gradebookservices */ final class gradebookservices_test extends \advanced_testcase { /** * Load the necessary libs for the tests. */ public static function setUpBeforeClass(): void { global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); parent::setUpBeforeClass(); } /** * @covers ::instance_added * * Test saving a graded LTI with resource and tag info (as a result of * content item selection) creates a gradebookservices record * that can be retrieved using the gradebook service API. */ public function test_lti_add_coupled_lineitem(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $typeid = $this->create_type(); $course = $this->getDataGenerator()->create_course(); $resourceid = 'test-resource-id'; $tag = 'tag'; $subreviewurl = 'https://subreview.example.com'; $subreviewparams = 'a=2'; $ltiinstance = $this->create_graded_lti($typeid, $course, $resourceid, $tag, $subreviewurl, $subreviewparams); $this->assertNotNull($ltiinstance); $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lti($ltiinstance->id); $this->assertNotNull($gbs); $this->assertEquals($resourceid, $gbs->resourceid); $this->assertEquals($tag, $gbs->tag); $this->assertEquals($subreviewurl, $gbs->subreviewurl); $this->assertEquals($subreviewparams, $gbs->subreviewparams); $this->assert_lineitems($course, $typeid, $ltiinstance->name, $ltiinstance, $resourceid, $tag, $subreviewurl, $subreviewparams); } /** * @covers ::instance_added * * Test saving a graded LTI with resource and tag info (as a result of * content item selection) creates a gradebookservices record * that can be retrieved using the gradebook service API. */ public function test_lti_add_coupled_lineitem_default_subreview(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $typeid = $this->create_type(); $course = $this->getDataGenerator()->create_course(); $resourceid = 'test-resource-id'; $tag = 'tag'; $ltiinstance = $this->create_graded_lti($typeid, $course, $resourceid, $tag, 'DEFAULT'); $this->assertNotNull($ltiinstance); $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lti($ltiinstance->id); $this->assertNotNull($gbs); $this->assertEquals('DEFAULT', $gbs->subreviewurl); $this->assert_lineitems($course, $typeid, $ltiinstance->name, $ltiinstance, $resourceid, $tag, 'DEFAULT'); } /** * @covers ::add_standalone_lineitem * * Test saving a standalone LTI lineitem with resource and tag info * that can be retrieved using the gradebook service API. */ public function test_lti_add_standalone_lineitem(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $resourceid = "test-resource-standalone"; $tag = "test-tag-standalone"; $typeid = $this->create_type(); $this->create_standalone_lineitem($course->id, $typeid, $resourceid, $tag); $this->assert_lineitems($course, $typeid, "manualtest", null, $resourceid, $tag); } /** * @covers ::find_ltiservice_gradebookservice_for_lti * * Test line item URL is populated for coupled line item only * if there is not another line item bound to the lti instance, * since in that case there would be no rule to define which of * the line items should be actually passed. */ public function test_get_launch_parameters_coupled(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $typeid = $this->create_type(); $course = $this->getDataGenerator()->create_course(); $ltiinstance = $this->create_graded_lti($typeid, $course, 'resource-id', 'tag', 'https://subreview.url', 'sub=review'); $this->assertNotNull($ltiinstance); $gbservice = new gradebookservices(); $params = $gbservice->get_launch_parameters('basic-lti-launch-request', $course->id, 111, $typeid, $ltiinstance->id); $this->assertEquals('$LineItem.url', $params['lineitem_url']); $this->assertEquals('$LineItem.url', $params['lineitem_url']); $this->create_standalone_lineitem($course->id, $typeid, 'resource-id', 'tag', $ltiinstance->id); $params = $gbservice->get_launch_parameters('basic-lti-launch-request', $course->id, 111, $typeid, $ltiinstance->id); $this->assertEquals('$LineItems.url', $params['lineitems_url']); // 2 line items for a single link, we cannot return a single line item url. $this->assertFalse(array_key_exists('$LineItem.url', $params)); } /** * @covers ::override_endpoint * * Test Submission Review URL and custom parameter is applied when the * launch is submission review. */ public function test_get_launch_parameters_coupled_subreview_override(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $typeid = $this->create_type(); $course = $this->getDataGenerator()->create_course(); $ltiinstance = $this->create_graded_lti($typeid, $course, 'resource-id', 'tag', 'https://example.com/subreview', 'action=review'); $this->assertNotNull($ltiinstance); $gbservice = new gradebookservices(); $overrides = $gbservice->override_endpoint('LtiSubmissionReviewRequest', 'https://example.com/lti', "color=blue", $course->id, $ltiinstance); $this->assertEquals('https://example.com/subreview', $overrides[0]); $this->assertEquals("color=blue\naction=review", $overrides[1]); } /** * @covers ::override_endpoint * * Test Submission Review URL and custom parameter is applied when the * launch is submission review. */ public function test_get_launch_parameters_coupled_subreview_override_default(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $typeid = $this->create_type(); $course = $this->getDataGenerator()->create_course(); $ltiinstance = $this->create_graded_lti($typeid, $course, 'resource-id', 'tag', 'DEFAULT', ''); $this->assertNotNull($ltiinstance); $gbservice = new gradebookservices(); $overrides = $gbservice->override_endpoint('LtiSubmissionReviewRequest', 'https://example.com/lti', "color=blue", $course->id, $ltiinstance); $this->assertEquals('https://example.com/lti', $overrides[0]); $this->assertEquals("color=blue", $overrides[1]); } /** * @covers ::get_launch_parameters * * Test line item URL is populated for not coupled line item only * if there is a single line item attached to that lti instance. */ public function test_get_launch_parameters_decoupled(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $typeid = $this->create_type(); $course = $this->getDataGenerator()->create_course(); $ltiinstance = $this->create_notgraded_lti($typeid, $course); $this->assertNotNull($ltiinstance); $gbservice = new gradebookservices(); $params = $gbservice->get_launch_parameters('basic-lti-launch-request', $course->id, 111, $typeid, $ltiinstance->id); $this->assertEquals('$LineItems.url', $params['lineitems_url']); $this->assertFalse(array_key_exists('$LineItem.url', $params)); $this->create_standalone_lineitem($course->id, $typeid, 'resource-id', 'tag', $ltiinstance->id); $params = $gbservice->get_launch_parameters('basic-lti-launch-request', $course->id, 111, $typeid, $ltiinstance->id); $this->assertEquals('$LineItems.url', $params['lineitems_url']); $this->assertEquals('$LineItem.url', $params['lineitem_url']); // 2 line items for a single link, we cannot return a single line item url. $this->create_standalone_lineitem($course->id, $typeid, 'resource-id', 'tag-2', $ltiinstance->id); $this->assertFalse(array_key_exists('$LineItem.url', $params)); } /** * @covers ::is_user_gradable_in_course * * Test if a user can be graded in a course. */ public function test_is_user_gradable_in_course(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $user1 = $generator->create_user(); $user2 = $generator->create_user(); $generator->enrol_user($user1->id, $course->id, 'student'); $generator->enrol_user($user2->id, $course->id, 'editingteacher'); $this->assertTrue(gradebookservices::is_user_gradable_in_course($course->id, $user1->id)); $this->assertFalse(gradebookservices::is_user_gradable_in_course($course->id, $user2->id)); } /** * Asserts a matching gradebookservices record exist with the matching tag and resourceid. * * @param object $course current course * @param int $typeid Type id of the tool * @param string $label Label of the line item * @param object|null $ltiinstance lti instance related to that line item * @param string|null $resourceid resourceid the line item should have * @param string|null $tag tag the line item should have * @param string|null $subreviewurl submission review url * @param string|null $subreviewparams submission review custom params */ private function assert_lineitems(object $course, int $typeid, string $label, ?object $ltiinstance, ?string $resourceid, ?string $tag, ?string $subreviewurl = null, ?string $subreviewparams = null): void { $gbservice = new gradebookservices(); $gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid); // The 1st item in the array is the items count. $this->assertEquals(1, $gradeitems[0]); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertEquals(10, $lineitem->scoreMaximum); $this->assertEquals($resourceid, $lineitem->resourceId); $this->assertEquals($tag, $lineitem->tag); $this->assertEquals($label, $lineitem->label); $this->assertEquals(!empty($subreviewurl), isset($lineitem->submissionReview)); if ($subreviewurl) { if ($subreviewurl == 'DEFAULT') { $this->assertFalse(isset($lineitem->submissionReview->url)); } else { $this->assertEquals($subreviewurl, $lineitem->submissionReview->url); } if ($subreviewparams) { $custom = $lineitem->submissionReview->custom; $this->assertEquals($subreviewparams, join("\n", array_map(fn($k) => $k.'='.$custom[$k], array_keys($custom)))); } else { $this->assertFalse(isset($lineitem->submissionReview->custom)); } } $gradeitems = $gbservice->get_lineitems($course->id, $resourceid, null, null, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); if (isset($ltiinstance)) { $gradeitems = $gbservice->get_lineitems($course->id, null, $ltiinstance->id, null, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); $gradeitems = $gbservice->get_lineitems($course->id, null, $ltiinstance->id + 1, null, null, null, $typeid); $this->assertEquals(0, $gradeitems[0]); } $gradeitems = $gbservice->get_lineitems($course->id, null, null, $tag, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); $gradeitems = $gbservice->get_lineitems($course->id, 'an unknown resource id', null, null, null, null, $typeid); $this->assertEquals(0, $gradeitems[0]); $gradeitems = $gbservice->get_lineitems($course->id, null, null, 'an unknown tag', null, null, $typeid); $this->assertEquals(0, $gradeitems[0]); } /** * Inserts a graded lti instance, which should create a grade_item and gradebookservices record. * * @param int $typeid Type ID of the LTI Tool. * @param object $course course where to add the lti instance. * @param string|null $resourceid resource id * @param string|null $tag tag * @param string|null $subreviewurl submission review url * @param string|null $subreviewparams submission review custom params * * @return object lti instance created */ private function create_graded_lti(int $typeid, object $course, ?string $resourceid, ?string $tag, ?string $subreviewurl = null, ?string $subreviewparams = null): object { $lti = ['course' => $course->id, 'typeid' => $typeid, 'instructorchoiceacceptgrades' => LTI_SETTING_ALWAYS, 'grade' => 10, 'lineitemresourceid' => $resourceid, 'lineitemtag' => $tag, 'lineitemsubreviewurl' => $subreviewurl, 'lineitemsubreviewparams' => $subreviewparams]; return $this->getDataGenerator()->create_module('lti', $lti, array()); } /** * Inserts an lti instance that is not graded. * * @param int $typeid Type Id of the LTI Tool. * @param object $course course where to add the lti instance. * * @return object lti instance created */ private function create_notgraded_lti(int $typeid, object $course): object { $lti = ['course' => $course->id, 'typeid' => $typeid, 'instructorchoiceacceptgrades' => LTI_SETTING_NEVER]; return $this->getDataGenerator()->create_module('lti', $lti, array()); } /** * Inserts a standalone lineitem (gradeitem, gradebookservices entries). * * @param int $courseid Id of the course where the standalone line item will be added. * @param int $typeid of the LTI Tool * @param string|null $resourceid resource id * @param string|null $tag tag * @param int|null $ltiinstanceid Id of the LTI instance the standalone line item will be related to. * */ private function create_standalone_lineitem(int $courseid, int $typeid, ?string $resourceid, ?string $tag, ?int $ltiinstanceid = null): void { $gbservice = new gradebookservices(); $gbservice->add_standalone_lineitem($courseid, "manualtest", 10, "https://test.phpunit", $ltiinstanceid, $resourceid, $tag, $typeid, null /*toolproxyid*/); } /** * Creates a new LTI Tool Type. */ private function create_type() { $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->clientid = "Test client ID"; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $config = new \stdClass(); $config->ltiservice_gradesynchronization = 2; return lti_add_type($type, $config); } }