PageRenderTime 303ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/course/classes/local/service/content_item_service.php

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