Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Contains the content_item_service class. 19 * 20 * @package core 21 * @subpackage course 22 * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 namespace core_course\local\service; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 use core_course\local\exporters\course_content_items_exporter; 30 use core_course\local\repository\content_item_readonly_repository_interface; 31 32 /** 33 * The content_item_service class, providing the api for interacting with content items. 34 * 35 * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 class content_item_service { 39 40 /** @var content_item_readonly_repository_interface $repository a repository for content items. */ 41 private $repository; 42 43 /** string the component for this favourite. */ 44 public const COMPONENT = 'core_course'; 45 /** string the favourite prefix itemtype in the favourites table. */ 46 public const FAVOURITE_PREFIX = 'contentitem_'; 47 /** string the recommendation prefix itemtype in the favourites table. */ 48 public const RECOMMENDATION_PREFIX = 'recommend_'; 49 /** string the cache name for recommendations. */ 50 public const RECOMMENDATION_CACHE = 'recommendation_favourite_course_content_items'; 51 52 /** 53 * The content_item_service constructor. 54 * 55 * @param content_item_readonly_repository_interface $repository a content item repository. 56 */ 57 public function __construct(content_item_readonly_repository_interface $repository) { 58 $this->repository = $repository; 59 } 60 61 /** 62 * Returns an array of objects representing favourited content items. 63 * 64 * Each object contains the following properties: 65 * itemtype: a string containing the 'itemtype' key used by the favourites subsystem. 66 * ids[]: an array of ids, representing the content items within a component. 67 * 68 * Since two components can return (via their hook implementation) the same id, the itemtype is used for uniqueness. 69 * 70 * @param \stdClass $user 71 * @return array 72 */ 73 private function get_favourite_content_items_for_user(\stdClass $user): array { 74 $favcache = \cache::make('core', 'user_favourite_course_content_items'); 75 $key = $user->id; 76 $favmods = $favcache->get($key); 77 if ($favmods !== false) { 78 return $favmods; 79 } 80 81 $favourites = $this->get_content_favourites(self::FAVOURITE_PREFIX, \context_user::instance($user->id)); 82 83 $favcache->set($key, $favourites); 84 return $favourites; 85 } 86 87 /** 88 * Returns an array of objects representing recommended content items. 89 * 90 * Each object contains the following properties: 91 * itemtype: a string containing the 'itemtype' key used by the favourites subsystem. 92 * ids[]: an array of ids, representing the content items within a component. 93 * 94 * Since two components can return (via their hook implementation) the same id, the itemtype is used for uniqueness. 95 * 96 * @return array 97 */ 98 private function get_recommendations(): array { 99 global $CFG; 100 101 $recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE); 102 $key = $CFG->siteguest; 103 $favmods = $recommendationcache->get($key); 104 if ($favmods !== false) { 105 return $favmods; 106 } 107 108 // Make sure the guest user exists in the database. 109 if (!\core_user::get_user($CFG->siteguest)) { 110 throw new \coding_exception('The guest user does not exist in the database.'); 111 } 112 113 // Make sure the guest user context exists. 114 if (!$guestusercontext = \context_user::instance($CFG->siteguest, false)) { 115 throw new \coding_exception('The guest user context does not exist.'); 116 } 117 118 $favourites = $this->get_content_favourites(self::RECOMMENDATION_PREFIX, $guestusercontext); 119 120 $recommendationcache->set($CFG->siteguest, $favourites); 121 return $favourites; 122 } 123 124 /** 125 * Gets content favourites from the favourites system depending on the area. 126 * 127 * @param string $prefix Prefix for the item type. 128 * @param \context_user $usercontext User context for the favourite 129 * @return array An array of favourite objects. 130 */ 131 private function get_content_favourites(string $prefix, \context_user $usercontext): array { 132 // Get all modules and any submodules which implement get_course_content_items() hook. 133 // This gives us the set of all itemtypes which we'll use to register favourite content items. 134 // The ids that each plugin returns will be used together with the itemtype to uniquely identify 135 // each content item for favouriting. 136 $pluginmanager = \core_plugin_manager::instance(); 137 $plugins = $pluginmanager->get_plugins_of_type('mod'); 138 $itemtypes = []; 139 foreach ($plugins as $plugin) { 140 // Add the mod itself. 141 $itemtypes[] = $prefix . 'mod_' . $plugin->name; 142 143 // Add any subplugins to the list of item types. 144 $subplugins = $pluginmanager->get_subplugins_of_plugin('mod_' . $plugin->name); 145 foreach ($subplugins as $subpluginname => $subplugininfo) { 146 try { 147 if (component_callback_exists($subpluginname, 'get_course_content_items')) { 148 $itemtypes[] = $prefix . $subpluginname; 149 } 150 } catch (\moodle_exception $e) { 151 debugging('Cannot get_course_content_items: ' . $e->getMessage(), DEBUG_DEVELOPER); 152 } 153 } 154 } 155 156 $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); 157 $favourites = []; 158 $favs = $ufservice->find_all_favourites(self::COMPONENT, $itemtypes); 159 $favsreduced = array_reduce($favs, function($carry, $item) { 160 $carry[$item->itemtype][$item->itemid] = 0; 161 return $carry; 162 }, []); 163 164 foreach ($itemtypes as $type) { 165 $favourites[] = (object) [ 166 'itemtype' => $type, 167 'ids' => isset($favsreduced[$type]) ? array_keys($favsreduced[$type]) : [] 168 ]; 169 } 170 return $favourites; 171 } 172 173 /** 174 * Get all content items which may be added to courses, irrespective of course caps, for site admin views, etc. 175 * 176 * @param \stdClass $user the user object. 177 * @return array the array of exported content items. 178 */ 179 public function get_all_content_items(\stdClass $user): array { 180 $allcontentitems = $this->repository->find_all(); 181 182 return $this->export_content_items($user, $allcontentitems); 183 } 184 185 /** 186 * Get content items which name matches a certain pattern and may be added to courses, 187 * irrespective of course caps, for site admin views, etc. 188 * 189 * @param \stdClass $user The user object. 190 * @param string $pattern The search pattern. 191 * @return array The array of exported content items. 192 */ 193 public function get_content_items_by_name_pattern(\stdClass $user, string $pattern): array { 194 $allcontentitems = $this->repository->find_all(); 195 196 $filteredcontentitems = array_filter($allcontentitems, function($contentitem) use ($pattern) { 197 return preg_match("/$pattern/i", $contentitem->get_title()->get_value()); 198 }); 199 200 return $this->export_content_items($user, $filteredcontentitems); 201 } 202 203 /** 204 * Export content items. 205 * 206 * @param \stdClass $user The user object. 207 * @param array $contentitems The content items array. 208 * @return array The array of exported content items. 209 */ 210 private function export_content_items(\stdClass $user, $contentitems) { 211 global $PAGE; 212 213 // Export the objects to get the formatted objects for transfer/display. 214 $favourites = $this->get_favourite_content_items_for_user($user); 215 $recommendations = $this->get_recommendations(); 216 $ciexporter = new course_content_items_exporter( 217 $contentitems, 218 [ 219 'context' => \context_system::instance(), 220 'favouriteitems' => $favourites, 221 'recommended' => $recommendations 222 ] 223 ); 224 $exported = $ciexporter->export($PAGE->get_renderer('core')); 225 226 // Sort by title for return. 227 usort($exported->content_items, function($a, $b) { 228 return $a->title > $b->title; 229 }); 230 231 return $exported->content_items; 232 } 233 234 /** 235 * Return a representation of the available content items, for a user in a course. 236 * 237 * @param \stdClass $user the user to check access for. 238 * @param \stdClass $course the course to scope the content items to. 239 * @param array $linkparams the desired section to return to. 240 * @return \stdClass[] the content items, scoped to a course. 241 */ 242 public function get_content_items_for_user_in_course(\stdClass $user, \stdClass $course, array $linkparams = []): array { 243 global $PAGE; 244 245 if (!has_capability('moodle/course:manageactivities', \context_course::instance($course->id), $user)) { 246 return []; 247 } 248 249 // Get all the visible content items. 250 $allcontentitems = $this->repository->find_all_for_course($course, $user); 251 252 // Content items can only originate from modules or submodules. 253 $pluginmanager = \core_plugin_manager::instance(); 254 $components = \core_component::get_component_list(); 255 $parents = []; 256 foreach ($allcontentitems as $contentitem) { 257 if (!in_array($contentitem->get_component_name(), array_keys($components['mod']))) { 258 // It could be a subplugin. 259 $info = $pluginmanager->get_plugin_info($contentitem->get_component_name()); 260 if (!is_null($info)) { 261 $parent = $info->get_parent_plugin(); 262 if ($parent != false) { 263 if (in_array($parent, array_keys($components['mod']))) { 264 $parents[$contentitem->get_component_name()] = $parent; 265 continue; 266 } 267 } 268 } 269 throw new \moodle_exception('Only modules and submodules can generate content items. \'' 270 . $contentitem->get_component_name() . '\' is neither.'); 271 } 272 $parents[$contentitem->get_component_name()] = $contentitem->get_component_name(); 273 } 274 275 // Now, check access to these items for the user. 276 $availablecontentitems = array_filter($allcontentitems, function($contentitem) use ($course, $user, $parents) { 277 // Check the parent module access for the user. 278 return course_allowed_module($course, explode('_', $parents[$contentitem->get_component_name()])[1], $user); 279 }); 280 281 // Add the link params to the link, if any have been provided. 282 if (!empty($linkparams)) { 283 $availablecontentitems = array_map(function ($item) use ($linkparams) { 284 $item->get_link()->params($linkparams); 285 return $item; 286 }, $availablecontentitems); 287 } 288 289 // Export the objects to get the formatted objects for transfer/display. 290 $favourites = $this->get_favourite_content_items_for_user($user); 291 $recommended = $this->get_recommendations(); 292 $ciexporter = new course_content_items_exporter( 293 $availablecontentitems, 294 [ 295 'context' => \context_course::instance($course->id), 296 'favouriteitems' => $favourites, 297 'recommended' => $recommended 298 ] 299 ); 300 $exported = $ciexporter->export($PAGE->get_renderer('course')); 301 302 // Sort by title for return. 303 usort($exported->content_items, function($a, $b) { 304 return $a->title > $b->title; 305 }); 306 307 return $exported->content_items; 308 } 309 310 /** 311 * Add a content item to a user's favourites. 312 * 313 * @param \stdClass $user the user whose favourite this is. 314 * @param string $componentname the name of the component from which the content item originates. 315 * @param int $contentitemid the id of the content item. 316 * @return \stdClass the exported content item. 317 */ 318 public function add_to_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass { 319 $usercontext = \context_user::instance($user->id); 320 $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); 321 322 // Because each plugin decides its own ids for content items, a combination of 323 // itemtype and id is used to guarantee uniqueness across all content items. 324 $itemtype = self::FAVOURITE_PREFIX . $componentname; 325 326 $ufservice->create_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext); 327 328 $favcache = \cache::make('core', 'user_favourite_course_content_items'); 329 $favcache->delete($user->id); 330 331 $items = $this->get_all_content_items($user); 332 return $items[array_search($contentitemid, array_column($items, 'id'))]; 333 } 334 335 /** 336 * Remove the content item from a user's favourites. 337 * 338 * @param \stdClass $user the user whose favourite this is. 339 * @param string $componentname the name of the component from which the content item originates. 340 * @param int $contentitemid the id of the content item. 341 * @return \stdClass the exported content item. 342 */ 343 public function remove_from_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass { 344 $usercontext = \context_user::instance($user->id); 345 $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); 346 347 // Because each plugin decides its own ids for content items, a combination of 348 // itemtype and id is used to guarantee uniqueness across all content items. 349 $itemtype = self::FAVOURITE_PREFIX . $componentname; 350 351 $ufservice->delete_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext); 352 353 $favcache = \cache::make('core', 'user_favourite_course_content_items'); 354 $favcache->delete($user->id); 355 356 $items = $this->get_all_content_items($user); 357 return $items[array_search($contentitemid, array_column($items, 'id'))]; 358 } 359 360 /** 361 * Toggle an activity to being recommended or not. 362 * 363 * @param string $itemtype The component such as mod_assign, or assignsubmission_file 364 * @param int $itemid The id related to this component item. 365 * @return bool True on creating a favourite, false on deleting it. 366 */ 367 public function toggle_recommendation(string $itemtype, int $itemid): bool { 368 global $CFG; 369 370 $context = \context_system::instance(); 371 372 $itemtype = self::RECOMMENDATION_PREFIX . $itemtype; 373 374 // Favourites are created using a user context. We'll use the site guest user ID as that should not change and there 375 // can be only one. 376 $usercontext = \context_user::instance($CFG->siteguest); 377 378 $recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE); 379 380 $favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext); 381 if ($favouritefactory->favourite_exists(self::COMPONENT, $itemtype, $itemid, $context)) { 382 $favouritefactory->delete_favourite(self::COMPONENT, $itemtype, $itemid, $context); 383 $result = $recommendationcache->delete($CFG->siteguest); 384 return false; 385 } else { 386 $favouritefactory->create_favourite(self::COMPONENT, $itemtype, $itemid, $context); 387 $result = $recommendationcache->delete($CFG->siteguest); 388 return true; 389 } 390 } 391 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body