MDL-67585 core_course: add get_course_content_items hook
authorJake Dallimore <jake@moodle.com>
Thu, 23 Jan 2020 07:45:34 +0000 (15:45 +0800)
committerJake Dallimore <jake@moodle.com>
Thu, 20 Feb 2020 01:28:57 +0000 (09:28 +0800)
Plugins use this to report which content items relate to a user in
a course.

course/classes/local/repository/content_item_readonly_repository.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/tests/lib_test.php

index ed729f8..9140de2 100644 (file)
@@ -66,11 +66,21 @@ class content_item_readonly_repository implements content_item_readonly_reposito
      * @return content_item a content item object.
      */
     private function content_item_from_legacy_data(\stdClass $item): content_item {
+        global $OUTPUT;
+
         // Make sure the legacy data results in a content_item with id = 0.
         // Even with an id, we can't uniquely identify the item, because we can't guarantee what component it came from.
         // An id of -1, signifies this.
         $item->id = -1;
 
+        // If the module provides the helplink property, append it to the help text to match the look and feel
+        // of the default course modules.
+        if (isset($item->help) && isset($item->helplink)) {
+            $linktext = get_string('morehelp');
+            $item->help .= \html_writer::tag('div',
+                $OUTPUT->doc_link($item->helplink, $linktext, true), ['class' => 'helpdoclink']);
+        }
+
         if (is_string($item->title)) {
             $item->title = new string_title($item->title);
         } else if ($item->title instanceof \lang_string) {
@@ -106,6 +116,45 @@ class content_item_readonly_repository implements content_item_readonly_reposito
         return $item;
     }
 
+    /**
+     * Helper to get the contentitems from all subplugin hooks for a given module plugin.
+     *
+     * @param string $parentpluginname the name of the module plugin to check subplugins for.
+     * @param content_item $modulecontentitem the content item of the module plugin, to pass to the hooks.
+     * @param \stdClass $user the user object to pass to subplugins.
+     * @return array the array of content items.
+     */
+    private function get_subplugin_course_content_items(string $parentpluginname, content_item $modulecontentitem,
+            \stdClass $user): array {
+
+        $contentitems = [];
+        $pluginmanager = \core_plugin_manager::instance();
+        foreach ($pluginmanager->get_subplugins_of_plugin($parentpluginname) as $subpluginname => $subplugin) {
+            // Call the hook, but with a copy of the module content item data.
+            $spcontentitems = component_callback($subpluginname, 'get_course_content_items', [$modulecontentitem, $user], null);
+            if (!is_null($spcontentitems)) {
+                foreach ($spcontentitems as $spcontentitem) {
+                    $contentitems[] = $spcontentitem;
+                }
+            }
+        }
+        return $contentitems;
+    }
+
+    /**
+     * Helper to make sure any legacy items have certain properties, which, if missing are inherited from the parent module item.
+     *
+     * @param \stdClass $legacyitem the legacy information, a stdClass coming from get_shortcuts() hook.
+     * @param content_item $modulecontentitem The module's content item information, to inherit if needed.
+     * @return \stdClass the updated legacy item stdClass
+     */
+    private function legacy_item_inherit_missing(\stdClass $legacyitem, content_item $modulecontentitem): \stdClass {
+        // Fall back to the plugin parent value if the subtype didn't provide anything.
+        $legacyitem->archetype = $legacyitem->archetype ?? $modulecontentitem->get_archetype();
+        $legacyitem->icon = $legacyitem->icon ?? $modulecontentitem->get_icon();
+        return $legacyitem;
+    }
+
     /**
      * Get the list of potential content items for the given course.
      *
@@ -143,41 +192,54 @@ class content_item_readonly_repository implements content_item_readonly_reposito
                 'mod_' . $mod->name
             );
 
-            // Next step is to get the dynamically generated content items for ecah module, if provided.
-            // This is achieved by implementation of the hook, 'get_shortcuts'.
-
-            // Give each plugin implementing the hook the main entry for REFERENCE ONLY.
-            // The current hook, get_shortcuts, expects a stdClass representation of the core module content_item entry.
-            $modcontentitemreference = $this->content_item_to_legacy_data($contentitem);
-
-            // Next, get the content_items from the module callback, if implemented.
-            $items = component_callback($mod->name, 'get_shortcuts', [$modcontentitemreference], null);
-            if (!is_null($items)) {
-                foreach ($items as $item) {
-                    // Fall back to the plugin parent value if the subtype didn't provide anything.
-                    $item->archetype = $item->archetype ?? $contentitem->get_archetype();
-                    $item->icon = $item->icon ?? $contentitem->get_icon();
-
-                    // If the module provides the helplink property, append it to the help text to match the look and feel
-                    // of the default course modules.
-                    if (isset($item->help) && isset($item->helplink)) {
-                        $linktext = get_string('morehelp');
-                        $item->help .= \html_writer::tag('div',
-                            $OUTPUT->doc_link($item->helplink, $linktext, true), ['class' => 'helpdoclink']);
-                    }
+            // Legacy vs new hooks.
+            // If the new hook is found for a module plugin, use that path (calling mod plugins and their subplugins directly)
+            // If not, check the legacy hook. This won't provide us with enough information to identify items uniquely within their
+            // component (lti + lti source being an example), but we can still list these items.
+            $modcontentitemreference = clone($contentitem);
+
+            if (component_callback_exists('mod_' . $mod->name, 'get_course_content_items')) {
+                // Call the module hooks for this module.
+                $plugincontentitems = component_callback('mod_' . $mod->name, 'get_course_content_items',
+                    [$modcontentitemreference, $user, $course], []);
+                if (!empty($plugincontentitems)) {
+                    array_push($return, ...$plugincontentitems);
+                }
 
-                    // Create a content_item instance from the legacy callback data.
-                    $plugincontentitem = $this->content_item_from_legacy_data($item);
-                    $return[] = $plugincontentitem;
+                // Now, get those for subplugins of the module.
+                $subpluginitems = $this->get_subplugin_course_content_items('mod_' . $mod->name, $modcontentitemreference, $user);
+                if (!empty($subpluginitems)) {
+                    array_push($return, ...$subpluginitems);
                 }
 
+            } else if (component_callback_exists('mod_' . $mod->name, 'get_shortcuts')) {
                 // If get_shortcuts() callback is defined, the default module action is not added.
                 // It is a responsibility of the callback to add it to the return value unless it is not needed.
-                continue;
-            }
+                // The legacy hook, get_shortcuts, expects a stdClass representation of the core module content_item entry.
+                $modcontentitemreference = $this->content_item_to_legacy_data($contentitem);
+
+                $legacyitems = component_callback($mod->name, 'get_shortcuts', [$modcontentitemreference], null);
+                if (!is_null($legacyitems)) {
+                    foreach ($legacyitems as $legacyitem) {
 
-            // The callback get_shortcuts() was not found, use the default item for the activity chooser.
-            $return[] = $contentitem;
+                        $legacyitem = $this->legacy_item_inherit_missing($legacyitem, $contentitem);
+
+                        // All items must have different links, use them as a key in the return array.
+                        // If plugin returned the only one item with the same link as default item - keep $modname,
+                        // otherwise append the link url to the module name.
+                        $legacyitem->name = (count($legacyitems) == 1 &&
+                            $legacyitem->link->out() === $contentitem->get_link()->out()) ? $mod->name : $mod->name . ':' .
+                                $legacyitem->link;
+
+                        $plugincontentitem = $this->content_item_from_legacy_data($legacyitem);
+
+                        $return[] = $plugincontentitem;
+                    }
+                }
+            } else {
+                // Neither callback was found, so just use the default module content item.
+                $return[] = $contentitem;
+            }
         }
 
         return $return;
index e450cd6..e4e211d 100644 (file)
@@ -235,6 +235,64 @@ function lti_get_shortcuts($defaultitem) {
     return $types;
 }
 
+/**
+ * Return the preconfigured tools which are configured for inclusion in the activity picker.
+ *
+ * @param \core_course\local\entity\content_item $defaultmodulecontentitem reference to the content item for the LTI module.
+ * @param \stdClass $user the user object, to use for cap checks if desired.
+ * @param stdClass $course the course to scope items to.
+ * @return array the array of content items.
+ */
+function lti_get_course_content_items(\core_course\local\entity\content_item $defaultmodulecontentitem, \stdClass $user,
+        \stdClass $course) {
+    global $CFG, $OUTPUT;
+    require_once($CFG->dirroot.'/mod/lti/locallib.php');
+
+    $types = [];
+
+    // The 'External tool' entry (the main module content item), should always take the id of 1.
+    if (has_capability('mod/lti:addmanualinstance', context_course::instance($course->id), $user)) {
+        $types = [new \core_course\local\entity\content_item(
+            1,
+            $defaultmodulecontentitem->get_name(),
+            $defaultmodulecontentitem->get_title(),
+            $defaultmodulecontentitem->get_link(),
+            $defaultmodulecontentitem->get_icon(),
+            $defaultmodulecontentitem->get_help(),
+            $defaultmodulecontentitem->get_archetype(),
+            $defaultmodulecontentitem->get_component_name()
+        )];
+    }
+
+    // Other, preconfigured tools take their own id + 1, so we'll never clash with the module's entry.
+    $preconfiguredtools = lti_get_configured_types($course->id, $defaultmodulecontentitem->get_link()->param('sr'));
+    foreach ($preconfiguredtools as $preconfiguredtool) {
+
+        // Append the help link to the help text.
+        if (isset($preconfiguredtool->help)) {
+            if (isset($preconfiguredtool->helplink)) {
+                $linktext = get_string('morehelp');
+                $preconfiguredtool->help .= html_writer::tag('div',
+                    $OUTPUT->doc_link($preconfiguredtool->helplink, $linktext, true), ['class' => 'helpdoclink']);
+            }
+        } else {
+            $preconfiguredtool->help = '';
+        }
+
+        $types[] = new \core_course\local\entity\content_item(
+            $preconfiguredtool->id + 1,
+            $preconfiguredtool->name,
+            new \core_course\local\entity\string_title($preconfiguredtool->title),
+            $preconfiguredtool->link,
+            $preconfiguredtool->icon,
+            $preconfiguredtool->help,
+            $defaultmodulecontentitem->get_archetype(),
+            $defaultmodulecontentitem->get_component_name()
+        );
+    }
+    return $types;
+}
+
 /**
  * Given a coursemodule object, this function returns the extra
  * information needed to print this activity in various places.
index c69d3ab..6fff3c1 100644 (file)
@@ -2185,6 +2185,7 @@ function lti_get_configured_types($courseid, $sectionreturn = 0) {
 
     foreach ($admintypes as $ltitype) {
         $type           = new stdClass();
+        $type->id       = $ltitype->id;
         $type->modclass = MOD_CLASS_ACTIVITY;
         $type->name     = 'lti_type_' . $ltitype->id;
         // Clean the name. We don't want tags here.
index 3db751d..6f85ad9 100644 (file)
@@ -327,4 +327,115 @@ class mod_lti_lib_testcase extends advanced_testcase {
 
         return calendar_event::create($event);
     }
+
+    /**
+     * Test verifying the output of the lti_get_course_content_items and lti_get_all_content_items callbacks.
+     */
+    public function test_content_item_callbacks() {
+        $this->resetAfterTest();
+        global $DB, $CFG;
+        require_once($CFG->dirroot . '/mod/lti/locallib.php');
+
+        $admin = get_admin();
+        $time = time();
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $course2 = $this->getDataGenerator()->create_course();
+        $teacher2 = $this->getDataGenerator()->create_and_enrol($course2, 'editingteacher');
+
+        // Create some preconfigured tools.
+        $sitetoolrecord = (object) [
+            'name' => 'Site level tool which is available in the activity chooser',
+            'baseurl' => 'http://example.com',
+            'createdby' => $admin->id,
+            'course' => SITEID,
+            'ltiversion' => 'LTI-1p0',
+            'timecreated' => $time,
+            'timemodified' => $time,
+            'state' => LTI_TOOL_STATE_CONFIGURED,
+            'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER
+        ];
+        $sitetoolrecordnonchooser = (object) [
+            'name' => 'Site level tool which is NOT available in the course activity chooser',
+            'baseurl' => 'http://example2.com',
+            'createdby' => $admin->id,
+            'course' => SITEID,
+            'ltiversion' => 'LTI-1p0',
+            'timecreated' => $time,
+            'timemodified' => $time,
+            'state' => LTI_TOOL_STATE_CONFIGURED,
+            'coursevisible' => LTI_COURSEVISIBLE_PRECONFIGURED
+        ];
+        $course1toolrecord = (object) [
+            'name' => 'Course created tool which is available in the activity chooser',
+            'baseurl' => 'http://example3.com',
+            'createdby' => $teacher->id,
+            'course' => $course->id,
+            'ltiversion' => 'LTI-1p0',
+            'timecreated' => $time,
+            'timemodified' => $time,
+            'state' => LTI_TOOL_STATE_CONFIGURED,
+            'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER
+        ];
+        $course2toolrecord = (object) [
+            'name' => 'Course created tool which is available in the activity chooser',
+            'baseurl' => 'http://example4.com',
+            'createdby' => $teacher2->id,
+            'course' => $course2->id,
+            'ltiversion' => 'LTI-1p0',
+            'timecreated' => $time,
+            'timemodified' => $time,
+            'state' => LTI_TOOL_STATE_CONFIGURED,
+            'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER
+        ];
+        $tool1id = $DB->insert_record('lti_types', $sitetoolrecord);
+        $tool2id = $DB->insert_record('lti_types', $sitetoolrecordnonchooser);
+        $tool3id = $DB->insert_record('lti_types', $course1toolrecord);
+        $tool4id = $DB->insert_record('lti_types', $course2toolrecord);
+        $sitetoolrecord->id = $tool1id;
+        $sitetoolrecordnonchooser->id = $tool2id;
+        $course1toolrecord->id = $tool3id;
+        $course2toolrecord->id = $tool4id;
+
+        $defaultmodulecontentitem = new \core_course\local\entity\content_item(
+            '1',
+            'default module content item',
+            new \core_course\local\entity\string_title('Content item title'),
+            new moodle_url(''),
+            'icon',
+            'Description of the module',
+            MOD_ARCHETYPE_OTHER,
+            'mod_lti'
+        );
+
+        // The lti_get_lti_types_by_course method (used by the callbacks) assumes the global user.
+        $this->setUser($teacher);
+
+        // Teacher in course1 should be able to see the default module item ('external tool'),
+        // the site preconfigured tool and the tool created in course1.
+        $courseitems = lti_get_course_content_items($defaultmodulecontentitem, $teacher, $course);
+        $this->assertCount(3, $courseitems);
+        $ids = [];
+        foreach ($courseitems as $item) {
+            $ids[] = $item->get_id();
+        }
+        $this->assertContains(1, $ids);
+        $this->assertContains($sitetoolrecord->id + 1, $ids);
+        $this->assertContains($course1toolrecord->id + 1, $ids);
+        $this->assertNotContains($sitetoolrecordnonchooser->id + 1, $ids);
+
+        // The content items for teacher2 in course2 include the default module content item ('external tool'),
+        // the site preconfigured tool and the tool created in course2.
+        $this->setUser($teacher2);
+        $course2items = lti_get_course_content_items($defaultmodulecontentitem, $teacher2, $course2);
+        $this->assertCount(3, $course2items);
+        $ids = [];
+        foreach ($course2items as $item) {
+            $ids[] = $item->get_id();
+        }
+        $this->assertContains(1, $ids);
+        $this->assertContains($sitetoolrecord->id + 1, $ids);
+        $this->assertContains($course2toolrecord->id + 1, $ids);
+        $this->assertNotContains($sitetoolrecordnonchooser->id + 1, $ids);
+    }
 }