MDL-67585 core_course: add favouriting to the content item service class

This commit is contained in:
Jake Dallimore 2020-01-29 15:37:33 +08:00
parent 57dfcf951d
commit f09e9b887e
7 changed files with 206 additions and 12 deletions

View 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[]?'
];
}
}

View 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[]?'
];
}
}

View 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'))];
}
}

View 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);
}
}

View 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';

View 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,
],
);

View 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