+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \mod_lti\service\gradebookservices\local\resources\lineitem
+ */
+class lineitem_test extends \advanced_testcase {
+
+ /**
+ * @covers ::execute
+ *
+ * Test updating the line item with submission review.
+ */
+ public function test_execute_put_nosubreview() {
+ 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() {
+ 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() {
+ 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));
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/mod/lti/service/gradebookservices/upgrade.txt b/mod/lti/service/gradebookservices/upgrade.txt
new file mode 100644
index 00000000000..41e568471a1
--- /dev/null
+++ b/mod/lti/service/gradebookservices/upgrade.txt
@@ -0,0 +1,6 @@
+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).
diff --git a/mod/lti/service/gradebookservices/version.php b/mod/lti/service/gradebookservices/version.php
index b0360ed0f2f..8359579acce 100644
--- a/mod/lti/service/gradebookservices/version.php
+++ b/mod/lti/service/gradebookservices/version.php
@@ -25,6 +25,6 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2022041900;
+$plugin->version = 2022051900;
$plugin->requires = 2022041200;
$plugin->component = 'ltiservice_gradebookservices';
diff --git a/mod/lti/tests/locallib_test.php b/mod/lti/tests/locallib_test.php
index a9697fe9fb1..2561fe0f227 100644
--- a/mod/lti/tests/locallib_test.php
+++ b/mod/lti/tests/locallib_test.php
@@ -67,6 +67,19 @@ require_once($CFG->dirroot . '/mod/lti/tests/mod_lti_testcase.php');
*/
class locallib_test extends mod_lti_testcase {
+ /**
+ * @covers ::lti_split_parameters()
+ *
+ * Test the split parameters function
+ */
+ public function test_split_parameters() {
+ $this->assertEquals(lti_split_parameters(''), array());
+ $this->assertEquals(lti_split_parameters('a=1'), array('a' => '1'));
+ $this->assertEquals(lti_split_parameters("a=1\nb=2"), array('a' => '1', 'b' => '2'));
+ $this->assertEquals(lti_split_parameters("a=1\n\rb=2"), array('a' => '1', 'b' => '2'));
+ $this->assertEquals(lti_split_parameters("a=1\r\nb=2"), array('a' => '1', 'b' => '2'));
+ }
+
public function test_split_custom_parameters() {
$this->resetAfterTest();
@@ -618,6 +631,7 @@ class locallib_test extends mod_lti_testcase {
'basic-lti-launch-request' => 'LtiResourceLinkRequest',
'ContentItemSelectionRequest' => 'LtiDeepLinkingRequest',
'LtiDeepLinkingResponse' => 'ContentItemSelection',
+ 'LtiSubmissionReviewRequest' => 'LtiSubmissionReviewRequest'
];
$this->assertEquals($mapping, lti_get_jwt_message_type_mapping());
@@ -1005,6 +1019,12 @@ class locallib_test extends mod_lti_testcase {
'claim' => 'lis_result_sourcedid',
'isarray' => false
],
+ 'for_user_id' => [
+ 'suffix' => '',
+ 'group' => 'for_user',
+ 'claim' => 'user_id',
+ 'isarray' => false
+ ],
];
$this->assertEquals($mapping, lti_get_jwt_claim_mapping());
@@ -1339,6 +1359,99 @@ MwIDAQAB
$this->assertEquals($contentitems[0]['lineItem']['tag'], $config->lineitemtag);
$this->assertEquals($contentitems[0]['lineItem']['resourceId'], $config->lineitemresourceid);
$this->assertEquals($contentitems[0]['lineItem']['scoreMaximum'], $config->grade_modgrade_point);
+ $this->assertEquals('', $config->lineitemsubreviewurl);
+ $this->assertEquals('', $config->lineitemsubreviewparams);
+ }
+
+ /**
+ * @covers ::lti_tool_configuration_from_content_item()
+ *
+ * Test adding a single gradable item through content item with an empty subreview url.
+ */
+ public function test_lti_tool_configuration_from_content_item_single_gradable_subreview_default_emptyurl() {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ $type = new \stdClass();
+ $type->name = "Test tool";
+ $type->baseurl = "http://example.com";
+ $config = new \stdClass();
+ $config->lti_acceptgrades = LTI_SETTING_DELEGATE;
+ $typeid = lti_add_type($type, $config);
+
+ $contentitems = [];
+ $contentitems[] = [
+ 'type' => 'ltiResourceLink',
+ 'url' => 'http://example.com/messages/launch',
+ 'title' => 'Test title',
+ 'lineItem' => [
+ 'resourceId' => 'r12345',
+ 'tag' => 'final',
+ 'scoreMaximum' => 10.0,
+ 'submissionReview' => [
+ 'url' => ''
+ ]
+ ],
+ 'frame' => []
+ ];
+ $contentitemsjson13 = json_encode($contentitems);
+ $json11 = lti_convert_content_items($contentitemsjson13);
+
+ $config = lti_tool_configuration_from_content_item($typeid,
+ 'ContentItemSelection',
+ $type->ltiversion,
+ 'ConsumerKey',
+ $json11);
+
+ $this->assertEquals('DEFAULT', $config->lineitemsubreviewurl);
+ $this->assertEquals('', $config->lineitemsubreviewparams);
+ }
+
+ /**
+ * @covers ::lti_tool_configuration_from_content_item()
+ *
+ * Test adding a single gradable item through content item.
+ */
+ public function test_lti_tool_configuration_from_content_item_single_gradable_subreview_default() {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ $type = new \stdClass();
+ $type->name = "Test tool";
+ $type->baseurl = "http://example.com";
+ $config = new \stdClass();
+ $config->lti_acceptgrades = LTI_SETTING_DELEGATE;
+ $typeid = lti_add_type($type, $config);
+
+ $contentitems = [];
+ $contentitems[] = [
+ 'type' => 'ltiResourceLink',
+ 'url' => 'http://example.com/messages/launch',
+ 'title' => 'Test title',
+ 'lineItem' => [
+ 'resourceId' => 'r12345',
+ 'tag' => 'final',
+ 'scoreMaximum' => 10.0,
+ 'submissionReview' => []
+ ],
+ 'frame' => []
+ ];
+ $contentitemsjson13 = json_encode($contentitems);
+ $json11 = lti_convert_content_items($contentitemsjson13);
+
+ $config = lti_tool_configuration_from_content_item($typeid,
+ 'ContentItemSelection',
+ $type->ltiversion,
+ 'ConsumerKey',
+ $json11);
+
+ $this->assertEquals($contentitems[0]['url'], $config->toolurl);
+ $this->assertEquals(LTI_SETTING_ALWAYS, $config->instructorchoiceacceptgrades);
+ $this->assertEquals($contentitems[0]['lineItem']['tag'], $config->lineitemtag);
+ $this->assertEquals($contentitems[0]['lineItem']['resourceId'], $config->lineitemresourceid);
+ $this->assertEquals($contentitems[0]['lineItem']['scoreMaximum'], $config->grade_modgrade_point);
+ $this->assertEquals('DEFAULT', $config->lineitemsubreviewurl);
+ $this->assertEquals('', $config->lineitemsubreviewparams);
}
/**
@@ -1375,7 +1488,11 @@ MwIDAQAB
'lineItem' => [
'resourceId' => 'r12345',
'tag' => 'final',
- 'scoreMaximum' => 10.0
+ 'scoreMaximum' => 10.0,
+ 'submissionReview' => [
+ 'url' => 'https://testsub.url',
+ 'custom' => ['a' => 'b']
+ ]
],
'frame' => []
];
@@ -1397,6 +1514,8 @@ MwIDAQAB
$this->assertEquals($contentitems[1]['lineItem']['tag'], $config->multiple[1]->lineitemtag);
$this->assertEquals($contentitems[1]['lineItem']['resourceId'], $config->multiple[1]->lineitemresourceid);
$this->assertEquals($contentitems[1]['lineItem']['scoreMaximum'], $config->multiple[1]->grade_modgrade_point);
+ $this->assertEquals($contentitems[1]['lineItem']['submissionReview']['url'], $config->multiple[1]->lineitemsubreviewurl);
+ $this->assertEquals("a=b", $config->multiple[1]->lineitemsubreviewparams);
}
/**
@@ -1649,16 +1768,35 @@ MwIDAQAB
$config->typeid = 'some-type-id';
$config->lti_toolurl = 'some-lti-tool-url';
- $request = lti_build_login_request($course->id, $instance->id, $instance, $config, 'basic-lti-launch-request');
-
+ $request = lti_build_login_request($course->id, $instance->cmid, $instance, $config, 'basic-lti-launch-request');
$this->assertEquals($CFG->wwwroot, $request['iss']);
$this->assertEquals('http://some-lti-tool-url', $request['target_link_uri']);
$this->assertEquals(123456789, $request['login_hint']);
- $this->assertEquals($instance->id, $request['lti_message_hint']);
+ $this->assertTrue(strpos($request['lti_message_hint'], "\"cmid\":{$instance->cmid}") > 0);
+ $this->assertTrue(strpos($request['lti_message_hint'], "\"launchid\":\"ltilaunch{$instance->id}_") > 0);
$this->assertEquals('some-client-id', $request['client_id']);
$this->assertEquals('some-type-id', $request['lti_deployment_id']);
}
+ /**
+ * @covers ::lti_get_launch_data()
+ *
+ * Test for_user is passed as parameter when specified.
+ */
+ public function test_lti_get_launch_data_with_for_user() {
+ global $DB;
+ $this->resetAfterTest();
+ $this->setAdminUser();
+ $config = new \stdClass();
+ $config->lti_organizationid = '';
+ $course = $this->getDataGenerator()->create_course();
+ $type = $this->create_type($config);
+ $link = $this->create_instance($type, $course);
+ $launchdata = lti_get_launch_data($link, '', '', 345);
+ $this->assertEquals($launchdata[1]['lti_message_type'], 'basic-lti-launch-request');
+ $this->assertEquals($launchdata[1]['for_user_id'], 345);
+ }
+
/**
* Test default orgid is host if not specified in config (tool installed in earlier version of Moodle).
*/
diff --git a/mod/lti/upgrade.txt b/mod/lti/upgrade.txt
index 5fed05c84b1..503089d29f5 100644
--- a/mod/lti/upgrade.txt
+++ b/mod/lti/upgrade.txt
@@ -3,6 +3,7 @@ This files describes API changes in the lti code.
=== 4.1 ===
* The callback get_shortcuts() is now removed. Please use get_course_content_items and get_all_content_items instead.
+* Services can now implement the override_endpoint function to replace the launch url and/or the custom parameters.
=== 3.10 ===
diff --git a/mod/lti/view.php b/mod/lti/view.php
index c5ceebdd67d..7d7e942183f 100644
--- a/mod/lti/view.php
+++ b/mod/lti/view.php
@@ -53,6 +53,8 @@ require_once($CFG->dirroot.'/mod/lti/locallib.php');
$id = optional_param('id', 0, PARAM_INT); // Course Module ID, or
$l = optional_param('l', 0, PARAM_INT); // lti ID.
+$action = optional_param('action', '', PARAM_TEXT);
+$foruserid = optional_param('user', 0, PARAM_INT);
$forceview = optional_param('forceview', 0, PARAM_BOOL);
if ($l) { // Two ways to specify the module.
@@ -85,6 +87,10 @@ $PAGE->set_context($context);
require_login($course, true, $cm);
require_capability('mod/lti:view', $context);
+if (!empty($foruserid) && (int)$foruserid !== (int)$USER->id) {
+ require_capability('gradereport/grader:view', $context);
+}
+
$url = new moodle_url('/mod/lti/view.php', array('id' => $cm->id));
$PAGE->set_url($url);
@@ -126,29 +132,30 @@ if ($typeid) {
$config = new stdClass();
$config->lti_ltiversion = LTI_VERSION_1;
}
-
+$launchurl = new moodle_url('/mod/lti/launch.php', ['id' => $cm->id, 'triggerview' => 0]);
+if ($action) {
+ $launchurl->param('action', $action);;
+}
+if ($foruserid) {
+ $launchurl->param('user', $foruserid);;
+}
unset($SESSION->lti_initiatelogin_status);
-if ($launchcontainer == LTI_LAUNCH_CONTAINER_WINDOW) {
+if (($launchcontainer == LTI_LAUNCH_CONTAINER_WINDOW)) {
if (!$forceview) {
echo "\n";
echo "".get_string("basiclti_in_new_window", "lti")."
\n";
}
- $url = new moodle_url('/mod/lti/launch.php', array('id' => $cm->id));
echo html_writer::start_tag('p');
- echo html_writer::link($url, get_string("basiclti_in_new_window_open", "lti"), array('target' => '_blank'));
+ echo html_writer::link($launchurl->out(false), get_string("basiclti_in_new_window_open", "lti"), array('target' => '_blank'));
echo html_writer::end_tag('p');
} else {
$content = '';
- if ($config->lti_ltiversion === LTI_VERSION_1P3) {
- $content = lti_initiate_login($cm->course, $id, $lti, $config);
- }
-
- // Build the allowed URL, since we know what it will be from $toolurl.
- // If the specified URL is invalid, the iframe won't load, but we still want to avoid parse related errors here.
- // So we set an empty default allowed URL, and only build a real one if the parse is successful.
+ // Build the allowed URL, since we know what it will be from $lti->toolurl,
+ // If the specified toolurl is invalid the iframe won't load, but we still want to avoid parse related errors here.
+ // So we set an empty default allowed url, and only build a real one if the parse is successful.
$ltiallow = '';
$urlparts = parse_url($toolurl);
if ($urlparts && array_key_exists('scheme', $urlparts) && array_key_exists('host', $urlparts)) {
@@ -164,7 +171,7 @@ if ($launchcontainer == LTI_LAUNCH_CONTAINER_WINDOW) {
$attributes['id'] = "contentframe";
$attributes['height'] = '600px';
$attributes['width'] = '100%';
- $attributes['src'] = 'launch.php?id=' . $cm->id . '&triggerview=0';
+ $attributes['src'] = $launchurl;
$attributes['allow'] = "microphone $ltiallow; " .
"camera $ltiallow; " .
"geolocation $ltiallow; " .