diff --git a/lib/upgradelib.php b/lib/upgradelib.php index f131719cf28..c5703cfe0ce 100644 --- a/lib/upgradelib.php +++ b/lib/upgradelib.php @@ -527,7 +527,6 @@ function upgrade_stale_php_files_present(): bool { '/mod/forum/pix/icon.gif', '/tag/templates/tagname.mustache', // Removed in 3.0. - '/mod/lti/grade.php', '/tag/coursetagslib.php', // Removed in 2.9. '/lib/timezone.txt', diff --git a/mod/lti/amd/build/contentitem.min.js b/mod/lti/amd/build/contentitem.min.js index fe243d2e5d4..e18ce8fb895 100644 --- a/mod/lti/amd/build/contentitem.min.js +++ b/mod/lti/amd/build/contentitem.min.js @@ -9,6 +9,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.2 */ -define("mod_lti/contentitem",["jquery","core/notification","core/str","core/templates","mod_lti/form-field","core/modal_factory","core/modal_events"],(function($,notification,str,templates,FormField,ModalFactory,ModalEvents){var dialogue,doneCallback,contentItem={init:function(url,postData,cb){doneCallback=cb;var context={url:url,postData:postData},bodyPromise=templates.render("mod_lti/contentitem",context);if(dialogue)return dialogue.setBody(bodyPromise),void dialogue.show();str.get_string("selectcontent","lti").then((function(title){return ModalFactory.create({title:title,body:bodyPromise,large:!0})})).then((function(modal){dialogue=modal,modal.getRoot().on(ModalEvents.hidden,(function(){modal.setBody(""),notification.fetchNotifications()})),modal.show()})).catch(notification.exception)}},ltiFormFields=[new FormField("name",FormField.TYPES.TEXT,!1,""),new FormField("introeditor",FormField.TYPES.EDITOR,!1,""),new FormField("toolurl",FormField.TYPES.TEXT,!0,""),new FormField("securetoolurl",FormField.TYPES.TEXT,!0,""),new FormField("instructorchoiceacceptgrades",FormField.TYPES.CHECKBOX,!0,!0),new FormField("instructorchoicesendname",FormField.TYPES.CHECKBOX,!0,!0),new FormField("instructorchoicesendemailaddr",FormField.TYPES.CHECKBOX,!0,!0),new FormField("instructorcustomparameters",FormField.TYPES.TEXT,!0,""),new FormField("icon",FormField.TYPES.TEXT,!0,""),new FormField("secureicon",FormField.TYPES.TEXT,!0,""),new FormField("launchcontainer",FormField.TYPES.SELECT,!0,0),new FormField("grade_modgrade_point",FormField.TYPES.TEXT,!1,""),new FormField("lineitemresourceid",FormField.TYPES.TEXT,!0,""),new FormField("lineitemtag",FormField.TYPES.TEXT,!0,"")];const hideElement=e=>{e.setAttribute("hidden","true"),e.setAttribute("aria-hidden","true"),e.setAttribute("tab-index","-1")},showElement=e=>{e.removeAttribute("hidden"),e.setAttribute("aria-hidden","false"),e.setAttribute("tab-index","1")};return window.processContentItemReturnData=function(returnData){var index;if(dialogue&&dialogue.hide(),returnData.multiple){for(index in ltiFormFields)ltiFormFields[index].setFieldValue("name"===ltiFormFields[index].name?"item":null);var variants=[];returnData.multiple.forEach((function(v){variants.push((config=>{const variant={};return["name","toolurl","securetoolurl","instructorcustomparameters","icon","secureicon","launchcontainer","lineitemresourceid","lineitemtag"].forEach((function(name){variant[name]=config[name]||""})),variant["introeditor[text]"]=config.introeditor?config.introeditor.text:"",variant["introeditor[format]"]=config.introeditor?config.introeditor.format:"",1===config.instructorchoiceacceptgrades?(variant.instructorchoiceacceptgrades="1",variant["grade[modgrade_point]"]=config.grade_modgrade_point||"100"):variant.instructorchoiceacceptgrades="0",variant})(v))})),async function(items){const form=document.querySelector("#region-main-box form"),toolArea=form.querySelector('[data-attribute="dynamic-import"]'),buttonGroup=form.querySelector("#fgroup_id_buttonar"),submitAndLaunch=form.querySelector("#id_submitbutton");Array.from(form.children).forEach(hideElement),hideElement(submitAndLaunch);const{html:html,js:js}=await templates.renderForPromise("mod_lti/tool_deeplinking_results",{items:items});await templates.replaceNodeContents(toolArea,html,js),showElement(toolArea),showElement(buttonGroup)}(returnData.multiple);const submitAndCourse=document.querySelector("#id_submitbutton2");submitAndCourse.onclick=e=>{e.preventDefault(),submitAndCourse.disabled=!0;const fd=new FormData(document.querySelector("#region-main-box form")),backToCourse=()=>{document.querySelector("#id_cancel").click()};variants.reduce(((promise,variant)=>{Object.entries(variant).forEach((entry=>fd.set(entry[0],entry[1])));const body=new URLSearchParams(fd),doPost=()=>fetch(document.location.pathname,{method:"post",body:body});return promise.then(doPost).catch(doPost)}),Promise.resolve()).then(backToCourse).catch(backToCourse)}}else{for(index in ltiFormFields){var field=ltiFormFields[index],value=null;void 0!==returnData[field.name]&&(value=returnData[field.name]),field.setFieldValue(value)}field.setFieldValue(value)}doneCallback&&doneCallback(returnData)},contentItem})); +define("mod_lti/contentitem",["jquery","core/notification","core/str","core/templates","mod_lti/form-field","core/modal_factory","core/modal_events"],(function($,notification,str,templates,FormField,ModalFactory,ModalEvents){var dialogue,doneCallback,contentItem={init:function(url,postData,cb){doneCallback=cb;var context={url:url,postData:postData},bodyPromise=templates.render("mod_lti/contentitem",context);if(dialogue)return dialogue.setBody(bodyPromise),void dialogue.show();str.get_string("selectcontent","lti").then((function(title){return ModalFactory.create({title:title,body:bodyPromise,large:!0})})).then((function(modal){dialogue=modal,modal.getRoot().on(ModalEvents.hidden,(function(){modal.setBody(""),notification.fetchNotifications()})),modal.show()})).catch(notification.exception)}},ltiFormFields=[new FormField("name",FormField.TYPES.TEXT,!1,""),new FormField("introeditor",FormField.TYPES.EDITOR,!1,""),new FormField("toolurl",FormField.TYPES.TEXT,!0,""),new FormField("securetoolurl",FormField.TYPES.TEXT,!0,""),new FormField("instructorchoiceacceptgrades",FormField.TYPES.CHECKBOX,!0,!0),new FormField("instructorchoicesendname",FormField.TYPES.CHECKBOX,!0,!0),new FormField("instructorchoicesendemailaddr",FormField.TYPES.CHECKBOX,!0,!0),new FormField("instructorcustomparameters",FormField.TYPES.TEXT,!0,""),new FormField("icon",FormField.TYPES.TEXT,!0,""),new FormField("secureicon",FormField.TYPES.TEXT,!0,""),new FormField("launchcontainer",FormField.TYPES.SELECT,!0,0),new FormField("grade_modgrade_point",FormField.TYPES.TEXT,!1,""),new FormField("lineitemresourceid",FormField.TYPES.TEXT,!0,""),new FormField("lineitemtag",FormField.TYPES.TEXT,!0,""),new FormField("lineitemsubreviewurl",FormField.TYPES.TEXT,!0,""),new FormField("lineitemsubreviewparams",FormField.TYPES.TEXT,!0,"")];const hideElement=e=>{e.setAttribute("hidden","true"),e.setAttribute("aria-hidden","true"),e.setAttribute("tab-index","-1")},showElement=e=>{e.removeAttribute("hidden"),e.setAttribute("aria-hidden","false"),e.setAttribute("tab-index","1")};return window.processContentItemReturnData=function(returnData){var index;if(dialogue&&dialogue.hide(),returnData.multiple){for(index in ltiFormFields)ltiFormFields[index].setFieldValue("name"===ltiFormFields[index].name?"item":null);var variants=[];returnData.multiple.forEach((function(v){variants.push((config=>{const variant={};return["name","toolurl","securetoolurl","instructorcustomparameters","icon","secureicon","launchcontainer","lineitemresourceid","lineitemtag","lineitemsubreviewurl","lineitemsubreviewparams"].forEach((function(name){variant[name]=config[name]||""})),variant["introeditor[text]"]=config.introeditor?config.introeditor.text:"",variant["introeditor[format]"]=config.introeditor?config.introeditor.format:"",1===config.instructorchoiceacceptgrades?(variant.instructorchoiceacceptgrades="1",variant["grade[modgrade_point]"]=config.grade_modgrade_point||"100"):variant.instructorchoiceacceptgrades="0",variant})(v))})),async function(items){const form=document.querySelector("#region-main-box form"),toolArea=form.querySelector('[data-attribute="dynamic-import"]'),buttonGroup=form.querySelector("#fgroup_id_buttonar"),submitAndLaunch=form.querySelector("#id_submitbutton");Array.from(form.children).forEach(hideElement),hideElement(submitAndLaunch);const{html:html,js:js}=await templates.renderForPromise("mod_lti/tool_deeplinking_results",{items:items});await templates.replaceNodeContents(toolArea,html,js),showElement(toolArea),showElement(buttonGroup)}(returnData.multiple);const submitAndCourse=document.querySelector("#id_submitbutton2");submitAndCourse.onclick=e=>{e.preventDefault(),submitAndCourse.disabled=!0;const fd=new FormData(document.querySelector("#region-main-box form")),backToCourse=()=>{document.querySelector("#id_cancel").click()};variants.reduce(((promise,variant)=>{Object.entries(variant).forEach((entry=>fd.set(entry[0],entry[1])));const body=new URLSearchParams(fd),doPost=()=>fetch(document.location.pathname,{method:"post",body:body});return promise.then(doPost).catch(doPost)}),Promise.resolve()).then(backToCourse).catch(backToCourse)}}else{for(index in ltiFormFields){var field=ltiFormFields[index],value=null;void 0!==returnData[field.name]&&(value=returnData[field.name]),field.setFieldValue(value)}field.setFieldValue(value)}doneCallback&&doneCallback(returnData)},contentItem})); //# sourceMappingURL=contentitem.min.js.map \ No newline at end of file diff --git a/mod/lti/amd/build/contentitem.min.js.map b/mod/lti/amd/build/contentitem.min.js.map index 08f4420dba0..7350a480172 100644 --- a/mod/lti/amd/build/contentitem.min.js.map +++ b/mod/lti/amd/build/contentitem.min.js.map @@ -1 +1 @@ -{"version":3,"file":"contentitem.min.js","sources":["../src/contentitem.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Launches the modal dialogue that contains the iframe that sends the Content-Item selection request to an\n * LTI tool provider that supports Content-Item type message.\n *\n * See template: mod_lti/contentitem\n *\n * @module mod_lti/contentitem\n * @copyright 2016 Jun Pataleta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.2\n */\ndefine(\n [\n 'jquery',\n 'core/notification',\n 'core/str',\n 'core/templates',\n 'mod_lti/form-field',\n 'core/modal_factory',\n 'core/modal_events'\n ],\n function($, notification, str, templates, FormField, ModalFactory, ModalEvents) {\n var dialogue;\n var doneCallback;\n var contentItem = {\n /**\n * Init function.\n *\n * @param {string} url The URL for the content item selection.\n * @param {object} postData The data to be sent for the content item selection request.\n * @param {Function} cb The callback to run once the content item has been processed.\n */\n init: function(url, postData, cb) {\n doneCallback = cb;\n var context = {\n url: url,\n postData: postData\n };\n var bodyPromise = templates.render('mod_lti/contentitem', context);\n\n if (dialogue) {\n // Set dialogue body.\n dialogue.setBody(bodyPromise);\n // Display the dialogue.\n dialogue.show();\n return;\n }\n\n str.get_string('selectcontent', 'lti').then(function(title) {\n return ModalFactory.create({\n title: title,\n body: bodyPromise,\n large: true\n });\n }).then(function(modal) {\n dialogue = modal;\n // On hide handler.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Empty modal contents when it's hidden.\n modal.setBody('');\n\n // Fetch notifications.\n notification.fetchNotifications();\n });\n\n // Display the dialogue.\n modal.show();\n return;\n }).catch(notification.exception);\n }\n };\n\n /**\n * Array of form fields for LTI tool configuration.\n */\n var ltiFormFields = [\n new FormField('name', FormField.TYPES.TEXT, false, ''),\n new FormField('introeditor', FormField.TYPES.EDITOR, false, ''),\n new FormField('toolurl', FormField.TYPES.TEXT, true, ''),\n new FormField('securetoolurl', FormField.TYPES.TEXT, true, ''),\n new FormField('instructorchoiceacceptgrades', FormField.TYPES.CHECKBOX, true, true),\n new FormField('instructorchoicesendname', FormField.TYPES.CHECKBOX, true, true),\n new FormField('instructorchoicesendemailaddr', FormField.TYPES.CHECKBOX, true, true),\n new FormField('instructorcustomparameters', FormField.TYPES.TEXT, true, ''),\n new FormField('icon', FormField.TYPES.TEXT, true, ''),\n new FormField('secureicon', FormField.TYPES.TEXT, true, ''),\n new FormField('launchcontainer', FormField.TYPES.SELECT, true, 0),\n new FormField('grade_modgrade_point', FormField.TYPES.TEXT, false, ''),\n new FormField('lineitemresourceid', FormField.TYPES.TEXT, true, ''),\n new FormField('lineitemtag', FormField.TYPES.TEXT, true, '')\n ];\n\n /**\n * Hide the element, including aria and tab index.\n * @param {HTMLElement} e the element to be hidden.\n */\n const hideElement = (e) => {\n e.setAttribute('hidden', 'true');\n e.setAttribute('aria-hidden', 'true');\n e.setAttribute('tab-index', '-1');\n };\n\n /**\n * Show the element, including aria and tab index (set to 1).\n * @param {HTMLElement} e the element to be shown.\n */\n const showElement = (e) => {\n e.removeAttribute('hidden');\n e.setAttribute('aria-hidden', 'false');\n e.setAttribute('tab-index', '1');\n };\n\n /**\n * When more than one item needs to be added, the UI is simplified\n * to just list the items to be added. Form is hidden and the only\n * options is (save and return to course) or cancel.\n * This function injects the summary to the form page, and hides\n * the unneeded elements.\n * @param {Object[]} items items to be added to the course.\n */\n const showMultipleSummaryAndHideForm = async function(items) {\n const form = document.querySelector('#region-main-box form');\n const toolArea = form.querySelector('[data-attribute=\"dynamic-import\"]');\n const buttonGroup = form.querySelector('#fgroup_id_buttonar');\n const submitAndLaunch = form.querySelector('#id_submitbutton');\n Array.from(form.children).forEach(hideElement);\n hideElement(submitAndLaunch);\n const {html, js} = await templates.renderForPromise('mod_lti/tool_deeplinking_results',\n {items: items});\n\n await templates.replaceNodeContents(toolArea, html, js);\n showElement(toolArea);\n showElement(buttonGroup);\n };\n\n /**\n * Transforms config values aimed at populating the lti mod form to JSON variant\n * which are used to insert more than one activity modules in one submit\n * by applying variation to the submitted form.\n * See /course/modedit.php.\n * @private\n * @param {Object} config transforms a config to an actual form data to be posted.\n * @return {Object} variant that will be used to modify form values on submit.\n */\n var configToVariant = (config) => {\n const variant = {};\n ['name', 'toolurl', 'securetoolurl', 'instructorcustomparameters', 'icon', 'secureicon',\n 'launchcontainer', 'lineitemresourceid', 'lineitemtag'].forEach(\n function(name) {\n variant[name] = config[name] || '';\n }\n );\n variant['introeditor[text]'] = config.introeditor ? config.introeditor.text : '';\n variant['introeditor[format]'] = config.introeditor ? config.introeditor.format : '';\n if (config.instructorchoiceacceptgrades === 1) {\n variant.instructorchoiceacceptgrades = '1';\n variant['grade[modgrade_point]'] = config.grade_modgrade_point || '100';\n } else {\n variant.instructorchoiceacceptgrades = '0';\n }\n return variant;\n };\n\n /**\n * Window function that can be called from mod_lti/contentitem_return to close the dialogue and process the return data.\n * If the return data contains more than one item, the form will not be populated with item data\n * but rather hidden, and the item data will be added to a single input field used to create multiple\n * instances in one request.\n *\n * @param {object} returnData The fetched configuration data from the Content-Item selection dialogue.\n */\n window.processContentItemReturnData = function(returnData) {\n if (dialogue) {\n dialogue.hide();\n }\n var index;\n if (returnData.multiple) {\n for (index in ltiFormFields) {\n // Name is required, so putting a placeholder as it will not be used\n // in multi-items add.\n ltiFormFields[index].setFieldValue(ltiFormFields[index].name === 'name' ? 'item' : null);\n }\n var variants = [];\n returnData.multiple.forEach(function(v) {\n variants.push(configToVariant(v));\n });\n showMultipleSummaryAndHideForm(returnData.multiple);\n const submitAndCourse = document.querySelector('#id_submitbutton2');\n submitAndCourse.onclick = (e) => {\n e.preventDefault();\n submitAndCourse.disabled = true;\n const fd = new FormData(document.querySelector('#region-main-box form'));\n const postVariant = (promise, variant) => {\n Object.entries(variant).forEach((entry) => fd.set(entry[0], entry[1]));\n const body = new URLSearchParams(fd);\n const doPost = () => fetch(document.location.pathname, {method: 'post', body});\n return promise.then(doPost).catch(doPost);\n };\n const backToCourse = () => {\n document.querySelector(\"#id_cancel\").click();\n };\n variants.reduce(postVariant, Promise.resolve()).then(backToCourse).catch(backToCourse);\n };\n } else {\n // Populate LTI configuration fields from return data.\n for (index in ltiFormFields) {\n var field = ltiFormFields[index];\n var value = null;\n if (typeof returnData[field.name] !== 'undefined') {\n value = returnData[field.name];\n }\n field.setFieldValue(value);\n }\n field.setFieldValue(value);\n }\n\n if (doneCallback) {\n doneCallback(returnData);\n }\n };\n\n return contentItem;\n }\n);\n"],"names":["define","$","notification","str","templates","FormField","ModalFactory","ModalEvents","dialogue","doneCallback","contentItem","init","url","postData","cb","context","bodyPromise","render","setBody","show","get_string","then","title","create","body","large","modal","getRoot","on","hidden","fetchNotifications","catch","exception","ltiFormFields","TYPES","TEXT","EDITOR","CHECKBOX","SELECT","hideElement","e","setAttribute","showElement","removeAttribute","window","processContentItemReturnData","returnData","index","hide","multiple","setFieldValue","name","variants","forEach","v","push","config","variant","introeditor","text","format","instructorchoiceacceptgrades","grade_modgrade_point","configToVariant","async","items","form","document","querySelector","toolArea","buttonGroup","submitAndLaunch","Array","from","children","html","js","renderForPromise","replaceNodeContents","showMultipleSummaryAndHideForm","submitAndCourse","onclick","preventDefault","disabled","fd","FormData","backToCourse","click","reduce","promise","Object","entries","entry","set","URLSearchParams","doPost","fetch","location","pathname","method","Promise","resolve","field","value"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"contentitem.min.js","sources":["../src/contentitem.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Launches the modal dialogue that contains the iframe that sends the Content-Item selection request to an\n * LTI tool provider that supports Content-Item type message.\n *\n * See template: mod_lti/contentitem\n *\n * @module mod_lti/contentitem\n * @copyright 2016 Jun Pataleta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.2\n */\ndefine(\n [\n 'jquery',\n 'core/notification',\n 'core/str',\n 'core/templates',\n 'mod_lti/form-field',\n 'core/modal_factory',\n 'core/modal_events'\n ],\n function($, notification, str, templates, FormField, ModalFactory, ModalEvents) {\n var dialogue;\n var doneCallback;\n var contentItem = {\n /**\n * Init function.\n *\n * @param {string} url The URL for the content item selection.\n * @param {object} postData The data to be sent for the content item selection request.\n * @param {Function} cb The callback to run once the content item has been processed.\n */\n init: function(url, postData, cb) {\n doneCallback = cb;\n var context = {\n url: url,\n postData: postData\n };\n var bodyPromise = templates.render('mod_lti/contentitem', context);\n\n if (dialogue) {\n // Set dialogue body.\n dialogue.setBody(bodyPromise);\n // Display the dialogue.\n dialogue.show();\n return;\n }\n\n str.get_string('selectcontent', 'lti').then(function(title) {\n return ModalFactory.create({\n title: title,\n body: bodyPromise,\n large: true\n });\n }).then(function(modal) {\n dialogue = modal;\n // On hide handler.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Empty modal contents when it's hidden.\n modal.setBody('');\n\n // Fetch notifications.\n notification.fetchNotifications();\n });\n\n // Display the dialogue.\n modal.show();\n return;\n }).catch(notification.exception);\n }\n };\n\n /**\n * Array of form fields for LTI tool configuration.\n */\n var ltiFormFields = [\n new FormField('name', FormField.TYPES.TEXT, false, ''),\n new FormField('introeditor', FormField.TYPES.EDITOR, false, ''),\n new FormField('toolurl', FormField.TYPES.TEXT, true, ''),\n new FormField('securetoolurl', FormField.TYPES.TEXT, true, ''),\n new FormField('instructorchoiceacceptgrades', FormField.TYPES.CHECKBOX, true, true),\n new FormField('instructorchoicesendname', FormField.TYPES.CHECKBOX, true, true),\n new FormField('instructorchoicesendemailaddr', FormField.TYPES.CHECKBOX, true, true),\n new FormField('instructorcustomparameters', FormField.TYPES.TEXT, true, ''),\n new FormField('icon', FormField.TYPES.TEXT, true, ''),\n new FormField('secureicon', FormField.TYPES.TEXT, true, ''),\n new FormField('launchcontainer', FormField.TYPES.SELECT, true, 0),\n new FormField('grade_modgrade_point', FormField.TYPES.TEXT, false, ''),\n new FormField('lineitemresourceid', FormField.TYPES.TEXT, true, ''),\n new FormField('lineitemtag', FormField.TYPES.TEXT, true, ''),\n new FormField('lineitemsubreviewurl', FormField.TYPES.TEXT, true, ''),\n new FormField('lineitemsubreviewparams', FormField.TYPES.TEXT, true, '')\n ];\n\n /**\n * Hide the element, including aria and tab index.\n * @param {HTMLElement} e the element to be hidden.\n */\n const hideElement = (e) => {\n e.setAttribute('hidden', 'true');\n e.setAttribute('aria-hidden', 'true');\n e.setAttribute('tab-index', '-1');\n };\n\n /**\n * Show the element, including aria and tab index (set to 1).\n * @param {HTMLElement} e the element to be shown.\n */\n const showElement = (e) => {\n e.removeAttribute('hidden');\n e.setAttribute('aria-hidden', 'false');\n e.setAttribute('tab-index', '1');\n };\n\n /**\n * When more than one item needs to be added, the UI is simplified\n * to just list the items to be added. Form is hidden and the only\n * options is (save and return to course) or cancel.\n * This function injects the summary to the form page, and hides\n * the unneeded elements.\n * @param {Object[]} items items to be added to the course.\n */\n const showMultipleSummaryAndHideForm = async function(items) {\n const form = document.querySelector('#region-main-box form');\n const toolArea = form.querySelector('[data-attribute=\"dynamic-import\"]');\n const buttonGroup = form.querySelector('#fgroup_id_buttonar');\n const submitAndLaunch = form.querySelector('#id_submitbutton');\n Array.from(form.children).forEach(hideElement);\n hideElement(submitAndLaunch);\n const {html, js} = await templates.renderForPromise('mod_lti/tool_deeplinking_results',\n {items: items});\n\n await templates.replaceNodeContents(toolArea, html, js);\n showElement(toolArea);\n showElement(buttonGroup);\n };\n\n /**\n * Transforms config values aimed at populating the lti mod form to JSON variant\n * which are used to insert more than one activity modules in one submit\n * by applying variation to the submitted form.\n * See /course/modedit.php.\n * @private\n * @param {Object} config transforms a config to an actual form data to be posted.\n * @return {Object} variant that will be used to modify form values on submit.\n */\n var configToVariant = (config) => {\n const variant = {};\n ['name', 'toolurl', 'securetoolurl', 'instructorcustomparameters', 'icon', 'secureicon',\n 'launchcontainer', 'lineitemresourceid', 'lineitemtag', 'lineitemsubreviewurl',\n 'lineitemsubreviewparams'].forEach(\n function(name) {\n variant[name] = config[name] || '';\n }\n );\n variant['introeditor[text]'] = config.introeditor ? config.introeditor.text : '';\n variant['introeditor[format]'] = config.introeditor ? config.introeditor.format : '';\n if (config.instructorchoiceacceptgrades === 1) {\n variant.instructorchoiceacceptgrades = '1';\n variant['grade[modgrade_point]'] = config.grade_modgrade_point || '100';\n } else {\n variant.instructorchoiceacceptgrades = '0';\n }\n return variant;\n };\n\n /**\n * Window function that can be called from mod_lti/contentitem_return to close the dialogue and process the return data.\n * If the return data contains more than one item, the form will not be populated with item data\n * but rather hidden, and the item data will be added to a single input field used to create multiple\n * instances in one request.\n *\n * @param {object} returnData The fetched configuration data from the Content-Item selection dialogue.\n */\n window.processContentItemReturnData = function(returnData) {\n if (dialogue) {\n dialogue.hide();\n }\n var index;\n if (returnData.multiple) {\n for (index in ltiFormFields) {\n // Name is required, so putting a placeholder as it will not be used\n // in multi-items add.\n ltiFormFields[index].setFieldValue(ltiFormFields[index].name === 'name' ? 'item' : null);\n }\n var variants = [];\n returnData.multiple.forEach(function(v) {\n variants.push(configToVariant(v));\n });\n showMultipleSummaryAndHideForm(returnData.multiple);\n const submitAndCourse = document.querySelector('#id_submitbutton2');\n submitAndCourse.onclick = (e) => {\n e.preventDefault();\n submitAndCourse.disabled = true;\n const fd = new FormData(document.querySelector('#region-main-box form'));\n const postVariant = (promise, variant) => {\n Object.entries(variant).forEach((entry) => fd.set(entry[0], entry[1]));\n const body = new URLSearchParams(fd);\n const doPost = () => fetch(document.location.pathname, {method: 'post', body});\n return promise.then(doPost).catch(doPost);\n };\n const backToCourse = () => {\n document.querySelector(\"#id_cancel\").click();\n };\n variants.reduce(postVariant, Promise.resolve()).then(backToCourse).catch(backToCourse);\n };\n } else {\n // Populate LTI configuration fields from return data.\n for (index in ltiFormFields) {\n var field = ltiFormFields[index];\n var value = null;\n if (typeof returnData[field.name] !== 'undefined') {\n value = returnData[field.name];\n }\n field.setFieldValue(value);\n }\n field.setFieldValue(value);\n }\n\n if (doneCallback) {\n doneCallback(returnData);\n }\n };\n\n return contentItem;\n }\n);\n"],"names":["define","$","notification","str","templates","FormField","ModalFactory","ModalEvents","dialogue","doneCallback","contentItem","init","url","postData","cb","context","bodyPromise","render","setBody","show","get_string","then","title","create","body","large","modal","getRoot","on","hidden","fetchNotifications","catch","exception","ltiFormFields","TYPES","TEXT","EDITOR","CHECKBOX","SELECT","hideElement","e","setAttribute","showElement","removeAttribute","window","processContentItemReturnData","returnData","index","hide","multiple","setFieldValue","name","variants","forEach","v","push","config","variant","introeditor","text","format","instructorchoiceacceptgrades","grade_modgrade_point","configToVariant","async","items","form","document","querySelector","toolArea","buttonGroup","submitAndLaunch","Array","from","children","html","js","renderForPromise","replaceNodeContents","showMultipleSummaryAndHideForm","submitAndCourse","onclick","preventDefault","disabled","fd","FormData","backToCourse","click","reduce","promise","Object","entries","entry","set","URLSearchParams","doPost","fetch","location","pathname","method","Promise","resolve","field","value"],"mappings":"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"} \ No newline at end of file diff --git a/mod/lti/amd/src/contentitem.js b/mod/lti/amd/src/contentitem.js index dd33c0b9b82..5c0869a5bb9 100644 --- a/mod/lti/amd/src/contentitem.js +++ b/mod/lti/amd/src/contentitem.js @@ -102,7 +102,9 @@ define( new FormField('launchcontainer', FormField.TYPES.SELECT, true, 0), new FormField('grade_modgrade_point', FormField.TYPES.TEXT, false, ''), new FormField('lineitemresourceid', FormField.TYPES.TEXT, true, ''), - new FormField('lineitemtag', FormField.TYPES.TEXT, true, '') + new FormField('lineitemtag', FormField.TYPES.TEXT, true, ''), + new FormField('lineitemsubreviewurl', FormField.TYPES.TEXT, true, ''), + new FormField('lineitemsubreviewparams', FormField.TYPES.TEXT, true, '') ]; /** @@ -160,7 +162,8 @@ define( var configToVariant = (config) => { const variant = {}; ['name', 'toolurl', 'securetoolurl', 'instructorcustomparameters', 'icon', 'secureicon', - 'launchcontainer', 'lineitemresourceid', 'lineitemtag'].forEach( + 'launchcontainer', 'lineitemresourceid', 'lineitemtag', 'lineitemsubreviewurl', + 'lineitemsubreviewparams'].forEach( function(name) { variant[name] = config[name] || ''; } diff --git a/mod/lti/auth.php b/mod/lti/auth.php index 830561a99f8..0971be31335 100644 --- a/mod/lti/auth.php +++ b/mod/lti/auth.php @@ -43,7 +43,7 @@ $responsetype = optional_param('response_type', '', PARAM_TEXT); $clientid = optional_param('client_id', '', PARAM_TEXT); $redirecturi = optional_param('redirect_uri', '', PARAM_URL); $loginhint = optional_param('login_hint', '', PARAM_TEXT); -$ltimessagehint = optional_param('lti_message_hint', 0, PARAM_INT); +$ltimessagehintenc = optional_param('lti_message_hint', '', PARAM_TEXT); $state = optional_param('state', '', PARAM_TEXT); $responsemode = optional_param('response_mode', '', PARAM_TEXT); $nonce = optional_param('nonce', '', PARAM_TEXT); @@ -51,11 +51,17 @@ $prompt = optional_param('prompt', '', PARAM_TEXT); $ok = !empty($scope) && !empty($responsetype) && !empty($clientid) && !empty($redirecturi) && !empty($loginhint) && - !empty($nonce) && !empty($SESSION->lti_message_hint); + !empty($nonce); if (!$ok) { $error = 'invalid_request'; } +$ltimessagehint = json_decode($ltimessagehintenc); +$ok = $ok && isset($ltimessagehint->launchid); +if (!$ok) { + $error = 'invalid_request'; + $desc = 'No launch id in LTI hint'; +} if ($ok && ($scope !== 'openid')) { $ok = false; $error = 'invalid_scope'; @@ -65,16 +71,13 @@ if ($ok && ($responsetype !== 'id_token')) { $error = 'unsupported_response_type'; } if ($ok) { - list($courseid, $typeid, $id, $titleb64, $textb64) = explode(',', $SESSION->lti_message_hint, 5); - $ok = ($id !== $ltimessagehint); + $launchid = $ltimessagehint->launchid; + list($courseid, $typeid, $id, $messagetype, $foruserid, $titleb64, $textb64) = explode(',', $SESSION->$launchid, 7); + unset($SESSION->$launchid); + $config = lti_get_type_type_config($typeid); + $ok = ($clientid === $config->lti_clientid); if (!$ok) { - $error = 'invalid_request'; - } else { - $config = lti_get_type_type_config($typeid); - $ok = ($clientid === $config->lti_clientid); - if (!$ok) { - $error = 'unauthorized_client'; - } + $error = 'unauthorized_client'; } } if ($ok && ($loginhint !== $USER->id)) { @@ -119,7 +122,7 @@ if ($ok) { require_capability('mod/lti:view', $context); $lti = $DB->get_record('lti', array('id' => $cm->instance), '*', MUST_EXIST); $lti->cmid = $cm->id; - list($endpoint, $params) = lti_get_launch_data($lti, $nonce); + list($endpoint, $params) = lti_get_launch_data($lti, $nonce, $messagetype, $foruserid); } else { require_login($course); $context = context_course::instance($courseid); diff --git a/mod/lti/classes/local/ltiservice/response.php b/mod/lti/classes/local/ltiservice/response.php index a36417e6bf3..da92eebb133 100644 --- a/mod/lti/classes/local/ltiservice/response.php +++ b/mod/lti/classes/local/ltiservice/response.php @@ -200,6 +200,15 @@ class response { $this->data = $data; } + /** + * Get the response body. + * + * @return string + */ + public function get_body() { + return $this->body; + } + /** * Set the response body. * diff --git a/mod/lti/classes/local/ltiservice/service_base.php b/mod/lti/classes/local/ltiservice/service_base.php index 234b558f1da..9f45235c164 100644 --- a/mod/lti/classes/local/ltiservice/service_base.php +++ b/mod/lti/classes/local/ltiservice/service_base.php @@ -255,6 +255,23 @@ abstract class service_base { } + /** + * Called when the launch data is created, offering a possibility to alter the + * target link URI. + * + * @param string $messagetype message type for this launch + * @param string $targetlinkuri current target link uri + * @param null|string $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 { + return [$targetlinkuri, $customstr]; + } + /** * Called when a new LTI Instance is deleted. * diff --git a/mod/lti/grade.php b/mod/lti/grade.php new file mode 100644 index 00000000000..01ca8353e8e --- /dev/null +++ b/mod/lti/grade.php @@ -0,0 +1,36 @@ +. + +/** + * This page is the grade hook allowing LTI launches from gradebook. + * + * @package mod_lti + * @category grade + * @copyright 2022 Cengage Group {@link https://cengage.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../config.php'); + +$id = required_param('id', PARAM_INT); +$userid = optional_param('userid', 0, PARAM_INT); + +$cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +require_login($course, false, $cm); + +redirect(new moodle_url('/mod/lti/view.php', array( + 'id' => $cm->id, 'action' => 'gradeReport', 'user' => $userid))); diff --git a/mod/lti/launch.php b/mod/lti/launch.php index 3ae04408865..5b55e9e70f6 100644 --- a/mod/lti/launch.php +++ b/mod/lti/launch.php @@ -50,10 +50,12 @@ require_once("../../config.php"); require_once($CFG->dirroot.'/mod/lti/lib.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); -$id = required_param('id', PARAM_INT); // Course Module ID. +$cmid = required_param('id', PARAM_INT); // Course Module ID. $triggerview = optional_param('triggerview', 1, PARAM_BOOL); +$action = optional_param('action', '', PARAM_TEXT); +$foruserid = optional_param('user', 0, PARAM_INT); -$cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); +$cm = get_coursemodule_from_id('lti', $cmid, 0, false, MUST_EXIST); $lti = $DB->get_record('lti', array('id' => $cm->instance), '*', MUST_EXIST); $typeid = $lti->typeid; @@ -64,7 +66,11 @@ if ($typeid) { $config = lti_get_type_type_config($typeid); if ($config->lti_ltiversion === LTI_VERSION_1P3) { if (!isset($SESSION->lti_initiatelogin_status)) { - echo lti_initiate_login($cm->course, $id, $lti, $config); + $msgtype = 'basic-lti-launch-request'; + if ($action === 'gradeReport') { + $msgtype = 'LtiSubmissionReviewRequest'; + } + echo lti_initiate_login($cm->course, $cmid, $lti, $config, $msgtype, '', '', $foruserid); exit; } else { unset($SESSION->lti_initiatelogin_status); @@ -85,5 +91,4 @@ if ($triggerview) { } $lti->cmid = $cm->id; -lti_launch_tool($lti); - +lti_launch_tool($lti, $foruserid); diff --git a/mod/lti/locallib.php b/mod/lti/locallib.php index 84c175d87ff..aa8cd198f46 100644 --- a/mod/lti/locallib.php +++ b/mod/lti/locallib.php @@ -116,6 +116,7 @@ function lti_get_jwt_message_type_mapping() { 'basic-lti-launch-request' => 'LtiResourceLinkRequest', 'ContentItemSelectionRequest' => 'LtiDeepLinkingRequest', 'LtiDeepLinkingResponse' => 'ContentItemSelection', + 'LtiSubmissionReviewRequest' => 'LtiSubmissionReviewRequest', ); } @@ -257,6 +258,12 @@ function lti_get_jwt_claim_mapping() { 'claim' => 'type', 'isarray' => true ], + 'for_user_id' => [ + 'suffix' => '', + 'group' => 'for_user', + 'claim' => 'user_id', + 'isarray' => false + ], 'lis_course_offering_sourcedid' => [ 'suffix' => '', 'group' => 'lis', @@ -531,9 +538,9 @@ function lti_get_instance_type(object $instance) : ?object { * @return array the endpoint URL and parameters (including the signature) * @since Moodle 3.0 */ -function lti_get_launch_data($instance, $nonce = '') { - global $PAGE, $CFG, $USER; - +function lti_get_launch_data($instance, $nonce = '', $messagetype = 'basic-lti-launch-request', $foruserid = 0) { + global $PAGE, $USER; + $messagetype = $messagetype ? $messagetype : 'basic-lti-launch-request'; $tool = lti_get_instance_type($instance); if ($tool) { $typeid = $tool->id; @@ -606,17 +613,22 @@ function lti_get_launch_data($instance, $nonce = '') { $course = $PAGE->course; $islti2 = isset($tool->toolproxyid); - $allparams = lti_build_request($instance, $typeconfig, $course, $typeid, $islti2); + $allparams = lti_build_request($instance, $typeconfig, $course, $typeid, $islti2, $messagetype, $foruserid); if ($islti2) { $requestparams = lti_build_request_lti2($tool, $allparams); } else { $requestparams = $allparams; } - $requestparams = array_merge($requestparams, lti_build_standard_message($instance, $orgid, $ltiversion)); + $requestparams = array_merge($requestparams, lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype)); $customstr = ''; if (isset($typeconfig['customparameters'])) { $customstr = $typeconfig['customparameters']; } + $services = lti_get_services(); + foreach ($services as $service) { + [$endpoint, $customstr] = $service->override_endpoint($messagetype, + $endpoint, $customstr, $instance->course, $instance); + } $requestparams = array_merge($requestparams, lti_build_custom_parameters($toolproxy, $tool, $instance, $allparams, $customstr, $instance->instructorcustomparameters, $islti2)); @@ -708,12 +720,13 @@ function lti_get_launch_data($instance, $nonce = '') { /** * Launch an external tool activity. * - * @param stdClass $instance the external tool activity settings + * @param stdClass $instance the external tool activity settings + * @param int $foruserid for user param, optional * @return string The HTML code containing the javascript code for the launch */ -function lti_launch_tool($instance) { +function lti_launch_tool($instance, $foruserid=0) { - list($endpoint, $parms) = lti_get_launch_data($instance); + list($endpoint, $parms) = lti_get_launch_data($instance, '', '', $foruserid); $debuglaunch = ( $instance->debuglaunch == 1 ); $content = lti_post_launch_html($parms, $endpoint, $debuglaunch); @@ -833,10 +846,13 @@ function lti_build_sourcedid($instanceid, $userid, $servicesalt, $typeid = null, * @param object $course Course object * @param int|null $typeid Basic LTI tool ID * @param boolean $islti2 True if an LTI 2 tool is being launched + * @param string $messagetype LTI Message Type for this launch + * @param int $foruserid User targeted by this launch * * @return array Request details */ -function lti_build_request($instance, $typeconfig, $course, $typeid = null, $islti2 = false) { +function lti_build_request($instance, $typeconfig, $course, $typeid = null, $islti2 = false, + $messagetype = 'basic-lti-launch-request', $foruserid = 0) { global $USER, $CFG; if (empty($instance->cmid)) { @@ -853,6 +869,12 @@ function lti_build_request($instance, $typeconfig, $course, $typeid = null, $isl 'context_label' => trim(html_to_text($course->shortname, 0)), 'context_title' => trim(html_to_text($course->fullname, 0)), ); + if ($foruserid) { + $requestparams['for_user_id'] = $foruserid; + } + if ($messagetype) { + $requestparams['lti_message_type'] = $messagetype; + } if (!empty($instance->name)) { $requestparams['resource_link_title'] = trim(html_to_text($instance->name, 0)); } @@ -1432,6 +1454,21 @@ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) { return $tool; } +/** + * Converts an array of custom parameters to a new line separated string. + * + * @param object $params list of params to concatenate + * + * @return string + */ +function params_to_string(object $params) { + $customparameters = []; + foreach ($params as $key => $value) { + $customparameters[] = "{$key}={$value}"; + } + return implode("\n", $customparameters); +} + /** * Converts LTI 1.1 Content Item for LTI Link to Form data. * @@ -1502,12 +1539,24 @@ function content_item_to_form(object $tool, object $typeconfig, object $item) : $config->grade_modgrade_point = $maxscore; $config->lineitemresourceid = ''; $config->lineitemtag = ''; + $config->lineitemsubreviewurl = ''; + $config->lineitemsubreviewparams = ''; if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) { $config->lineitemresourceid = $lineitem->assignedActivity->activityId?:''; } if (isset($lineitem->tag)) { $config->lineitemtag = $lineitem->tag?:''; } + if (isset($lineitem->submissionReview)) { + $subreview = $lineitem->submissionReview; + $config->lineitemsubreviewurl = 'DEFAULT'; + if (!empty($subreview->url)) { + $config->lineitemsubreviewurl = $subreview->url; + } + if (isset($subreview->custom)) { + $config->lineitemsubreviewparams = params_to_string($subreview->custom); + } + } } } } @@ -1524,11 +1573,7 @@ function content_item_to_form(object $tool, object $typeconfig, object $item) : } } if (isset($item->custom)) { - $customparameters = []; - foreach ($item->custom as $key => $value) { - $customparameters[] = "{$key}={$value}"; - } - $config->instructorcustomparameters = implode("\n", $customparameters); + $config->instructorcustomparameters = params_to_string($item->custom); } return $config; } @@ -1698,6 +1743,9 @@ function lti_convert_content_items($param) { $newitem->lineItem->scoreConstraints->{'@type'} = 'NumericLimits'; $newitem->lineItem->scoreConstraints->totalMaximum = $item->lineItem->scoreMaximum; } + if (isset($item->lineItem->submissionReview)) { + $newitem->lineItem->submissionReview = $item->lineItem->submissionReview; + } } $items[] = $newitem; } @@ -1950,17 +1998,13 @@ function lti_get_enabled_capabilities($tool) { } /** - * Splits the custom parameters field to the various parameters + * Splits the custom parameters * - * @param object $toolproxy Tool proxy instance object - * @param object $tool Tool instance object - * @param array $params LTI launch parameters * @param string $customstr String containing the parameters - * @param boolean $islti2 True if an LTI 2 tool is being launched * * @return array of custom parameters */ -function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) { +function lti_split_parameters($customstr) { $customstr = str_replace("\r\n", "\n", $customstr); $customstr = str_replace("\n\r", "\n", $customstr); $customstr = str_replace("\r", "\n", $customstr); @@ -1973,6 +2017,26 @@ function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $is } $key = trim(core_text::substr($line, 0, $pos)); $val = trim(core_text::substr($line, $pos + 1, strlen($line))); + $retval[$key] = $val; + } + return $retval; +} + +/** + * Splits the custom parameters field to the various parameters + * + * @param object $toolproxy Tool proxy instance object + * @param object $tool Tool instance object + * @param array $params LTI launch parameters + * @param string $customstr String containing the parameters + * @param boolean $islti2 True if an LTI 2 tool is being launched + * + * @return array of custom parameters + */ +function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) { + $splitted = lti_split_parameters($customstr); + $retval = array(); + foreach ($splitted as $key => $val) { $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2); $key2 = lti_map_keyname($key); $retval['custom_'.$key2] = $val; @@ -3550,21 +3614,20 @@ function lti_post_launch_html($newparms, $endpoint, $debug=false) { * Generate the form for initiating a login request for an LTI 1.3 message * * @param int $courseid Course ID - * @param int $id LTI instance ID + * @param int $cmid LTI instance ID * @param stdClass|null $instance LTI instance * @param stdClass $config Tool type configuration * @param string $messagetype LTI message type * @param string $title Title of content item * @param string $text Description of content item + * @param int $foruserid Id of the user targeted by the launch * @return string */ -function lti_initiate_login($courseid, $id, $instance, $config, $messagetype = 'basic-lti-launch-request', $title = '', - $text = '') { +function lti_initiate_login($courseid, $cmid, $instance, $config, $messagetype = 'basic-lti-launch-request', + $title = '', $text = '', $foruserid = 0) { global $SESSION; - $params = lti_build_login_request($courseid, $id, $instance, $config, $messagetype); - $SESSION->lti_message_hint = "{$courseid},{$config->typeid},{$id}," . base64_encode($title) . ',' . - base64_encode($text); + $params = lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid, $title, $text); $r = "
lti_initiatelogin . "\" name=\"ltiInitiateLoginForm\" id=\"ltiInitiateLoginForm\" method=\"post\" " . @@ -3590,25 +3653,39 @@ function lti_initiate_login($courseid, $id, $instance, $config, $messagetype = ' * Prepares an LTI 1.3 login request * * @param int $courseid Course ID - * @param int $id LTI instance ID + * @param int $cmid Course Module instance ID * @param stdClass|null $instance LTI instance * @param stdClass $config Tool type configuration * @param string $messagetype LTI message type + * @param int $foruserid Id of the user targeted by the launch + * @param string $title Title of content item + * @param string $text Description of content item * @return array Login request parameters */ -function lti_build_login_request($courseid, $id, $instance, $config, $messagetype) { - global $USER, $CFG; - +function lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid=0, $title = '', $text = '') { + global $USER, $CFG, $SESSION; + $ltihint = []; if (!empty($instance)) { $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl; + $launchid = 'ltilaunch'.$instance->id.'_'.rand(); + $ltihint['cmid'] = $cmid; + $SESSION->$launchid = "{$courseid},{$config->typeid},{$cmid},{$messagetype},{$foruserid},,"; } else { $endpoint = $config->lti_toolurl; if (($messagetype === 'ContentItemSelectionRequest') && !empty($config->lti_toolurl_ContentItemSelectionRequest)) { $endpoint = $config->lti_toolurl_ContentItemSelectionRequest; } + $launchid = "ltilaunch_$messagetype".rand(); + $SESSION->$launchid = + "{$courseid},{$config->typeid},,{$messagetype},{$foruserid}," . base64_encode($title) . ',' . base64_encode($text); } $endpoint = trim($endpoint); + $services = lti_get_services(); + foreach ($services as $service) { + [$endpoint] = $service->override_endpoint($messagetype ?? 'basic-lti-launch-request', $endpoint, '', $courseid, $instance); + } + $ltihint['launchid'] = $launchid; // If SSL is forced make sure https is on the normal launch URL. if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) { $endpoint = lti_ensure_url_is_https($endpoint); @@ -3620,7 +3697,7 @@ function lti_build_login_request($courseid, $id, $instance, $config, $messagetyp $params['iss'] = $CFG->wwwroot; $params['target_link_uri'] = $endpoint; $params['login_hint'] = $USER->id; - $params['lti_message_hint'] = $id; + $params['lti_message_hint'] = json_encode($ltihint); $params['client_id'] = $config->lti_clientid; $params['lti_deployment_id'] = $config->typeid; return $params; diff --git a/mod/lti/mod_form.php b/mod/lti/mod_form.php index 27fee1a0afc..077ad68fbba 100644 --- a/mod/lti/mod_form.php +++ b/mod/lti/mod_form.php @@ -221,6 +221,12 @@ class mod_lti_mod_form extends moodleform_mod { $mform->addElement('hidden', 'lineitemtag', '', array( 'id' => 'id_lineitemtag')); $mform->setType('lineitemtag', PARAM_TEXT); + $mform->addElement('hidden', 'lineitemsubreviewurl', '', array( 'id' => 'id_lineitemsubreviewurl')); + $mform->setType('lineitemsubreviewurl', PARAM_URL); + + $mform->addElement('hidden', 'lineitemsubreviewparams', '', array( 'id' => 'id_lineitemsubreviewparams')); + $mform->setType('lineitemsubreviewparams', PARAM_TEXT); + $launchoptions = array(); $launchoptions[LTI_LAUNCH_CONTAINER_DEFAULT] = get_string('default', 'lti'); $launchoptions[LTI_LAUNCH_CONTAINER_EMBED] = get_string('embed', 'lti'); diff --git a/mod/lti/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php b/mod/lti/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php index 27273747860..e992e5a748e 100644 --- a/mod/lti/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php +++ b/mod/lti/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php @@ -64,7 +64,9 @@ class backup_ltiservice_gradebookservices_subplugin extends backup_subplugin { 'resourceid', 'tag', 'vendorcode', - 'guid' + 'guid', + 'subreviewurl', + 'subreviewparams' ) ); diff --git a/mod/lti/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php b/mod/lti/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php index b58ab260e89..f829deb94a9 100644 --- a/mod/lti/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php +++ b/mod/lti/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php @@ -115,7 +115,9 @@ class restore_ltiservice_gradebookservices_subplugin extends restore_subplugin { 'typeid' => $newtypeid, 'baseurl' => $data->baseurl, 'resourceid' => $resourceid, - 'tag' => $data->tag + 'tag' => $data->tag, + 'subreviewparams' => $data->subreviewparams ?? '', + 'subreviewurl' => $data->subreviewurl ?? '' )); $this->set_mapping('gbsgradeitemoldid', $newgbsid, $data->gradeitemid); $this->set_mapping('gbsgradeitemrestored', $data->id, $data->id); diff --git a/mod/lti/service/gradebookservices/classes/local/resources/lineitem.php b/mod/lti/service/gradebookservices/classes/local/resources/lineitem.php index 7427ca43125..58528171105 100644 --- a/mod/lti/service/gradebookservices/classes/local/resources/lineitem.php +++ b/mod/lti/service/gradebookservices/classes/local/resources/lineitem.php @@ -178,6 +178,19 @@ class lineitem extends resource_base { } $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)) { @@ -257,7 +270,9 @@ class lineitem extends resource_base { 'baseurl' => $baseurl, 'ltilinkid' => $ltilinkid, 'resourceid' => $resourceid, - 'tag' => $gbs->tag + 'tag' => $gbs->tag, + 'subreviewurl' => $gbs->subreviewurl, + 'subreviewparams' => $gbs->subreviewparams )); } @@ -315,7 +330,13 @@ class lineitem extends resource_base { } $id = optional_param('id', 0, PARAM_INT); // Course Module ID. if (empty($id)) { - $id = optional_param('lti_message_hint', 0, PARAM_INT); + $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); diff --git a/mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php b/mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php index e51e41f2355..3dc691e035b 100644 --- a/mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php +++ b/mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php @@ -31,9 +31,13 @@ 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. * @@ -143,6 +147,42 @@ class gradebookservices extends service_base { $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/values to add to the launch parameters. * @@ -489,6 +529,16 @@ class gradebookservices extends service_base { $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)) { @@ -615,25 +665,31 @@ class gradebookservices extends service_base { } /** - * Updates the tag and resourceid values for a grade item coupled to an lti link instance. + * 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) : void { + 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; @@ -644,7 +700,9 @@ class gradebookservices extends service_base { 'baseurl' => $baseurl, 'ltilinkid' => $ltiinstance->id, 'resourceid' => $resourceid, - 'tag' => $tag + 'tag' => $tag, + 'subreviewurl' => $subreviewurlstr, + 'subreviewparams' => $subreviewparams )); } } @@ -657,7 +715,9 @@ class gradebookservices extends service_base { * @param object $lti LTI Instance. */ public function instance_added(object $lti): void { - self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null); + self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null, + isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null, + $lti->lineitemsubreviewparams ?? null); } /** @@ -666,7 +726,9 @@ class gradebookservices extends service_base { * @param object $lti LTI Instance. */ public function instance_updated(object $lti): void { - self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null); + self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null, + isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null, + $lti->lineitemsubreviewparams ?? null); } /** @@ -677,11 +739,15 @@ class gradebookservices extends service_base { 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; } } } diff --git a/mod/lti/service/gradebookservices/db/install.xml b/mod/lti/service/gradebookservices/db/install.xml index e75e0d7f89f..9d978071692 100644 --- a/mod/lti/service/gradebookservices/db/install.xml +++ b/mod/lti/service/gradebookservices/db/install.xml @@ -15,6 +15,8 @@ + + diff --git a/mod/lti/service/gradebookservices/db/upgrade.php b/mod/lti/service/gradebookservices/db/upgrade.php index 1327d1c0f87..be24279b7c2 100644 --- a/mod/lti/service/gradebookservices/db/upgrade.php +++ b/mod/lti/service/gradebookservices/db/upgrade.php @@ -113,5 +113,20 @@ function xmldb_ltiservice_gradebookservices_upgrade($oldversion) { // Automatically generated Moodle v4.0.0 release upgrade line. // Put any upgrade step following this. + if ($oldversion < 2022051900) { + $table = new xmldb_table('ltiservice_gradebookservices'); + $field = new xmldb_field('subreviewurl', XMLDB_TYPE_TEXT, null, null, null, null, null); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('subreviewparams', XMLDB_TYPE_TEXT, null, null, null, null, null); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Lti savepoint reached. + upgrade_plugin_savepoint(true, 2022051900, 'ltiservice', 'gradebookservices'); + } + return true; } diff --git a/mod/lti/service/gradebookservices/tests/gradebookservices_test.php b/mod/lti/service/gradebookservices/tests/gradebookservices_test.php index b1d297be889..50b5479dc96 100644 --- a/mod/lti/service/gradebookservices/tests/gradebookservices_test.php +++ b/mod/lti/service/gradebookservices/tests/gradebookservices_test.php @@ -25,10 +25,13 @@ use ltiservice_gradebookservices\local\service\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 */ class gradebookservices_test extends \advanced_testcase { /** + * @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. @@ -46,8 +49,10 @@ class gradebookservices_test extends \advanced_testcase { $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); + $ltiinstance = $this->create_graded_lti($typeid, $course, $resourceid, $tag, $subreviewurl, $subreviewparams); $this->assertNotNull($ltiinstance); @@ -56,11 +61,47 @@ class gradebookservices_test extends \advanced_testcase { $this->assertNotNull($gbs); $this->assertEquals($resourceid, $gbs->resourceid); $this->assertEquals($tag, $gbs->tag); - - $this->assert_lineitems($course, $typeid, $ltiinstance->name, $ltiinstance, $resourceid, $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() { + global $CFG; + require_once($CFG->dirroot . '/mod/lti/locallib.php'); + + $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. */ @@ -79,6 +120,8 @@ class gradebookservices_test extends \advanced_testcase { } /** + * @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 @@ -96,7 +139,7 @@ class gradebookservices_test extends \advanced_testcase { $typeid = $this->create_type(); $course = $this->getDataGenerator()->create_course(); - $ltiinstance = $this->create_graded_lti($typeid, $course, 'resource-id', 'tag'); + $ltiinstance = $this->create_graded_lti($typeid, $course, 'resource-id', 'tag', 'https://subreview.url', 'sub=review'); $this->assertNotNull($ltiinstance); @@ -113,6 +156,70 @@ class gradebookservices_test extends \advanced_testcase { } /** + * @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() { + global $CFG; + require_once($CFG->dirroot . '/mod/lti/locallib.php'); + + $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() { + global $CFG; + require_once($CFG->dirroot . '/mod/lti/locallib.php'); + + $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. */ @@ -149,6 +256,8 @@ class gradebookservices_test extends \advanced_testcase { } /** + * @covers ::is_user_gradable_in_course + * * Test if a user can be graded in a course. */ public function test_is_user_gradable_in_course() { @@ -174,20 +283,36 @@ class gradebookservices_test extends \advanced_testcase { * @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) : void { + 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($this->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($this->submissionReview->custom)); + } + } $gradeitems = $gbservice->get_lineitems($course->id, $resourceid, null, null, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); @@ -216,17 +341,22 @@ class gradebookservices_test extends \advanced_testcase { * @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) : object { + 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]; + 'lineitemtag' => $tag, + 'lineitemsubreviewurl' => $subreviewurl, + 'lineitemsubreviewparams' => $subreviewparams]; return $this->getDataGenerator()->create_module('lti', $lti, array()); } diff --git a/mod/lti/service/gradebookservices/tests/lineitem_test.php b/mod/lti/service/gradebookservices/tests/lineitem_test.php new file mode 100644 index 00000000000..01e5493f68b --- /dev/null +++ b/mod/lti/service/gradebookservices/tests/lineitem_test.php @@ -0,0 +1,236 @@ +. + +namespace ltiservice_gradebookservices; + +use ltiservice_gradebookservices\local\resources\lineitem; +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 + */ +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; " .