MDL-67585 core_course: add favouriting to the content item service class
authorJake Dallimore <jake@moodle.com>
Wed, 29 Jan 2020 07:37:33 +0000 (15:37 +0800)
committerJake Dallimore <jake@moodle.com>
Thu, 20 Feb 2020 03:42:22 +0000 (11:42 +0800)
course/classes/local/exporters/course_content_item_exporter.php
course/classes/local/exporters/course_content_items_exporter.php
course/classes/local/service/content_item_service.php
course/tests/services_content_item_service_test.php
lang/en/cache.php
lib/db/caches.php
version.php

index 855a2eb..cf8d77c 100644 (file)
@@ -49,7 +49,7 @@ class course_content_item_exporter extends exporter {
     public function __construct(content_item $contentitem, array $related = []) {
         $this->contentitem = $contentitem;
 
-        return parent::__construct($contentitem, $related);
+        return parent::__construct([], $related);
     }
 
     /**
@@ -77,7 +77,7 @@ class course_content_item_exporter extends exporter {
      */
     protected static function define_other_properties() {
         // This will hold user-dependant properties such as whether the item is starred or recommended.
-        return [];
+        return ['favourite' => ['type' => PARAM_BOOL, 'description' => 'Has the user favourited the content item']];
     }
 
     /**
@@ -96,6 +96,17 @@ class course_content_item_exporter extends exporter {
      * @return array The array of property values, indexed by name.
      */
     protected function get_other_values(\renderer_base $output) {
+
+        $favourite = false;
+        $itemtype = 'contentitem_' . $this->contentitem->get_component_name();
+        if (isset($this->related['favouriteitems'])) {
+            foreach ($this->related['favouriteitems'] as $favobj) {
+                if ($favobj->itemtype === $itemtype && in_array($this->contentitem->get_id(), $favobj->ids)) {
+                    $favourite = true;
+                }
+            }
+        }
+
         $properties = [
             'id' => $this->contentitem->get_id(),
             'name' => $this->contentitem->get_name(),
@@ -105,6 +116,7 @@ class course_content_item_exporter extends exporter {
             'help' => $this->contentitem->get_help(),
             'archetype' => $this->contentitem->get_archetype(),
             'componentname' => $this->contentitem->get_component_name(),
+            'favourite' => $favourite
         ];
 
         return $properties;
@@ -117,7 +129,8 @@ class course_content_item_exporter extends exporter {
      */
     protected static function define_related(): array {
         return [
-            'context' => 'context'
+            'context' => '\context',
+            'favouriteitems' => '\stdClass[]?'
         ];
     }
 }
index 177e49f..e468a7c 100644 (file)
@@ -49,7 +49,7 @@ class course_content_items_exporter extends exporter {
     public function __construct(array $contentitems, array $related) {
         $this->contentitems = $contentitems;
 
-        parent::__construct($contentitems, $related);
+        parent::__construct([], $related);
     }
 
     /**
@@ -78,7 +78,8 @@ class course_content_items_exporter extends exporter {
             $exporter = new course_content_item_exporter(
                 $contentitem,
                 [
-                    'context' => $this->related['context']
+                    'context' => $this->related['context'],
+                    'favouriteitems' => $this->related['favouriteitems'],
                 ]
             );
             return $exporter->export($output);
@@ -98,7 +99,8 @@ class course_content_items_exporter extends exporter {
      */
     protected static function define_related() {
         return [
-            'context' => '\context'
+            'context' => '\context',
+            'favouriteitems' => '\stdClass[]?'
         ];
     }
 }
index 415d5e4..5505dcf 100644 (file)
@@ -49,19 +49,75 @@ class content_item_service {
         $this->repository = $repository;
     }
 
+    /**
+     * Returns an array of objects representing favourited content items.
+     *
+     * Each object contains the following properties:
+     * itemtype: a string containing the 'itemtype' key used by the favourites subsystem.
+     * ids[]: an array of ids, representing the content items within a component.
+     *
+     * Since two components can return (via their hook implementation) the same id, the itemtype is used for uniqueness.
+     *
+     * @param \stdClass $user
+     * @return array
+     */
+    private function get_favourite_content_items_for_user(\stdClass $user): array {
+        $favcache = \cache::make('core', 'user_favourite_course_content_items');
+        $key = $user->id;
+        $favmods = $favcache->get($key);
+        if ($favmods !== false) {
+            return $favmods;
+        }
+
+        // Get all modules and any submodules which implement get_course_content_items() hook.
+        // This gives us the set of all itemtypes which we'll use to register favourite content items.
+        // The ids that each plugin returns will be used together with the itemtype to uniquely identify
+        // each content item for favouriting.
+        $pluginmanager = \core_plugin_manager::instance();
+        $plugins = $pluginmanager->get_plugins_of_type('mod');
+        $itemtypes = [];
+        foreach ($plugins as $plugin) {
+            // Add the mod itself.
+            $itemtypes[] = 'contentitem_mod_' . $plugin->name;
+
+            // Add any subplugins to the list of item types.
+            $subplugins = $pluginmanager->get_subplugins_of_plugin('mod_' . $plugin->name);
+            foreach ($subplugins as $subpluginname => $subplugininfo) {
+                if (component_callback_exists($subpluginname, 'get_course_content_items')) {
+                    $itemtypes[] = 'contentitem_' . $subpluginname;
+                }
+            }
+        }
+
+        $ufservice = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($user->id));
+        $favourites = [];
+        foreach ($itemtypes as $itemtype) {
+            $favs = $ufservice->find_favourites_by_type('core_course', $itemtype);
+            $favobj = (object) ['itemtype' => $itemtype, 'ids' => array_column($favs, 'itemid')];
+            $favourites[] = $favobj;
+        }
+        $favcache->set($key, $favourites);
+        return $favourites;
+    }
+
     /**
      * Get all content items which may be added to courses, irrespective of course caps, for site admin views, etc.
      *
+     * @param \stdClass $user the user object.
      * @return array the array of exported content items.
      */
-    public function get_all_content_items(): array {
+    public function get_all_content_items(\stdClass $user): array {
         global $PAGE;
         $allcontentitems = $this->repository->find_all();
 
         // Export the objects to get the formatted objects for transfer/display.
+        $favourites = $this->get_favourite_content_items_for_user($user);
         $ciexporter = new course_content_items_exporter(
             $allcontentitems,
-            ['context' => \context_system::instance()]
+            [
+                'context' => \context_system::instance(),
+                'favouriteitems' => $favourites
+            ]
         );
         $exported = $ciexporter->export($PAGE->get_renderer('core'));
 
@@ -129,9 +185,13 @@ class content_item_service {
         }
 
         // Export the objects to get the formatted objects for transfer/display.
+        $favourites = $this->get_favourite_content_items_for_user($user);
         $ciexporter = new course_content_items_exporter(
             $availablecontentitems,
-            ['context' => \context_course::instance($course->id)]
+            [
+                'context' => \context_course::instance($course->id),
+                'favouriteitems' => $favourites
+            ]
         );
         $exported = $ciexporter->export($PAGE->get_renderer('course'));
 
@@ -142,4 +202,54 @@ class content_item_service {
 
         return $exported->content_items;
     }
+
+    /**
+     * Add a content item to a user's favourites.
+     *
+     * @param \stdClass $user the user whose favourite this is.
+     * @param string $componentname the name of the component from which the content item originates.
+     * @param int $contentitemid the id of the content item.
+     * @return \stdClass the exported content item.
+     */
+    public function add_to_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass {
+        $usercontext = \context_user::instance($user->id);
+        $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
+
+        // Because each plugin decides its own ids for content items, a combination of
+        // itemtype and id is used to guarantee uniqueness across all content items.
+        $itemtype = 'contentitem_' . $componentname;
+
+        $ufservice->create_favourite('core_course', $itemtype, $contentitemid, $usercontext);
+
+        $favcache = \cache::make('core', 'user_favourite_course_content_items');
+        $favcache->delete($user->id);
+
+        $items = $this->get_all_content_items($user);
+        return $items[array_search($contentitemid, array_column($items, 'id'))];
+    }
+
+    /**
+     * Remove the content item from a user's favourites.
+     *
+     * @param \stdClass $user the user whose favourite this is.
+     * @param string $componentname the name of the component from which the content item originates.
+     * @param int $contentitemid the id of the content item.
+     * @return \stdClass the exported content item.
+     */
+    public function remove_from_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass {
+        $usercontext = \context_user::instance($user->id);
+        $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
+
+        // Because each plugin decides its own ids for content items, a combination of
+        // itemtype and id is used to guarantee uniqueness across all content items.
+        $itemtype = 'contentitem_' . $componentname;
+
+        $ufservice->delete_favourite('core_course', $itemtype, $contentitemid, $usercontext);
+
+        $favcache = \cache::make('core', 'user_favourite_course_content_items');
+        $favcache->delete($user->id);
+
+        $items = $this->get_all_content_items($user);
+        return $items[array_search($contentitemid, array_column($items, 'id'))];
+    }
 }
index ecce849..2e7d2a4 100644 (file)
@@ -116,7 +116,7 @@ class services_content_item_service_testcase extends \advanced_testcase {
         $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
 
         $cis = new content_item_service(new content_item_readonly_repository());
-        $allcontentitems = $cis->get_all_content_items();
+        $allcontentitems = $cis->get_all_content_items($user);
         $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,
@@ -128,9 +128,71 @@ class services_content_item_service_testcase extends \advanced_testcase {
         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();
+        $allcontentitems = $cis->get_all_content_items($user);
         $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'));
     }
+
+    /**
+     * Test confirming that a content item can be added to a user's favourites.
+     */
+    public function test_add_to_user_favourites() {
+        $this->resetAfterTest();
+
+        // 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());
+
+        // Grab a the assign content item, which we'll favourite for the user.
+        $items = $cis->get_all_content_items($user);
+        $assign = $items[array_search('assign', array_column($items, 'name'))];
+        $contentitem = $cis->add_to_user_favourites($user, 'mod_assign', $assign->id);
+
+        // Verify the exported result is marked as a favourite.
+        $this->assertEquals('assign', $contentitem->name);
+        $this->assertTrue($contentitem->favourite);
+
+        // Verify the item is marked as a favourite when returned from the other service methods.
+        $allitems = $cis->get_all_content_items($user);
+        $allitemsassign = $allitems[array_search('assign', array_column($allitems, 'name'))];
+
+        $courseitems = $cis->get_content_items_for_user_in_course($user, $course);
+        $courseitemsassign = $courseitems[array_search('assign', array_column($courseitems, 'name'))];
+        $this->assertTrue($allitemsassign->favourite);
+        $this->assertTrue($courseitemsassign->favourite);
+    }
+
+    /**
+     * Test verifying that content items can be removed from a user's favourites.
+     */
+    public function test_remove_from_user_favourites() {
+        $this->resetAfterTest();
+
+        // 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());
+
+        // Grab a the assign content item, which we'll favourite for the user.
+        $items = $cis->get_all_content_items($user);
+        $assign = $items[array_search('assign', array_column($items, 'name'))];
+        $cis->add_to_user_favourites($user, 'mod_assign', $assign->id);
+
+        // Now, remove the favourite, and verify it.
+        $contentitem = $cis->remove_from_user_favourites($user, 'mod_assign', $assign->id);
+
+        // Verify the exported result is not marked as a favourite.
+        $this->assertEquals('assign', $contentitem->name);
+        $this->assertFalse($contentitem->favourite);
+
+        // Verify the item is not marked as a favourite when returned from the other service methods.
+        $allitems = $cis->get_all_content_items($user);
+        $allitemsassign = $allitems[array_search('assign', array_column($allitems, 'name'))];
+        $courseitems = $cis->get_content_items_for_user_in_course($user, $course);
+        $courseitemsassign = $courseitems[array_search('assign', array_column($courseitems, 'name'))];
+        $this->assertFalse($allitemsassign->favourite);
+        $this->assertFalse($courseitemsassign->favourite);
+    }
 }
index 93c7256..6822d32 100644 (file)
@@ -78,6 +78,7 @@ $string['cachedef_string'] = 'Language string cache';
 $string['cachedef_tags'] = 'Tags collections and areas';
 $string['cachedef_temp_tables'] = 'Temporary tables cache';
 $string['cachedef_userselections'] = 'Data used to persist user selections throughout Moodle';
+$string['cachedef_user_favourite_course_content_items'] = 'User\'s favourite content items (activities, resources and their subtypes)';
 $string['cachedef_user_group_groupings'] = 'User\'s groupings and groups per course';
 $string['cachedef_user_course_content_items'] = 'User\'s content items (activities, resources and their subtypes) per course';
 $string['cachedef_yuimodules'] = 'YUI Module definitions';
index 9109ccf..2fd4a36 100644 (file)
@@ -421,4 +421,10 @@ $definitions = array(
         'mode' => cache_store::MODE_REQUEST,
         'simplekeys' => true,
     ],
+
+    // The list of favourited content items (activities, resources and their subtypes) for a user.
+    'user_favourite_course_content_items' => [
+        'mode' => cache_store::MODE_APPLICATION,
+        'simplekeys' => true,
+    ],
 );
index b1a76b2..4644e3f 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2020021400.01;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2020021400.02;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 $release  = '3.9dev (Build: 20200214)'; // Human-friendly version name