MDL-67262 core_course: Added the ability to recommend activities.
authorAdrian Greeve <abgreeve@gmail.com>
Mon, 10 Feb 2020 02:18:27 +0000 (10:18 +0800)
committerJake Dallimore <jake@moodle.com>
Mon, 24 Feb 2020 02:59:55 +0000 (10:59 +0800)
This allows an adminsitrator or a user with
'moodle/course:recommendactivity' to recommend activities in the
activity chooser.

19 files changed:
admin/settings/courses.php
course/amd/build/recommendations.min.js [new file with mode: 0644]
course/amd/build/recommendations.min.js.map [new file with mode: 0644]
course/amd/src/recommendations.js [new file with mode: 0644]
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/classes/output/recommendations/activity_list.php [new file with mode: 0644]
course/classes/output/recommendations/renderer.php [new file with mode: 0644]
course/externallib.php
course/recommendations.php [new file with mode: 0644]
course/templates/activity_list.mustache [new file with mode: 0644]
lang/en/cache.php
lang/en/course.php
lang/en/role.php
lib/db/access.php
lib/db/caches.php
lib/db/services.php
version.php

index ad5ede6..caa8d7c 100644 (file)
@@ -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 (file)
index 0000000..59b3a55
Binary files /dev/null and b/course/amd/build/recommendations.min.js differ
diff --git a/course/amd/build/recommendations.min.js.map b/course/amd/build/recommendations.min.js.map
new file mode 100644 (file)
index 0000000..11c28c1
Binary files /dev/null and b/course/amd/build/recommendations.min.js.map differ
diff --git a/course/amd/src/recommendations.js b/course/amd/src/recommendations.js
new file mode 100644 (file)
index 0000000..e992b85
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+
+/**
+ * A javascript module to handle toggling activity chooser recommendations.
+ *
+ * @module     core_course/recommendations
+ * @copyright  2020 Adrian Greeve <adrian@moodle.com>
+ * @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
index 3123418..2386446 100644 (file)
@@ -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[]?'
         ];
     }
 }
index e468a7c..9b1c694 100644 (file)
@@ -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[]?'
         ];
     }
 }
index 5505dcf..3c2a605 100644 (file)
@@ -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 (file)
index 0000000..18150c6
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 (file)
index 0000000..b677027
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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
index 3095668..5ddf89e 100644 (file)
@@ -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 (file)
index 0000000..63e7a33
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 (file)
index 0000000..e50dd92
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @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}}
+<h3>{{categoryname}}</h3>
+<table class="table table-striped table-hover">
+    <thead>
+        <tr class="d-flex">
+            <th scope="col" class="col-7 c0">{{#str}}module, course{{/str}}</th>
+            <th scope="col" class="col-5 c1">{{#str}}recommend, course{{/str}}</th>
+        </tr>
+    </thead>
+    <tbody>
+        {{#categorydata}}
+        <tr class="d-flex">
+            <td class="font-weight-bold col-7 c0"><span>{{{icon}}}</span>{{name}}</td>
+            {{#id}}
+            <td class="col-5 c1 colselect">
+            <input class="activity-recommend-checkbox" type="checkbox" aria-label="{{#str}}recommendcheckbox, course, {{name}}{{/str}}" data-area="{{componentname}}" data-id="{{id}}" {{#recommended}}checked="checked"{{/recommended}}  />
+            </td>
+            {{/id}}
+            {{^id}}
+            <td class="col-5"></td>
+            {{/id}}
+        </tr>
+        {{/categorydata}}
+    </tbody>
+</table>
+{{/categories}}
+{{#js}}
+require([
+    'core_course/recommendations',
+], function(Recommendations) {
+    Recommendations.init();
+});
+{{/js}}
\ No newline at end of file
index 6822d32..2bb8a14 100644 (file)
@@ -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';
index a4d8531..1d1da04 100644 (file)
@@ -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},
 
index 96d071a..0ae3f80 100644 (file)
@@ -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';
index 8e33d64..95e1d4f 100644 (file)
@@ -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,
+        ]
+    ]
 );
index 2fd4a36..ce4c641 100644 (file)
@@ -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,
+    ],
 );
index 3579809..9442f9c 100644 (file)
@@ -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',
index e7f1c98..844e310 100644 (file)
@@ -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