From cd09777dbdd3848cfaa8eedc3ebbfde62adb8085 Mon Sep 17 00:00:00 2001 From: Adrian Greeve Date: Mon, 10 Feb 2020 10:18:27 +0800 Subject: [PATCH] MDL-67262 core_course: Added the ability to recommend activities. This allows an adminsitrator or a user with 'moodle/course:recommendactivity' to recommend activities in the activity chooser. --- admin/settings/courses.php | 6 + course/amd/build/recommendations.min.js | 2 + course/amd/build/recommendations.min.js.map | 1 + course/amd/src/recommendations.js | 54 +++++++++ .../course_content_item_exporter.php | 20 +++- .../course_content_items_exporter.php | 4 +- .../local/service/content_item_service.php | 107 ++++++++++++++++-- .../output/recommendations/activity_list.php | 68 +++++++++++ .../output/recommendations/renderer.php | 46 ++++++++ course/externallib.php | 49 ++++++++ course/recommendations.php | 54 +++++++++ course/templates/activity_list.mustache | 56 +++++++++ lang/en/cache.php | 1 + lang/en/course.php | 5 + lang/en/role.php | 1 + lib/db/access.php | 9 ++ lib/db/caches.php | 5 + lib/db/services.php | 8 ++ version.php | 2 +- 19 files changed, 482 insertions(+), 16 deletions(-) create mode 100644 course/amd/build/recommendations.min.js create mode 100644 course/amd/build/recommendations.min.js.map create mode 100644 course/amd/src/recommendations.js create mode 100644 course/classes/output/recommendations/activity_list.php create mode 100644 course/classes/output/recommendations/renderer.php create mode 100644 course/recommendations.php create mode 100644 course/templates/activity_list.mustache diff --git a/admin/settings/courses.php b/admin/settings/courses.php index ad5ede65db3..caa8d7c7d5d 100644 --- a/admin/settings/courses.php +++ b/admin/settings/courses.php @@ -61,6 +61,12 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { array('moodle/restore:restorecourse') ) ); + $ADMIN->add('courses', + new admin_externalpage('activitychooser', new lang_string('activitychooser', 'course'), + new moodle_url('/course/recommendations.php'), + array('moodle/course:recommendactivity') + ) + ); // Course Default Settings Page. // NOTE: these settings must be applied after all other settings because they depend on them. diff --git a/course/amd/build/recommendations.min.js b/course/amd/build/recommendations.min.js new file mode 100644 index 00000000000..59b3a55afe8 --- /dev/null +++ b/course/amd/build/recommendations.min.js @@ -0,0 +1,2 @@ +define ("core_course/recommendations",["exports","core/ajax","core/notification"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}var e=function(a){var d={methodname:"core_course_toggle_activity_recommendation",args:{area:a.currentTarget.dataset.area,id:a.currentTarget.dataset.id}};b.default.call([d])[0].fail(c.default.exception)};a.init=function init(){var a=document.querySelectorAll("[data-area]");a.forEach(function(a){a.addEventListener("change",e)})}}); +//# sourceMappingURL=recommendations.min.js.map diff --git a/course/amd/build/recommendations.min.js.map b/course/amd/build/recommendations.min.js.map new file mode 100644 index 00000000000..11c28c1f9e8 --- /dev/null +++ b/course/amd/build/recommendations.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/recommendations.js"],"names":["toggleRecommendation","e","data","methodname","args","area","currentTarget","dataset","id","Ajax","call","fail","Notification","exception","init","checkboxelements","document","querySelectorAll","forEach","checkbox","addEventListener"],"mappings":"8KAuBA,OACA,O,sDAQMA,CAAAA,CAAoB,CAAG,SAACC,CAAD,CAAO,CAChC,GAAIC,CAAAA,CAAI,CAAG,CACPC,UAAU,CAAE,4CADL,CAEPC,IAAI,CAAE,CACFC,IAAI,CAAEJ,CAAC,CAACK,aAAF,CAAgBC,OAAhB,CAAwBF,IAD5B,CAEFG,EAAE,CAAEP,CAAC,CAACK,aAAF,CAAgBC,OAAhB,CAAwBC,EAF1B,CAFC,CAAX,CAOAC,UAAKC,IAAL,CAAU,CAACR,CAAD,CAAV,EAAkB,CAAlB,EAAqBS,IAArB,CAA0BC,UAAaC,SAAvC,CACH,C,QAOmB,QAAPC,CAAAA,IAAO,EAAM,CACtB,GAAMC,CAAAA,CAAgB,CAAGC,QAAQ,CAACC,gBAAT,CAA0B,aAA1B,CAAzB,CACAF,CAAgB,CAACG,OAAjB,CAAyB,SAACC,CAAD,CAAc,CACnCA,CAAQ,CAACC,gBAAT,CAA0B,QAA1B,CAAoCpB,CAApC,CACH,CAFD,CAGH,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A javascript module to handle toggling activity chooser recommendations.\n *\n * @module core_course/recommendations\n * @copyright 2020 Adrian Greeve \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\n/**\n * Do an ajax call to toggle the recommendation\n *\n * @param {object} e The event\n * @return {void}\n */\nconst toggleRecommendation = (e) => {\n let data = {\n methodname: 'core_course_toggle_activity_recommendation',\n args: {\n area: e.currentTarget.dataset.area,\n id: e.currentTarget.dataset.id\n }\n };\n Ajax.call([data])[0].fail(Notification.exception);\n};\n\n/**\n * Initialisation function\n *\n * @return {void}\n */\nexport const init = () => {\n const checkboxelements = document.querySelectorAll(\"[data-area]\");\n checkboxelements.forEach((checkbox) => {\n checkbox.addEventListener('change', toggleRecommendation);\n });\n};"],"file":"recommendations.min.js"} \ No newline at end of file diff --git a/course/amd/src/recommendations.js b/course/amd/src/recommendations.js new file mode 100644 index 00000000000..e992b855725 --- /dev/null +++ b/course/amd/src/recommendations.js @@ -0,0 +1,54 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * A javascript module to handle toggling activity chooser recommendations. + * + * @module core_course/recommendations + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Ajax from 'core/ajax'; +import Notification from 'core/notification'; + +/** + * Do an ajax call to toggle the recommendation + * + * @param {object} e The event + * @return {void} + */ +const toggleRecommendation = (e) => { + let data = { + methodname: 'core_course_toggle_activity_recommendation', + args: { + area: e.currentTarget.dataset.area, + id: e.currentTarget.dataset.id + } + }; + Ajax.call([data])[0].fail(Notification.exception); +}; + +/** + * Initialisation function + * + * @return {void} + */ +export const init = () => { + const checkboxelements = document.querySelectorAll("[data-area]"); + checkboxelements.forEach((checkbox) => { + checkbox.addEventListener('change', toggleRecommendation); + }); +}; \ No newline at end of file diff --git a/course/classes/local/exporters/course_content_item_exporter.php b/course/classes/local/exporters/course_content_item_exporter.php index 312341862b9..2386446e877 100644 --- a/course/classes/local/exporters/course_content_item_exporter.php +++ b/course/classes/local/exporters/course_content_item_exporter.php @@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die(); use core\external\exporter; use core_course\local\entity\content_item; +use core_course\local\service\content_item_service; /** * The course_content_item_exporter class. @@ -82,7 +83,8 @@ class course_content_item_exporter extends exporter { 'legacyitem' => [ 'type' => PARAM_BOOL, 'description' => 'If this item was pulled from the old callback and has no item id.' - ] + ], + 'recommended' => ['type' => PARAM_BOOL, 'description' => 'Has this item been recommended'], ]; } @@ -113,6 +115,16 @@ class course_content_item_exporter extends exporter { } } + $recommended = false; + $itemtype = content_item_service::RECOMMENDATION_PREFIX . $this->contentitem->get_component_name(); + if (isset($this->related['recommended'])) { + foreach ($this->related['recommended'] as $favobj) { + if ($favobj->itemtype === $itemtype && in_array($this->contentitem->get_id(), $favobj->ids)) { + $recommended = true; + } + } + } + $properties = [ 'id' => $this->contentitem->get_id(), 'name' => $this->contentitem->get_name(), @@ -123,7 +135,8 @@ class course_content_item_exporter extends exporter { 'archetype' => $this->contentitem->get_archetype(), 'componentname' => $this->contentitem->get_component_name(), 'favourite' => $favourite, - 'legacyitem' => ($this->contentitem->get_id() == -1) + 'legacyitem' => ($this->contentitem->get_id() == -1), + 'recommended' => $recommended ]; return $properties; @@ -137,7 +150,8 @@ class course_content_item_exporter extends exporter { protected static function define_related(): array { return [ 'context' => '\context', - 'favouriteitems' => '\stdClass[]?' + 'favouriteitems' => '\stdClass[]?', + 'recommended' => '\stdClass[]?' ]; } } diff --git a/course/classes/local/exporters/course_content_items_exporter.php b/course/classes/local/exporters/course_content_items_exporter.php index e468a7ce248..9b1c6948d08 100644 --- a/course/classes/local/exporters/course_content_items_exporter.php +++ b/course/classes/local/exporters/course_content_items_exporter.php @@ -80,6 +80,7 @@ class course_content_items_exporter extends exporter { [ 'context' => $this->related['context'], 'favouriteitems' => $this->related['favouriteitems'], + 'recommended' => $this->related['recommended'] ] ); return $exporter->export($output); @@ -100,7 +101,8 @@ class course_content_items_exporter extends exporter { protected static function define_related() { return [ 'context' => '\context', - 'favouriteitems' => '\stdClass[]?' + 'favouriteitems' => '\stdClass[]?', + 'recommended' => '\stdClass[]?' ]; } } diff --git a/course/classes/local/service/content_item_service.php b/course/classes/local/service/content_item_service.php index 5505dcf624e..3c2a605c9be 100644 --- a/course/classes/local/service/content_item_service.php +++ b/course/classes/local/service/content_item_service.php @@ -40,6 +40,15 @@ class content_item_service { /** @var content_item_readonly_repository_interface $repository a repository for content items. */ private $repository; + /** string the component for this favourite. */ + public const COMPONENT = 'core_course'; + /** string the favourite prefix itemtype in the favourites table. */ + public const FAVOURITE_PREFIX = 'contentitem_'; + /** string the recommendation prefix itemtype in the favourites table. */ + public const RECOMMENDATION_PREFIX = 'recommend_'; + /** string the cache name for recommendations. */ + public const RECOMMENDATION_CACHE = 'recommendation_favourite_course_content_items'; + /** * The content_item_service constructor. * @@ -69,6 +78,47 @@ class content_item_service { return $favmods; } + $favourites = $this->get_content_favourites(self::FAVOURITE_PREFIX, \context_user::instance($user->id)); + + $favcache->set($key, $favourites); + return $favourites; + } + + /** + * Returns an array of objects representing recommended 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. + * + * @return array + */ + private function get_recommendations(): array { + global $CFG; + + $recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE); + $key = $CFG->siteguest; + $favmods = $recommendationcache->get($key); + if ($favmods !== false) { + return $favmods; + } + + $favourites = $this->get_content_favourites(self::RECOMMENDATION_PREFIX, \context_user::instance($CFG->siteguest)); + + $recommendationcache->set($CFG->siteguest, $favourites); + return $favourites; + } + + /** + * Gets content favourites from the favourites system depending on the area. + * + * @param string $prefix Prefix for the item type. + * @param \context_user $usercontext User context for the favourite + * @return array An array of favourite objects. + */ + private function get_content_favourites(string $prefix, \context_user $usercontext): array { // 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 @@ -78,25 +128,24 @@ class content_item_service { $itemtypes = []; foreach ($plugins as $plugin) { // Add the mod itself. - $itemtypes[] = 'contentitem_mod_' . $plugin->name; + $itemtypes[] = $prefix . '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; + $itemtypes[] = $prefix . $subpluginname; } } } - $ufservice = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($user->id)); + $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); $favourites = []; foreach ($itemtypes as $itemtype) { - $favs = $ufservice->find_favourites_by_type('core_course', $itemtype); + $favs = $ufservice->find_favourites_by_type(self::COMPONENT, $itemtype); $favobj = (object) ['itemtype' => $itemtype, 'ids' => array_column($favs, 'itemid')]; $favourites[] = $favobj; } - $favcache->set($key, $favourites); return $favourites; } @@ -112,11 +161,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); + $recommendations = $this->get_recommendations(); $ciexporter = new course_content_items_exporter( $allcontentitems, [ 'context' => \context_system::instance(), - 'favouriteitems' => $favourites + 'favouriteitems' => $favourites, + 'recommended' => $recommendations ] ); $exported = $ciexporter->export($PAGE->get_renderer('core')); @@ -186,11 +237,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); + $recommended = $this->get_recommendations(); $ciexporter = new course_content_items_exporter( $availablecontentitems, [ 'context' => \context_course::instance($course->id), - 'favouriteitems' => $favourites + 'favouriteitems' => $favourites, + 'recommended' => $recommended ] ); $exported = $ciexporter->export($PAGE->get_renderer('course')); @@ -217,9 +270,9 @@ class content_item_service { // 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; + $itemtype = self::FAVOURITE_PREFIX . $componentname; - $ufservice->create_favourite('core_course', $itemtype, $contentitemid, $usercontext); + $ufservice->create_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext); $favcache = \cache::make('core', 'user_favourite_course_content_items'); $favcache->delete($user->id); @@ -242,9 +295,9 @@ class content_item_service { // 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; + $itemtype = self::FAVOURITE_PREFIX . $componentname; - $ufservice->delete_favourite('core_course', $itemtype, $contentitemid, $usercontext); + $ufservice->delete_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext); $favcache = \cache::make('core', 'user_favourite_course_content_items'); $favcache->delete($user->id); @@ -252,4 +305,36 @@ class content_item_service { $items = $this->get_all_content_items($user); return $items[array_search($contentitemid, array_column($items, 'id'))]; } + + /** + * Toggle an activity to being recommended or not. + * + * @param string $itemtype The component such as mod_assign, or assignsubmission_file + * @param int $itemid The id related to this component item. + * @return bool True on creating a favourite, false on deleting it. + */ + public function toggle_recommendation(string $itemtype, int $itemid): bool { + global $CFG; + + $context = \context_system::instance(); + + $itemtype = self::RECOMMENDATION_PREFIX . $itemtype; + + // Favourites are created using a user context. We'll use the site guest user ID as that should not change and there + // can be only one. + $usercontext = \context_user::instance($CFG->siteguest); + + $recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE); + + $favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext); + if ($favouritefactory->favourite_exists(self::COMPONENT, $itemtype, $itemid, $context)) { + $favouritefactory->delete_favourite(self::COMPONENT, $itemtype, $itemid, $context); + $result = $recommendationcache->delete($CFG->siteguest); + return false; + } else { + $favouritefactory->create_favourite(self::COMPONENT, $itemtype, $itemid, $context); + $result = $recommendationcache->delete($CFG->siteguest); + return true; + } + } } diff --git a/course/classes/output/recommendations/activity_list.php b/course/classes/output/recommendations/activity_list.php new file mode 100644 index 00000000000..18150c6d4c0 --- /dev/null +++ b/course/classes/output/recommendations/activity_list.php @@ -0,0 +1,68 @@ +. + +/** + * Contains renderers for the recommendations page. + * + * @package core_course + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output\recommendations; + +/** + * Main renderer for the recommendations page. + * + * @package core_course + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class activity_list implements \renderable, \templatable { + + /** @var array $modules activities to display in the recommendations page. */ + protected $modules; + + /** + * Constructor method. + * + * @param array $modules Activities to display + */ + public function __construct(array $modules) { + $this->modules = $modules; + } + + /** + * Export method to configure information into something the template can use. + * + * @param \renderer_base $output Not actually used. + * @return array Template context information. + */ + public function export_for_template(\renderer_base $output): array { + + $info = array_map(function($module) { + return [ + 'id' => $module->id ?? '', + 'name' => $module->title, + 'componentname' => $module->componentname, + 'icon' => $module->icon, + 'recommended' => $module->recommended ?? '' + ]; + }, $this->modules); + + return ['categories' => ['categoryname' => get_string('activities'), 'categorydata' => $info]]; + } +} \ No newline at end of file diff --git a/course/classes/output/recommendations/renderer.php b/course/classes/output/recommendations/renderer.php new file mode 100644 index 00000000000..b677027dca5 --- /dev/null +++ b/course/classes/output/recommendations/renderer.php @@ -0,0 +1,46 @@ +. + +/** + * Contains renderers for the recommendations page. + * + * @package core_course + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output\recommendations; + +/** + * Main renderer for the recommendations page. + * + * @package core_course + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends \plugin_renderer_base { + + /** + * Render a list of activities to recommend. + * + * @param \core_course\output\recommendations\activity_list $page activity list renderable + * @return string html for displaying. + */ + public function render_activity_list(\core_course\output\recommendations\activity_list $page): string { + $data = $page->export_for_template($this); + return parent::render_from_template('core_course/activity_list', $data); + } +} \ No newline at end of file diff --git a/course/externallib.php b/course/externallib.php index 30956684616..5ddf89e5e30 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -4287,4 +4287,53 @@ class core_course_external extends external_api { $contentitems = $contentitemservice->get_content_items_for_user_in_course($USER, $course); return ['content_items' => $contentitems]; } + + /** + * Returns description of method parameters. + * + * @return external_function_parameters + */ + public static function toggle_activity_recommendation_parameters() { + return new external_function_parameters([ + 'area' => new external_value(PARAM_TEXT, 'The favourite area (itemtype)', VALUE_REQUIRED), + 'id' => new external_value(PARAM_INT, 'id of the activity or whatever', VALUE_REQUIRED), + ]); + } + + /** + * Update the recommendation for an activity item. + * + * @param string $area identifier for this activity. + * @param int $id Associated id. This is needed in conjunction with the area to find the recommendation. + * @return array some warnings or something. + */ + public static function toggle_activity_recommendation(string $area, int $id): array { + ['area' => $area, 'id' => $id] = self::validate_parameters(self::toggle_activity_recommendation_parameters(), + ['area' => $area, 'id' => $id]); + + $context = context_system::instance(); + self::validate_context($context); + + require_capability('moodle/course:recommendactivity', $context); + + $manager = \core_course\local\factory\content_item_service_factory::get_content_item_service(); + + $status = $manager->toggle_recommendation($area, $id); + return ['id' => $id, 'area' => $area, 'status' => $status]; + } + + /** + * Returns warnings. + * + * @return external_description + */ + public static function toggle_activity_recommendation_returns() { + return new external_single_structure( + [ + 'id' => new external_value(PARAM_INT, 'id of the activity or whatever'), + 'area' => new external_value(PARAM_TEXT, 'The favourite area (itemtype)'), + 'status' => new external_value(PARAM_BOOL, 'If created or deleted'), + ] + ); + } } diff --git a/course/recommendations.php b/course/recommendations.php new file mode 100644 index 00000000000..63e7a33d741 --- /dev/null +++ b/course/recommendations.php @@ -0,0 +1,54 @@ +. + +/** + * Site recommendations for the activity chooser. + * + * @package core_course + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once("../config.php"); + +$context = context_system::instance(); +$url = new moodle_url('/course/recommendations.php'); + +$pageheading = format_string($SITE->fullname, true, ['context' => $context]); + +$PAGE->set_context($context); +$PAGE->set_url($url); +$PAGE->set_pagelayout('admin'); + +$PAGE->set_title(get_string('activitychoosertitle', 'course')); +$PAGE->set_heading($pageheading); + +require_login(); +require_capability('moodle/course:recommendactivity', $context); + +$renderer = $PAGE->get_renderer('core_course', 'recommendations'); + +echo $renderer->header(); +echo $renderer->heading(get_string('activitychooser', 'course')); + +$manager = \core_course\local\factory\content_item_service_factory::get_content_item_service(); +$modules = $manager->get_all_content_items($USER); + +$activitylist = new \core_course\output\recommendations\activity_list($modules); + +echo $renderer->render_activity_list($activitylist); + +echo $renderer->footer(); diff --git a/course/templates/activity_list.mustache b/course/templates/activity_list.mustache new file mode 100644 index 00000000000..e50dd922293 --- /dev/null +++ b/course/templates/activity_list.mustache @@ -0,0 +1,56 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_course/activity_list + + Displays a list of activities to recommend in the activity chooser. + + No example given as the js will fire and create records from the template library page. +}} +{{#categories}} +

{{categoryname}}

+ + + + + + + + + {{#categorydata}} + + + {{#id}} + + {{/id}} + {{^id}} + + {{/id}} + + {{/categorydata}} + +
{{#str}}module, course{{/str}}{{#str}}recommend, course{{/str}}
{{{icon}}}{{name}} + +
+{{/categories}} +{{#js}} +require([ + 'core_course/recommendations', +], function(Recommendations) { + Recommendations.init(); +}); +{{/js}} \ No newline at end of file diff --git a/lang/en/cache.php b/lang/en/cache.php index 6822d3282a7..2bb8a141cf1 100644 --- a/lang/en/cache.php +++ b/lang/en/cache.php @@ -71,6 +71,7 @@ $string['cachedef_portfolio_add_button_portfolio_instances'] = 'Portfolio instan $string['cachedef_postprocessedcss'] = 'Post processed CSS'; $string['cachedef_tagindexbuilder'] = 'Search results for tagged items'; $string['cachedef_questiondata'] = 'Question definitions'; +$string['cachedef_recommendation_favourite_course_content_items'] = 'Recommendation of course content items'; $string['cachedef_repositories'] = 'Repositories instances data'; $string['cachedef_roledefs'] = 'Role definitions'; $string['cachedef_grade_categories'] = 'Grade category queries'; diff --git a/lang/en/course.php b/lang/en/course.php index a4d853164ce..1d1da040458 100644 --- a/lang/en/course.php +++ b/lang/en/course.php @@ -22,6 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['activitychooser'] = 'Activity chooser'; +$string['activitychoosertitle'] = 'Recommendations for the activity chooser'; $string['aria:coursecategory'] = 'Course category'; $string['aria:courseimage'] = 'Course image'; $string['aria:courseshortname'] = 'Course short name'; @@ -46,6 +48,7 @@ $string['errorendbeforestart'] = 'The end date ({$a}) is before the course start $string['favourite'] = 'Starred course'; $string['gradetopassnotset'] = 'This course does not have a grade to pass set. It may be set in the grade item of the course (Gradebook setup).'; $string['informationformodule'] = 'Information about the {$a} activity'; +$string['module'] = 'Module'; $string['nocourseactivity'] = 'Not enough course activity between the start and the end of the course'; $string['nocourseendtime'] = 'The course does not have an end time'; $string['nocoursesections'] = 'No course sections'; @@ -65,6 +68,8 @@ $string['privacy:favouritespath'] = 'Course starred information'; $string['privacy:metadata:activityfavouritessummary'] = 'The course system contains information about which items from the activity chooser have been starred by the user.'; $string['privacy:metadata:completionsummary'] = 'The course contains completion information about the user.'; $string['privacy:metadata:favouritessummary'] = 'The course contains information relating to the course being starred by the user.'; +$string['recommend'] = 'Recommend'; +$string['recommendcheckbox'] = 'Recommend activity: {$a}'; $string['studentsatriskincourse'] = 'Students at risk in {$a} course'; $string['studentsatriskinfomessage'] = 'Hi {$a->userfirstname}, diff --git a/lang/en/role.php b/lang/en/role.php index 96d071ae0f3..0ae3f80ef86 100644 --- a/lang/en/role.php +++ b/lang/en/role.php @@ -179,6 +179,7 @@ $string['course:markcomplete'] = 'Mark users as complete in course completion'; $string['course:movesections'] = 'Move sections'; $string['course:overridecompletion'] = 'Override activity completion status'; $string['course:renameroles'] = 'Rename roles'; +$string['course:recommendactivity'] = 'Recommend activities to the activity chooser'; $string['course:request'] = 'Request new courses'; $string['course:reset'] = 'Reset course'; $string['course:reviewotherusers'] = 'Review other users'; diff --git a/lib/db/access.php b/lib/db/access.php index 8e33d64e751..95e1d4fab5f 100644 --- a/lib/db/access.php +++ b/lib/db/access.php @@ -2471,4 +2471,13 @@ $capabilities = array( 'manager' => CAP_ALLOW, ] ], + + // Allow users to recommend activities in the activity chooser. + 'moodle/course:recommendactivity' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + ] + ] ); diff --git a/lib/db/caches.php b/lib/db/caches.php index 2fd4a36e389..ce4c6416959 100644 --- a/lib/db/caches.php +++ b/lib/db/caches.php @@ -427,4 +427,9 @@ $definitions = array( 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, ], + + \core_course\local\service\content_item_service::RECOMMENDATION_CACHE => [ + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + ], ); diff --git a/lib/db/services.php b/lib/db/services.php index 357980988d1..9442f9cb3b5 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -653,6 +653,14 @@ $functions = array( 'type' => 'read', 'ajax' => true, ), + 'core_course_toggle_activity_recommendation' => array( + 'classname' => 'core_course_external', + 'methodname' => 'toggle_activity_recommendation', + 'classpath' => 'course/externallib.php', + 'description' => 'Adds or removes an activity as a recommendation in the activity chooser.', + 'type' => 'write', + 'ajax' => true, + ), 'core_enrol_get_course_enrolment_methods' => array( 'classname' => 'core_enrol_external', 'methodname' => 'get_course_enrolment_methods', diff --git a/version.php b/version.php index e7f1c984035..844e3109265 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2020022100.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2020022100.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '3.9dev (Build: 20200221)'; // Human-friendly version name