MDL-67585 core_course: add hook get_all_content_items
authorJake Dallimore <jake@moodle.com>
Wed, 29 Jan 2020 03:52:46 +0000 (11:52 +0800)
committerJake Dallimore <jake@moodle.com>
Thu, 20 Feb 2020 03:42:22 +0000 (11:42 +0800)
Returns all content items which are provided by the plugin, irrespective
of whether or not a user can see an item in a particular course. This is
used to generate a global list of content items, allowing for admin
level features to be added.

course/classes/local/repository/caching_content_item_readonly_repository.php
course/classes/local/repository/content_item_readonly_repository.php
course/classes/local/repository/content_item_readonly_repository_interface.php
course/classes/local/service/content_item_service.php
course/tests/content_item_readonly_repository_test.php
course/tests/services_content_item_service_test.php
mod/lti/lib.php
mod/lti/tests/lib_test.php

index a5066b9..51c93fb 100644 (file)
@@ -76,4 +76,13 @@ class caching_content_item_readonly_repository implements content_item_readonly_
         $this->cachestore->set($key, $contentitems);
         return $contentitems;
     }
+
+    /**
+     * Find all the content items made available by core and plugins.
+     *
+     * @return array
+     */
+    public function find_all(): array {
+        return $this->contentitemrepository->find_all();
+    }
 }
index 8befe48..b92e4fe 100644 (file)
@@ -141,6 +141,28 @@ class content_item_readonly_repository implements content_item_readonly_reposito
         return $contentitems;
     }
 
+    /**
+     * Get all the content items for a subplugin.
+     *
+     * @param string $parentpluginname
+     * @param content_item $modulecontentitem
+     * @return array
+     */
+    private function get_subplugin_all_content_items(string $parentpluginname, content_item $modulecontentitem): 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_all_content_items', [$modulecontentitem], 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.
      *
@@ -155,6 +177,60 @@ class content_item_readonly_repository implements content_item_readonly_reposito
         return $legacyitem;
     }
 
+    /**
+     * Find all the available content items, not restricted to course or user.
+     *
+     * @return array the array of content items.
+     */
+    public function find_all(): array {
+        global $OUTPUT, $DB;
+
+        // Get all modules so we know which plugins are enabled and able to add content.
+        // Only module plugins may add content items.
+        $modules = $DB->get_records('modules', ['visible' => 1]);
+        $return = [];
+
+        // Now, generate the content_items.
+        foreach ($modules as $modid => $mod) {
+            // Create the content item for the module itself.
+            // If the module chooses to implement the hook, this may be thrown away.
+            $help = $this->get_core_module_help_string($mod->name);
+            $archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
+
+            $contentitem = new content_item(
+                $mod->id,
+                $mod->name,
+                new lang_string_title("modulename", $mod->name),
+                new \moodle_url(''), // No course scope, so just an empty link.
+                $OUTPUT->pix_icon('icon', '', $mod->name, ['class' => 'icon']),
+                $help,
+                $archetype,
+                'mod_' . $mod->name
+            );
+
+            $modcontentitemreference = clone($contentitem);
+
+            if (component_callback_exists('mod_' . $mod->name, 'get_all_content_items')) {
+                // Call the module hooks for this module.
+                $plugincontentitems = component_callback('mod_' . $mod->name, 'get_all_content_items',
+                    [$modcontentitemreference], []);
+                if (!empty($plugincontentitems)) {
+                    array_push($return, ...$plugincontentitems);
+                }
+
+                // Now, get those for subplugins of the module.
+                $subplugincontentitems = $this->get_subplugin_all_content_items('mod_' . $mod->name, $modcontentitemreference);
+                if (!empty($subplugincontentitems)) {
+                    array_push($return, ...$subplugincontentitems);
+                }
+            } else {
+                // Neither callback was found, so just use the default module content item.
+                $return[] = $contentitem;
+            }
+        }
+        return $return;
+    }
+
     /**
      * Get the list of potential content items for the given course.
      *
index 4c023ca..d33e85b 100644 (file)
@@ -38,4 +38,11 @@ interface content_item_readonly_repository_interface {
      * @return array the array of content items.
      */
     public function find_all_for_course(\stdClass $course, \stdClass $user): array;
+
+    /**
+     * Find all content items that can be presented, irrespective of course.
+     *
+     * @return array the array of content items.
+     */
+    public function find_all(): array;
 }
index 8bc930b..415d5e4 100644 (file)
@@ -49,6 +49,30 @@ class content_item_service {
         $this->repository = $repository;
     }
 
+    /**
+     * Get all content items which may be added to courses, irrespective of course caps, for site admin views, etc.
+     *
+     * @return array the array of exported content items.
+     */
+    public function get_all_content_items(): array {
+        global $PAGE;
+        $allcontentitems = $this->repository->find_all();
+
+        // Export the objects to get the formatted objects for transfer/display.
+        $ciexporter = new course_content_items_exporter(
+            $allcontentitems,
+            ['context' => \context_system::instance()]
+        );
+        $exported = $ciexporter->export($PAGE->get_renderer('core'));
+
+        // Sort by title for return.
+        usort($exported->content_items, function($a, $b) {
+            return $a->title > $b->title;
+        });
+
+        return $exported->content_items;
+    }
+
     /**
      * Return a representation of the available content items, for a user in a course.
      *
index 02bbd88..d8e7998 100644 (file)
@@ -72,4 +72,33 @@ class content_item_readonly_repository_testcase extends \advanced_testcase {
         $items = $cir->find_all_for_course($course, $user);
         $this->assertArrayNotHasKey($module->name, $items);
     }
+
+    /**
+     * Test confirming that all content items can be fetched, even those which require certain caps when in a course.
+     */
+    public function test_find_all() {
+        $this->resetAfterTest();
+        global $DB;
+
+        // We'll compare our results to those which are course-specific.
+        $course = $this->getDataGenerator()->create_course();
+        $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+        assign_capability('mod/lti:addmanualinstance', CAP_PROHIBIT, $teacherrole->id, \context_course::instance($course->id));
+        $cir = new content_item_readonly_repository();
+
+        // Course specific - lti won't be returned as the user doesn't have the required cap.
+        $forcourse = $cir->find_all_for_course($course, $user);
+        $forcourse = array_filter($forcourse, function($contentitem) {
+            return $contentitem->get_name() === 'lti';
+        });
+        $this->assertEmpty($forcourse);
+
+        // All - all items are returned, including lti.
+        $all = $cir->find_all();
+        $all = array_filter($all, function($contentitem) {
+            return $contentitem->get_name() === 'lti';
+        });
+        $this->assertCount(1, $all);
+    }
 }
index eb9ff72..ecce849 100644 (file)
@@ -103,4 +103,34 @@ class services_content_item_service_testcase extends \advanced_testcase {
             $this->assertStringContainsString('sr=7', $item->link);
         }
     }
+
+    /**
+     * Test confirming that all content items can be fetched irrespective of permissions.
+     */
+    public function test_get_all_content_items() {
+        $this->resetAfterTest();
+        global $DB;
+
+        // Create a user in a course.
+        $course = $this->getDataGenerator()->create_course();
+        $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+
+        $cis = new content_item_service(new content_item_readonly_repository());
+        $allcontentitems = $cis->get_all_content_items();
+        $coursecontentitems = $cis->get_content_items_for_user_in_course($user, $course);
+
+        // The call to get_all_content_items() should return the same items as for the course,
+        // given the user in an editing teacher and can add manual lti instances.
+        $this->assertEquals(array_column($allcontentitems, 'name'), array_column($coursecontentitems, 'name'));
+
+        // Now removing the cap 'mod/lti:addinstance'. This will restrict those items returned by the course-specific method.
+        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+        assign_capability('mod/lti:addinstance', CAP_PROHIBIT, $teacherrole->id, \context_course::instance($course->id));
+
+        // Verify that all items, including lti, are still returned by the get_all_content_items() call.
+        $allcontentitems = $cis->get_all_content_items();
+        $coursecontentitems = $cis->get_content_items_for_user_in_course($user, $course);
+        $this->assertNotContains('lti', array_column($coursecontentitems, 'name'));
+        $this->assertContains('lti', array_column($allcontentitems, 'name'));
+    }
 }
index 5029c9b..833be56 100644 (file)
@@ -295,6 +295,67 @@ function lti_get_course_content_items(\core_course\local\entity\content_item $de
     return $types;
 }
 
+/**
+ * Return all content items which can be added to any course.
+ *
+ * @param \core_course\local\entity\content_item $defaultmodulecontentitem
+ * @return array the array of content items.
+ */
+function mod_lti_get_all_content_items(\core_course\local\entity\content_item $defaultmodulecontentitem): array {
+    global $OUTPUT, $CFG;
+    require_once($CFG->dirroot . '/mod/lti/locallib.php'); // For access to constants.
+
+    // The 'External tool' entry (the main module content item), should always take the id of 1.
+    $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()
+    )];
+
+    foreach (lti_get_lti_types() as $ltitype) {
+        if ($ltitype->coursevisible != LTI_COURSEVISIBLE_ACTIVITYCHOOSER) {
+            continue;
+        }
+        $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.
+        $type->title    = clean_param($ltitype->name, PARAM_NOTAGS);
+        $trimmeddescription = trim($ltitype->description);
+        $type->help = '';
+        if ($trimmeddescription != '') {
+            // Clean the description. We don't want tags here.
+            $type->help     = clean_param($trimmeddescription, PARAM_NOTAGS);
+            $type->helplink = get_string('modulename_shortcut_link', 'lti');
+        }
+        if (empty($ltitype->icon)) {
+            $type->icon = $OUTPUT->pix_icon('icon', '', 'lti', array('class' => 'icon'));
+        } else {
+            $type->icon = html_writer::empty_tag('img', array('src' => $ltitype->icon, 'alt' => $ltitype->name, 'class' => 'icon'));
+        }
+        $type->link = new moodle_url('/course/modedit.php', array('add' => 'lti', 'return' => 0, 'typeid' => $ltitype->id));
+
+        $types[] = new \core_course\local\entity\content_item(
+            $type->id + 1,
+            $type->name,
+            new \core_course\local\entity\string_title($type->title),
+            $type->link,
+            $type->icon,
+            $type->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 6f85ad9..a570734 100644 (file)
@@ -437,5 +437,20 @@ class mod_lti_lib_testcase extends advanced_testcase {
         $this->assertContains($sitetoolrecord->id + 1, $ids);
         $this->assertContains($course2toolrecord->id + 1, $ids);
         $this->assertNotContains($sitetoolrecordnonchooser->id + 1, $ids);
+
+        // When fetching all content items, we expect to see all items available in activity choosers (in any course),
+        // plus the default module content item ('external tool').
+        $this->setAdminUser();
+        $allitems = mod_lti_get_all_content_items($defaultmodulecontentitem);
+        $this->assertCount(4, $allitems);
+        $ids = [];
+        foreach ($allitems as $item) {
+            $ids[] = $item->get_id();
+        }
+        $this->assertContains(1, $ids);
+        $this->assertContains($sitetoolrecord->id + 1, $ids);
+        $this->assertContains($course1toolrecord->id + 1, $ids);
+        $this->assertContains($course2toolrecord->id + 1, $ids);
+        $this->assertNotContains($sitetoolrecordnonchooser->id + 1, $ids);
     }
 }