MDL-67473 mod_lti: deeplinking multiple support
authorClaude Vervoort <claude.vervoort@cengage.com>
Fri, 6 Dec 2019 18:47:54 +0000 (13:47 -0500)
committerClaude Vervoort <claude.vervoort@cengage.com>
Wed, 30 Sep 2020 12:03:59 +0000 (08:03 -0400)
AMOS BEGIN
 CPY [contentitem_help,mod_lti],[contentitem_deeplinking_help,mod_lti]
AMOS END

15 files changed:
mod/lti/amd/build/contentitem.min.js
mod/lti/amd/build/contentitem.min.js.map
mod/lti/amd/src/contentitem.js
mod/lti/auth.php
mod/lti/edit_form.php
mod/lti/lang/en/deprecated.txt
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/mod_form.js
mod/lti/mod_form.php
mod/lti/templates/tool_deeplinking_results.mustache [new file with mode: 0644]
mod/lti/tests/behat/contentitem.feature
mod/lti/tests/behat/contentitemregistration.feature
mod/lti/tests/locallib_test.php
mod/lti/upgrade.txt

index 607b518..7d681e8 100644 (file)
Binary files a/mod/lti/amd/build/contentitem.min.js and b/mod/lti/amd/build/contentitem.min.js differ
index 32c354f..c9ddf82 100644 (file)
Binary files a/mod/lti/amd/build/contentitem.min.js.map and b/mod/lti/amd/build/contentitem.min.js.map differ
index bc18ae2..3d10cb4 100644 (file)
@@ -109,8 +109,81 @@ define(
             new FormField('lineitemtag', FormField.TYPES.TEXT, true, '')
         ];
 
+        /**
+         * Hide the element, including aria and tab index.
+         * @param {HTMLElement} e the element to be hidden.
+         */
+        const hideElement = (e) => {
+            e.setAttribute('hidden', 'true');
+            e.setAttribute('aria-hidden', 'true');
+            e.setAttribute('tab-index', '-1');
+        };
+
+        /**
+         * Show the element, including aria and tab index (set to 1).
+         * @param {HTMLElement} e the element to be shown.
+         */
+        const showElement = (e) => {
+            e.removeAttribute('hidden');
+            e.setAttribute('aria-hidden', 'false');
+            e.setAttribute('tab-index', '1');
+        };
+
+        /**
+         * When more than one item needs to be added, the UI is simplified
+         * to just list the items to be added. Form is hidden and the only
+         * options is (save and return to course) or cancel.
+         * This function injects the summary to the form page, and hides
+         * the unneeded elements.
+         * @param {Object[]} items items to be added to the course.
+         */
+        const showMultipleSummaryAndHideForm = async function(items) {
+            const form = document.querySelector('#region-main-box form');
+            const toolArea = form.querySelector('[data-attribute="dynamic-import"]');
+            const buttonGroup = form.querySelector('#fgroup_id_buttonar');
+            const submitAndLaunch = form.querySelector('#id_submitbutton');
+            Array.from(form.children).forEach(hideElement);
+            hideElement(submitAndLaunch);
+            const {html, js} = await templates.renderForPromise('mod_lti/tool_deeplinking_results',
+                {items: items});
+
+            await templates.replaceNodeContents(toolArea, html, js);
+            showElement(toolArea);
+            showElement(buttonGroup);
+        };
+
+        /**
+         * Transforms config values aimed at populating the lti mod form to JSON variant
+         * which are used to insert more than one activity modules in one submit
+         * by applying variation to the submitted form.
+         * See /course/modedit.php.
+         * @private
+         * @param {Object} config transforms a config to an actual form data to be posted.
+         * @return {Object} variant that will be used to modify form values on submit.
+         */
+        var configToVariant = (config) => {
+            const variant = {};
+            ['name', 'toolurl', 'securetoolurl', 'instructorcustomparameters', 'icon', 'secureicon', 'launchcontainer'].forEach(
+                function(name) {
+                    variant[name] = config[name] || '';
+                }
+            );
+            variant['introeditor[text]'] = config.introeditor ? config.introeditor.text : '';
+            variant['introeditor[format]'] = config.introeditor ? config.introeditor.format : '';
+            if (config.instructorchoiceacceptgrades === 1) {
+                variant.instructorchoiceacceptgrades = '1';
+                variant['grade[modgrade_point]'] = config.grade_modgrade_point || '100';
+            } else {
+                variant.instructorchoiceacceptgrades = '0';
+            }
+            return variant;
+        };
+
         /**
          * Window function that can be called from mod_lti/contentitem_return to close the dialogue and process the return data.
+         * If the return data contains more than one item, the form will not be populated with item data
+         * but rather hidden, and the item data will be added to a single input field used to create multiple
+         * instances in one request.
          *
          * @param {object} returnData The fetched configuration data from the Content-Item selection dialogue.
          */
@@ -118,20 +191,49 @@ define(
             if (dialogue) {
                 dialogue.hide();
             }
-
-            // Populate LTI configuration fields from return data.
             var index;
-            for (index in ltiFormFields) {
-                var field = ltiFormFields[index];
-                var value = null;
-                if (typeof returnData[field.name] !== 'undefined') {
-                    value = returnData[field.name];
+            if (returnData.multiple) {
+                for (index in ltiFormFields) {
+                    // Name is required, so putting a placeholder as it will not be used
+                    // in multi-items add.
+                    ltiFormFields[index].setFieldValue(ltiFormFields[index].name === 'name' ? 'item' : null);
+                }
+                var variants = [];
+                returnData.multiple.forEach(function(v) {
+                    variants.push(configToVariant(v));
+                });
+                showMultipleSummaryAndHideForm(returnData.multiple);
+                const submitAndCourse = document.querySelector('#id_submitbutton2');
+                submitAndCourse.onclick = (e) => {
+                    e.preventDefault();
+                    submitAndCourse.disabled = true;
+                    const fd = new FormData(document.querySelector('form.mform'));
+                    const postVariant = (promise, variant) => {
+                        Object.entries(variant).forEach((entry) => fd.set(entry[0], entry[1]));
+                        const body = new URLSearchParams(fd);
+                        const doPost = () => fetch(document.location.pathname, {method: 'post', body});
+                        return promise.then(doPost).catch(doPost);
+                    };
+                    const backToCourse = () => {
+                        document.querySelector("#id_cancel").click();
+                    };
+                    variants.reduce(postVariant, Promise.resolve()).then(backToCourse).catch(backToCourse);
+                };
+            } else {
+                // Populate LTI configuration fields from return data.
+                for (index in ltiFormFields) {
+                    var field = ltiFormFields[index];
+                    var value = null;
+                    if (typeof returnData[field.name] !== 'undefined') {
+                        value = returnData[field.name];
+                    }
+                    field.setFieldValue(value);
                 }
                 field.setFieldValue(value);
             }
 
             if (doneCallback) {
-                doneCallback();
+                doneCallback(returnData);
             }
         };
 
index 68c5ccd..0f57e6a 100644 (file)
@@ -120,7 +120,7 @@ if ($ok) {
         $title = base64_decode($titleb64);
         $text = base64_decode($textb64);
         $request = lti_build_content_item_selection_request($typeid, $course, $returnurl, $title, $text,
-                                                            [], [], false, false, false, false, false, $nonce);
+                                                            [], [], false, true, false, false, false, $nonce);
         $endpoint = $request->url;
         $params = $request->params;
     }
index 83361f5..d0f06fd 100644 (file)
@@ -214,9 +214,8 @@ class mod_lti_edit_types_form extends moodleform {
         $mform->addHelpButton('lti_launchcontainer', 'default_launch_container', 'lti');
         $mform->setType('lti_launchcontainer', PARAM_INT);
 
-        $mform->addElement('advcheckbox', 'lti_contentitem', get_string('contentitem', 'lti'));
-        $mform->addHelpButton('lti_contentitem', 'contentitem', 'lti');
-        $mform->setAdvanced('lti_contentitem');
+        $mform->addElement('advcheckbox', 'lti_contentitem', get_string('contentitem_deeplinking', 'lti'));
+        $mform->addHelpButton('lti_contentitem', 'contentitem_deeplinking', 'lti');
         if ($istool) {
             $mform->disabledIf('lti_contentitem', null);
         }
@@ -224,7 +223,6 @@ class mod_lti_edit_types_form extends moodleform {
         $mform->addElement('text', 'lti_toolurl_ContentItemSelectionRequest',
             get_string('toolurl_contentitemselectionrequest', 'lti'), array('size' => '64'));
         $mform->setType('lti_toolurl_ContentItemSelectionRequest', PARAM_URL);
-        $mform->setAdvanced('lti_toolurl_ContentItemSelectionRequest');
         $mform->addHelpButton('lti_toolurl_ContentItemSelectionRequest', 'toolurl_contentitemselectionrequest', 'lti');
         $mform->disabledIf('lti_toolurl_ContentItemSelectionRequest', 'lti_contentitem', 'notchecked');
         if ($istool) {
@@ -271,7 +269,12 @@ class mod_lti_edit_types_form extends moodleform {
             // LTI Extensions.
 
             // Add grading preferences fieldset where the tool is allowed to return grades.
-            $mform->addElement('select', 'lti_acceptgrades', get_string('accept_grades_admin', 'lti'), $options);
+            $gradeoptions = array();
+            $gradeoptions[] = get_string('never', 'lti');
+            $gradeoptions[] = get_string('always', 'lti');
+            $gradeoptions[] = get_string('delegate_tool', 'lti');
+
+            $mform->addElement('select', 'lti_acceptgrades', get_string('accept_grades_admin', 'lti'), $gradeoptions);
             $mform->setType('lti_acceptgrades', PARAM_INT);
             $mform->setDefault('lti_acceptgrades', '2');
             $mform->addHelpButton('lti_acceptgrades', 'accept_grades_admin', 'lti');
index 0d0691f..33cc7da 100644 (file)
@@ -1,3 +1,5 @@
 leaveblank,mod_lti
 organizationid,mod_lti
 organizationid_help,mod_lti
+contentitem,mod_lti
+contentitem_help,mod_lti
index 01adfc7..51d2593 100644 (file)
@@ -108,8 +108,10 @@ $string['configtoolurl'] = 'Default remote tool URL';
 $string['configtypes'] = 'Enable LTI applications';
 $string['configured'] = 'Configured';
 $string['confirmtoolactivation'] = 'Are you sure you would like to activate this tool?';
-$string['contentitem'] = 'Content-Item Message';
-$string['contentitem_help'] = 'If ticked, the option \'Select content\' will be available when adding an external tool.';
+$string['contentitem_deeplinking'] = 'Supports Deep Linking (Content-Item Message)';
+$string['contentitem_deeplinking_help'] = 'If ticked, the option \'Select content\' will be available when adding an external tool.';
+$string['contentitem_multiple_description'] = 'The following items will be added to your course:';
+$string['contentitem_multiple_graded'] = 'Graded activity (Maximum grade: {$a})';
 $string['course_tool_types'] = 'Course tools';
 $string['courseactivitiesorresources'] = 'Course activities or resources';
 $string['courseid'] = 'Course ID number';
@@ -141,6 +143,7 @@ real estate to the tool, and others provide a more integrated feel with the Mood
         Depending on the browser, it will open in a new tab or a popup window.
         It is possible that browsers will prevent the new window from opening.';
 $string['delegate'] = 'Delegate to teacher';
+$string['delegate_tool'] = 'As specified in Deep Linking definition or Delegate to teacher';
 $string['delete'] = 'Delete';
 $string['delete_confirmation'] = 'Are you sure you want to delete this preconfigured tool?';
 $string['deletetype'] = 'Delete preconfigured tool';
@@ -593,3 +596,8 @@ $string['organizationid'] = 'Organisation ID';
 $string['organizationid_help'] = 'A unique identifier for this Moodle instance. Typically, the DNS name of the organisation is used.
 
 If this field is left blank, the host name of this Moodle site will be used as the default value.';
+
+// Deprecated since Moodle 3.10.
+$string['contentitem'] = 'Content-Item Message';
+$string['contentitem_help'] = 'If ticked, the option \'Select content\' will be available when adding an external tool.';
+
index 1e548d5..b33e91f 100644 (file)
@@ -1083,7 +1083,7 @@ function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $cus
  * @throws coding_exception For invalid media type and presentation target parameters.
  */
 function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [],
-                                                  $presentationtargets = [], $autocreate = false, $multiple = false,
+                                                  $presentationtargets = [], $autocreate = false, $multiple = true,
                                                   $unsigned = false, $canconfirm = false, $copyadvice = false, $nonce = '') {
     global $USER;
 
@@ -1421,6 +1421,109 @@ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {
     return $tool;
 }
 
+/**
+ * Converts LTI 1.1 Content Item for LTI Link to Form data.
+ *
+ * @param object $tool Tool for which the item is created for.
+ * @param object $typeconfig The tool configuration.
+ * @param object $item Item populated from JSON to be converted to Form form
+ *
+ * @return stdClass Form config for the item
+ */
+function content_item_to_form(object $tool, object $typeconfig, object $item) : stdClass {
+    $config = new stdClass();
+    $config->name = '';
+    if (isset($item->title)) {
+        $config->name = $item->title;
+    }
+    if (empty($config->name)) {
+        $config->name = $tool->name;
+    }
+    if (isset($item->text)) {
+        $config->introeditor = [
+            'text' => $item->text,
+            'format' => FORMAT_PLAIN
+        ];
+    } else {
+        $config->introeditor = [
+            'text' => '',
+            'format' => FORMAT_PLAIN
+        ];
+    }
+    if (isset($item->icon->{'@id'})) {
+        $iconurl = new moodle_url($item->icon->{'@id'});
+        // Assign item's icon URL to secureicon or icon depending on its scheme.
+        if (strtolower($iconurl->get_scheme()) === 'https') {
+            $config->secureicon = $iconurl->out(false);
+        } else {
+            $config->icon = $iconurl->out(false);
+        }
+    }
+    if (isset($item->url)) {
+        $url = new moodle_url($item->url);
+        $config->toolurl = $url->out(false);
+        $config->typeid = 0;
+    } else {
+        $config->typeid = $tool->id;
+    }
+    $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
+    $islti2 = $tool->ltiversion === LTI_VERSION_2;
+    if (!$islti2 && isset($typeconfig->lti_acceptgrades)) {
+        $acceptgrades = $typeconfig->lti_acceptgrades;
+        if ($acceptgrades == LTI_SETTING_ALWAYS) {
+            // We create a line item regardless if the definition contains one or not.
+            $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
+            $config->grade_modgrade_point = 100;
+        }
+        if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) {
+            if (isset($item->lineItem)) {
+                $lineitem = $item->lineItem;
+                $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
+                $maxscore = 100;
+                if (isset($lineitem->scoreConstraints)) {
+                    $sc = $lineitem->scoreConstraints;
+                    if (isset($sc->totalMaximum)) {
+                        $maxscore = $sc->totalMaximum;
+                    } else if (isset($sc->normalMaximum)) {
+                        $maxscore = $sc->normalMaximum;
+                    }
+                }
+                $config->grade_modgrade_point = $maxscore;
+                $config->lineitemresourceid = '';
+                $config->lineitemtag = '';
+                if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) {
+                    $config->lineitemresourceid = $lineitem->assignedActivity->activityId?:'';
+                }
+                if (isset($lineitem->tag)) {
+                    $config->lineitemtag = $lineitem->tag?:'';
+                }
+            }
+        }
+    }
+    $config->instructorchoicesendname = LTI_SETTING_NEVER;
+    $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER;
+    $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
+    if (isset($item->placementAdvice->presentationDocumentTarget)) {
+        if ($item->placementAdvice->presentationDocumentTarget === 'window') {
+            $config->launchcontainer = LTI_LAUNCH_CONTAINER_WINDOW;
+        } else if ($item->placementAdvice->presentationDocumentTarget === 'frame') {
+            $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
+        } else if ($item->placementAdvice->presentationDocumentTarget === 'iframe') {
+            $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED;
+        }
+    }
+    if (isset($item->custom)) {
+        $customparameters = [];
+        foreach ($item->custom as $key => $value) {
+            $customparameters[] = "{$key}={$value}";
+        }
+        $config->instructorcustomparameters = implode("\n", $customparameters);
+    }
+    // Including a JSON version of the form data to support adding many items in one submit.
+    $config->contentitemjson = json_encode($item);
+    return $config;
+}
+
 /**
  * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the
  * selected content item. This configuration data can be then used when adding a tool into the course.
@@ -1460,97 +1563,24 @@ function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiver
     if (empty($items)) {
         throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson);
     }
-    if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'}) || (count($items->{'@graph'}) > 1)) {
+    if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'})) {
         throw new moodle_exception('errorinvalidresponseformat', 'mod_lti');
     }
 
     $config = null;
-    if (!empty($items->{'@graph'})) {
-        $item = $items->{'@graph'}[0];
+    $items = $items->{'@graph'};
+    if (!empty($items)) {
         $typeconfig = lti_get_type_type_config($tool->id);
-
-        $config = new stdClass();
-        $config->name = '';
-        if (isset($item->title)) {
-            $config->name = $item->title;
-        }
-        if (empty($config->name)) {
-            $config->name = $tool->name;
-        }
-        if (isset($item->text)) {
-            $config->introeditor = [
-                'text' => $item->text,
-                'format' => FORMAT_PLAIN
-            ];
-        }
-        if (isset($item->icon->{'@id'})) {
-            $iconurl = new moodle_url($item->icon->{'@id'});
-            // Assign item's icon URL to secureicon or icon depending on its scheme.
-            if (strtolower($iconurl->get_scheme()) === 'https') {
-                $config->secureicon = $iconurl->out(false);
-            } else {
-                $config->icon = $iconurl->out(false);
-            }
-        }
-        if (isset($item->url)) {
-            $url = new moodle_url($item->url);
-            $config->toolurl = $url->out(false);
-            $config->typeid = 0;
+        if (count($items) == 1) {
+            $config = content_item_to_form($tool, $typeconfig, $items[0]);
         } else {
-            $config->typeid = $tool->id;
-        }
-        $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
-        if (!$islti2 && isset($typeconfig->lti_acceptgrades)) {
-            $acceptgrades = $typeconfig->lti_acceptgrades;
-            if ($acceptgrades == LTI_SETTING_ALWAYS) {
-                // We create a line item regardless if the definition contains one or not.
-                $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
-            }
-            if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) {
-                if (isset($item->lineItem)) {
-                    $lineitem = $item->lineItem;
-                    $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
-                    $maxscore = 100;
-                    if (isset($lineitem->scoreConstraints)) {
-                        $sc = $lineitem->scoreConstraints;
-                        if (isset($sc->totalMaximum)) {
-                            $maxscore = $sc->totalMaximum;
-                        } else if (isset($sc->normalMaximum)) {
-                            $maxscore = $sc->normalMaximum;
-                        }
-                    }
-                    $config->grade_modgrade_point = $maxscore;
-                    $config->lineitemresourceid = '';
-                    $config->lineitemtag = '';
-                    if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) {
-                        $config->lineitemresourceid = $lineitem->assignedActivity->activityId ? : '';
-                    }
-                    if (isset($lineitem->tag)) {
-                        $config->lineitemtag = $lineitem->tag ? : '';
-                    }
-                }
-            }
-        }
-        $config->instructorchoicesendname = LTI_SETTING_NEVER;
-        $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER;
-        $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
-        if (isset($item->placementAdvice->presentationDocumentTarget)) {
-            if ($item->placementAdvice->presentationDocumentTarget === 'window') {
-                $config->launchcontainer = LTI_LAUNCH_CONTAINER_WINDOW;
-            } else if ($item->placementAdvice->presentationDocumentTarget === 'frame') {
-                $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
-            } else if ($item->placementAdvice->presentationDocumentTarget === 'iframe') {
-                $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED;
-            }
-        }
-        if (isset($item->custom)) {
-            $customparameters = [];
-            foreach ($item->custom as $key => $value) {
-                $customparameters[] = "{$key}={$value}";
+            $multiple = [];
+            foreach ($items as $item) {
+                $multiple[] = content_item_to_form($tool, $typeconfig, $item);
             }
-            $config->instructorcustomparameters = implode("\n", $customparameters);
+            $config = new stdClass();
+            $config->multiple = $multiple;
         }
-        $config->contentitemjson = json_encode($item);
     }
     return $config;
 }
@@ -1587,7 +1617,35 @@ function lti_convert_content_items($param) {
                     $newitem->text = $item->html;
                     unset($newitem->html);
                 }
-                if (isset($item->presentation)) {
+                if (isset($item->iframe)) {
+                    // DeepLinking allows multiple options to be declared as supported.
+                    // We favor iframe over new window if both are specified.
+                    $newitem->placementAdvice = new stdClass();
+                    $newitem->placementAdvice->presentationDocumentTarget = 'iframe';
+                    if (isset($item->iframe->width)) {
+                        $newitem->placementAdvice->displayWidth = $item->iframe->width;
+                    }
+                    if (isset($item->iframe->height)) {
+                        $newitem->placementAdvice->displayHeight = $item->iframe->height;
+                    }
+                    unset($newitem->iframe);
+                    unset($newitem->window);
+                } else if (isset($item->window)) {
+                    $newitem->placementAdvice = new stdClass();
+                    $newitem->placementAdvice->presentationDocumentTarget = 'window';
+                    if (isset($item->window->targetName)) {
+                        $newitem->placementAdvice->windowTarget = $item->window->targetName;
+                    }
+                    if (isset($item->window->width)) {
+                        $newitem->placementAdvice->displayWidth = $item->window->width;
+                    }
+                    if (isset($item->window->height)) {
+                        $newitem->placementAdvice->displayHeight = $item->window->height;
+                    }
+                    unset($newitem->window);
+                } else if (isset($item->presentation)) {
+                    // This may have been part of an early draft but is not in the final spec
+                    // so keeping it around for now in case it's actually been used.
                     $newitem->placementAdvice = new stdClass();
                     if (isset($item->presentation->documentTarget)) {
                         $newitem->placementAdvice->presentationDocumentTarget = $item->presentation->documentTarget;
index 2b21157..6398236 100644 (file)
                         };
 
                         require(['mod_lti/contentitem'], function(contentitem) {
-                            contentitem.init(contentItemUrl, postData, function() {
-                                M.mod_lti.editor.toggleGradeSection();
+                            contentitem.init(contentItemUrl, postData, function(returnData) {
+                                if (!returnData.multiple) {
+                                    M.mod_lti.editor.toggleGradeSection();
+                                }
                             });
                         });
                     }
index 70caa9c..adc17ba 100644 (file)
@@ -79,8 +79,11 @@ class mod_lti_mod_form extends moodleform_mod {
         $this->typeid = 0;
 
         $mform =& $this->_form;
+
         // Adding the "general" fieldset, where all the common settings are shown.
+        $mform->addElement('html', "<div data-attribute='dynamic-import' hidden aria-hidden='true' role='alert'></div>");
         $mform->addElement('header', 'general', get_string('general', 'form'));
+
         // Adding the standard "name" field.
         $mform->addElement('text', 'name', get_string('basicltiname', 'lti'), array('size' => '64'));
         $mform->setType('name', PARAM_TEXT);
diff --git a/mod/lti/templates/tool_deeplinking_results.mustache b/mod/lti/templates/tool_deeplinking_results.mustache
new file mode 100644 (file)
index 0000000..742f967
--- /dev/null
@@ -0,0 +1,59 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template mod_lti/tool_deeplinking_results
+
+    This template lists the items that will be added to the course after
+    a deep linking flow.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    *
+
+    Example context (json):
+    {
+        "items": [
+            {
+                "name": "Chapter 1",
+                "instructorchoiceacceptgrades": 0
+            },
+            {
+                "name": "Quiz 1",
+                "instructorchoiceacceptgrades": 1,
+                "grade_modgrade_point": 20.5
+            }
+        ]
+    }
+}}
+<p>
+    {{#str}} contentitem_multiple_description, mod_lti {{/str}}
+</p>
+<ul>
+    {{#items}}
+        <li>
+            <strong>{{{name}}}</strong>
+            {{#instructorchoiceacceptgrades}}
+                <em>{{#str}} contentitem_multiple_graded , mod_lti, {{{grade_modgrade_point}}} {{/str}}</em>
+            {{/instructorchoiceacceptgrades}}
+        </li>
+    {{/items}}
+</ul>
index 9b6ed76..c75cc2b 100644 (file)
@@ -2,7 +2,7 @@
 Feature: Content-Item support
   In order to easily add activities and content in a course from an external tool
   As a teacher
-  I need to utilise a tool that supports the Content-Item Message type
+  I need to utilise a tool that supports the Deep Linking (Content-Item Message) type
 
   Background:
     Given the following "users" exist:
@@ -16,25 +16,25 @@ Feature: Content-Item support
       | teacher1 | C1 | editingteacher |
     And I log in as "admin"
     And I navigate to "Plugins > Activity modules > External tool > Manage tools" in site administration
-    # Create tool type that supports content-item.
+    # Create tool type that supports deep linking.
     And I follow "configure a tool manually"
     And I set the field "Tool name" to "Teaching Tool 1"
     And I set the field "Tool URL" to local url "/mod/lti/tests/fixtures/tool_provider.php"
     And I set the field "Tool configuration usage" to "Show in activity chooser and as a preconfigured tool"
     And I expand all fieldsets
-    And I set the field "Content-Item Message" to "1"
+    And I set the field "Supports Deep Linking (Content-Item Message)" to "1"
     And I press "Save changes"
     And I log out
 
   @javascript
-  Scenario: Tool that supports Content-Item Message type should be able to configure a tool via the Select content button
+  Scenario: Tool that supports Deep Linking should be able to configure a tool via the Select content button
     When I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
     And I add a "Teaching Tool 1" to section "1"
     Then the "Select content" "button" should be enabled
 
   @javascript
-  Scenario: Editing a tool's settings that was configured from a preconfigured tool that supports Content-Item.
+  Scenario: Editing a tool's settings that was configured from a preconfigured tool that supports Deep Linking.
     When I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
     And I add a "Teaching Tool 1" to section "1"
@@ -67,12 +67,12 @@ Feature: Content-Item support
     And I set the field "Activity name" to "Test tool activity 1"
     And the "Select content" "button" should be disabled
     And the "Tool URL" "field" should be enabled
-    # Selecting a tool that supports content-item: Select content button - enabled, Tool URL - enabled.
+    # Selecting a tool that supports deep linking: Select content button - enabled, Tool URL - enabled.
     And I set the field "Preconfigured tool" to "Teaching Tool 1"
     And I set the field "Activity name" to "Test tool activity 1"
     Then the "Select content" "button" should be enabled
     And the "Tool URL" "field" should be enabled
-    # Selecting a tool that does not support content-item: Select content button - disabled, Tool URL - disabled.
+    # Selecting a tool that does not support deep linking: Select content button - disabled, Tool URL - disabled.
     And I set the field "Preconfigured tool" to "Teaching Tool 2"
     And I set the field "Activity name" to "Test tool activity 1"
     And the "Select content" "button" should be disabled
index 8b40f69..dd4d04b 100644 (file)
@@ -1,8 +1,8 @@
 @mod @mod_lti
-Feature: Create/edit tool configuration that has Content-Item support
-  In order to provide external tools that support the Content-Item Message type for teachers and learners
+Feature: Create/edit tool configuration that has Deep Linking support
+  In order to provide external tools that support Deep Linking for teachers and learners
   As an admin
-  I need to be able to configure external tool registrations that support the Content-Item Message type.
+  I need to be able to configure external tool registrations that support Deep Linking.
 
   Background:
     Given I log in as "admin"
@@ -20,13 +20,13 @@ Feature: Create/edit tool configuration that has Content-Item support
     And I set the field "Tool URL" to local url "/mod/lti/tests/fixtures/tool_provider.php"
     And I set the field "Tool configuration usage" to "Show in activity chooser and as a preconfigured tool"
     And I expand all fieldsets
-    And I set the field "Content-Item Message" to "1"
+    And I set the field "Supports Deep Linking (Content-Item Message)" to "1"
     And I press "Save changes"
     And I follow "Edit"
     And I expand all fieldsets
-    Then the field "Content-Item Message" matches value "1"
-    And I set the field "Content-Item Message" to "0"
+    Then the field "Supports Deep Linking (Content-Item Message)" matches value "1"
+    And I set the field "Supports Deep Linking (Content-Item Message)" to "0"
     And I press "Save changes"
     And I follow "Edit"
     And I expand all fieldsets
-    And the field "Content-Item Message" matches value "0"
+    And the field "Supports Deep Linking (Content-Item Message)" matches value "0"
index 680da40..16cb005 100644 (file)
@@ -377,7 +377,7 @@ class mod_lti_locallib_testcase extends advanced_testcase {
         $this->assertEquals('frame,iframe,window', $params['accept_presentation_document_targets']);
         $this->assertEquals($returnurl->out(false), $params['content_item_return_url']);
         $this->assertEquals('false', $params['accept_unsigned']);
-        $this->assertEquals('false', $params['accept_multiple']);
+        $this->assertEquals('true', $params['accept_multiple']);
         $this->assertEquals('false', $params['accept_copy_advice']);
         $this->assertEquals('false', $params['auto_create']);
         $this->assertEquals($type->name, $params['title']);
@@ -1185,7 +1185,6 @@ MwIDAQAB
      */
     public function test_lti_verify_jwt_signature_no_public_key() {
         $this->resetAfterTest();
-
         $this->setAdminUser();
 
         // Create a tool type, associated with that proxy.
@@ -1214,7 +1213,28 @@ MwIDAQAB
             'url' => 'http://example.com/messages/launch',
             'title' => 'Test title',
             'text' => 'Test text',
-            'frame' => []
+            'iframe' => []
+        ];
+        $contentitems[] = [
+            'type' => 'ltiResourceLink',
+            'url' => 'http://example.com/messages/launch2',
+            'title' => 'Test title2',
+            'text' => 'Test text2',
+            'iframe' => [
+                'height' => 200,
+                'width' => 300
+            ],
+            'window' => []
+        ];
+        $contentitems[] = [
+            'type' => 'ltiResourceLink',
+            'url' => 'http://example.com/messages/launch3',
+            'title' => 'Test title3',
+            'text' => 'Test text3',
+            'window' => [
+                'targetName' => 'test-win',
+                'height' => 400
+            ]
         ];
 
         $contentitems = json_encode($contentitems);
@@ -1231,18 +1251,186 @@ MwIDAQAB
         $objgraph->url = 'http://example.com/messages/launch';
         $objgraph->title = 'Test title';
         $objgraph->text = 'Test text';
-        $objgraph->frame = [];
+        $objgraph->placementAdvice = new stdClass();
+        $objgraph->placementAdvice->presentationDocumentTarget = 'iframe';
         $objgraph->{$strtype} = 'LtiLinkItem';
         $objgraph->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
 
+        $objgraph2 = new stdClass();
+        $objgraph2->url = 'http://example.com/messages/launch2';
+        $objgraph2->title = 'Test title2';
+        $objgraph2->text = 'Test text2';
+        $objgraph2->placementAdvice = new stdClass();
+        $objgraph2->placementAdvice->presentationDocumentTarget = 'iframe';
+        $objgraph2->placementAdvice->displayHeight = 200;
+        $objgraph2->placementAdvice->displayWidth = 300;
+        $objgraph2->{$strtype} = 'LtiLinkItem';
+        $objgraph2->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
+
+        $objgraph3 = new stdClass();
+        $objgraph3->url = 'http://example.com/messages/launch3';
+        $objgraph3->title = 'Test title3';
+        $objgraph3->text = 'Test text3';
+        $objgraph3->placementAdvice = new stdClass();
+        $objgraph3->placementAdvice->presentationDocumentTarget = 'window';
+        $objgraph3->placementAdvice->displayHeight = 400;
+        $objgraph3->placementAdvice->windowTarget = 'test-win';
+        $objgraph3->{$strtype} = 'LtiLinkItem';
+        $objgraph3->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
+
         $expected = new stdClass();
         $expected->{$strcontext} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem';
         $expected->{$strgraph} = [];
         $expected->{$strgraph}[] = $objgraph;
+        $expected->{$strgraph}[] = $objgraph2;
+        $expected->{$strgraph}[] = $objgraph3;
 
         $this->assertEquals($expected, $jsondecode);
     }
 
+    /**
+     * Test adding a single gradable item through content item.
+     */
+    public function test_lti_tool_configuration_from_content_item_single_gradable() {
+        $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);
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti');
+        $contentitems = [];
+        $contentitems[] = [
+            'type' => 'ltiResourceLink',
+            'url' => 'http://example.com/messages/launch',
+            'title' => 'Test title',
+            'lineItem' => [
+                'resourceId' => 'r12345',
+                'tag' => 'final',
+                'scoreMaximum' => 10.0
+            ],
+            '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);
+    }
+
+    /**
+     * Test adding multiple gradable items through content item.
+     */
+    public function test_lti_tool_configuration_from_content_item_multiple() {
+        $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);
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti');
+        $contentitems = [];
+        $contentitems[] = [
+            'type' => 'ltiResourceLink',
+            'url' => 'http://example.com/messages/launch',
+            'title' => 'Test title',
+            'text' => 'Test text',
+            'icon' => [
+                'url' => 'http://lti.example.com/image.jpg',
+                'width' => 100
+            ],
+            'frame' => []
+        ];
+        $contentitems[] = [
+            'type' => 'ltiResourceLink',
+            'url' => 'http://example.com/messages/launchgraded',
+            'title' => 'Test Graded',
+            'lineItem' => [
+                'resourceId' => 'r12345',
+                'tag' => 'final',
+                'scoreMaximum' => 10.0
+            ],
+            '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->assertNotNull($config->multiple);
+        $this->assertEquals(2, count( $config->multiple ));
+        $this->assertEquals($contentitems[0]['title'], $config->multiple[0]->name);
+        $this->assertEquals($contentitems[0]['url'], $config->multiple[0]->toolurl);
+        $this->assertEquals(LTI_SETTING_NEVER, $config->multiple[0]->instructorchoiceacceptgrades);
+        $this->assertEquals($contentitems[1]['url'], $config->multiple[1]->toolurl);
+        $this->assertEquals(LTI_SETTING_ALWAYS, $config->multiple[1]->instructorchoiceacceptgrades);
+        $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);
+    }
+
+    /**
+     * Test adding a single non gradable item through content item.
+     */
+    public function test_lti_tool_configuration_from_content_item_single() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $type = new stdClass();
+        $type->name = "Test tool";
+        $type->baseurl = "http://example.com";
+        $config = new stdClass();
+        $typeid = lti_add_type($type, $config);
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti');
+        $contentitems = [];
+        $contentitems[] = [
+            'type' => 'ltiResourceLink',
+            'url' => 'http://example.com/messages/launch',
+            'title' => 'Test title',
+            'text' => 'Test text',
+            'icon' => [
+                'url' => 'http://lti.example.com/image.jpg',
+                'width' => 100
+            ],
+            '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]['title'], $config->name);
+        $this->assertEquals($contentitems[0]['text'], $config->introeditor['text']);
+        $this->assertEquals($contentitems[0]['url'], $config->toolurl);
+        $this->assertEquals($contentitems[0]['icon']['url'], $config->icon);
+        $this->assertEquals(LTI_SETTING_NEVER, $config->instructorchoiceacceptgrades);
+
+    }
+
     /**
      * Test lti_sign_jwt().
      */
index 4e8e5b9..a3c01f4 100644 (file)
@@ -1,5 +1,10 @@
 This files describes API changes in the lti code.
 
+=== 3.10 ===
+
+* Select Content supports multiple, allowing a tool to return more than one link at a time.
+  Parameter multiple in function lti_build_content_item_selection_request() is now set to true.
+
 === 3.8 ===
 
 * The following functions have been finally deprecated and can not be used anymore: