MDL-67883 core: Make core ready for MoodleNet.
authorMathew May <mathewm@hotmail.co.nz>
Fri, 5 Jun 2020 02:09:37 +0000 (10:09 +0800)
committerMathew May <mathewm@hotmail.co.nz>
Fri, 5 Jun 2020 03:47:42 +0000 (11:47 +0800)
32 files changed:
admin/settings/subsystems.php
admin/settings/users.php
course/amd/build/activitychooser.min.js
course/amd/build/activitychooser.min.js.map
course/amd/build/local/activitychooser/dialogue.min.js
course/amd/build/local/activitychooser/dialogue.min.js.map
course/amd/build/local/activitychooser/repository.min.js
course/amd/build/local/activitychooser/repository.min.js.map
course/amd/src/activitychooser.js
course/amd/src/local/activitychooser/dialogue.js
course/amd/src/local/activitychooser/repository.js
course/classes/local/entity/activity_chooser_footer.php [new file with mode: 0644]
course/externallib.php
course/templates/activitychooser.mustache
course/templates/local/activitychooser/footer_partial.mustache [new file with mode: 0644]
course/templates/local/activitychooser/help.mustache
course/tests/behat/activity_chooser.feature
lang/en/admin.php
lang/en/user.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/myprofilelib.php
mod/url/lib.php
pix/MoodleNet.png [new file with mode: 0644]
pix/MoodleNet.svg [new file with mode: 0644]
theme/boost/scss/moodle/core.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/classes/privacy/provider.php
user/editlib.php
version.php

index 14a1331..01de61c 100644 (file)
@@ -74,7 +74,4 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
             new lang_string('configallowemojipickerincompatible', 'admin')
         ));
     }
-
-    $optionalsubsystems->add(new admin_setting_configcheckbox('enablemoodlenet', new lang_string('enablemoodlenet', 'admin'),
-        new lang_string('enablemoodlenet_desc', 'admin'), 1, 1, 0));
 }
index b9612cb..0b7c4a4 100644 (file)
@@ -186,6 +186,7 @@ if ($hassiteconfig
                              'email' => new lang_string('email'),
                              'city' => new lang_string('city'),
                              'country' => new lang_string('country'),
+                             'moodlenetprofile' => new lang_string('moodlenetprofile', 'user'),
                              'timezone' => new lang_string('timezone'),
                              'webpage' => new lang_string('webpage'),
                              'icqnumber' => new lang_string('icqnumber'),
index 82e4295..27aebcf 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js and b/course/amd/build/activitychooser.min.js differ
index 288328b..055d5ac 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js.map and b/course/amd/build/activitychooser.min.js.map differ
index dde9eec..dbb33f4 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js and b/course/amd/build/local/activitychooser/dialogue.min.js differ
index 6f8a885..5c2bb8f 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js.map and b/course/amd/build/local/activitychooser/dialogue.min.js.map differ
index e7e7505..44e5ee1 100644 (file)
Binary files a/course/amd/build/local/activitychooser/repository.min.js and b/course/amd/build/local/activitychooser/repository.min.js differ
index e7bb504..0469db6 100644 (file)
Binary files a/course/amd/build/local/activitychooser/repository.min.js.map and b/course/amd/build/local/activitychooser/repository.min.js.map differ
index 3704632..176ed99 100644 (file)
@@ -85,6 +85,20 @@ const registerListenerEvents = (courseId, chooserConfig) => {
         };
     })();
 
+    const fetchFooterData = (() => {
+        let footerInnerPromise = null;
+
+        return (sectionId) => {
+            if (!footerInnerPromise) {
+                footerInnerPromise = new Promise((resolve) => {
+                    resolve(Repository.fetchFooterData(courseId, sectionId));
+                });
+            }
+
+            return footerInnerPromise;
+        };
+    })();
+
     CustomEvents.define(document, events);
 
     // Display module chooser event listeners.
@@ -115,7 +129,8 @@ const registerListenerEvents = (courseId, chooserConfig) => {
                     bodyPromiseResolver = resolve;
                 });
 
-                const sectionModal = buildModal(bodyPromise);
+                const footerData = await fetchFooterData(caller.dataset.sectionid);
+                const sectionModal = buildModal(bodyPromise, footerData);
 
                 // Now we have a modal we should start fetching data.
                 const data = await fetchModuleData();
@@ -127,6 +142,7 @@ const registerListenerEvents = (courseId, chooserConfig) => {
                     sectionModal,
                     builtModuleData,
                     partiallyAppliedFavouriteManager(data, caller.dataset.sectionid),
+                    footerData,
                 );
 
                 bodyPromiseResolver(await Templates.render(
@@ -221,13 +237,15 @@ const templateDataBuilder = (data, chooserConfig) => {
  *
  * @method buildModal
  * @param {Promise} bodyPromise
+ * @param {String|Boolean} footer Either a footer to add or nothing
  * @return {Object} The modal ready to display immediately and render body in later.
  */
-const buildModal = bodyPromise => {
+const buildModal = (bodyPromise, footer) => {
     return ModalFactory.create({
         type: ModalFactory.types.DEFAULT,
         title: getString('addresourceoractivity'),
         body: bodyPromise,
+        footer: footer.customfootertemplate,
         large: true,
         templateContext: {
             classes: 'modchooser'
index 3cbc5e8..cd045a3 100644 (file)
@@ -31,6 +31,7 @@ import {addIconToContainer} from 'core/loadingicon';
 import * as Repository from 'core_course/local/activitychooser/repository';
 import Notification from 'core/notification';
 import {debounce} from 'core/utils';
+const getPlugin = pluginName => import(pluginName);
 
 /**
  * Given an event from the main module 'page' navigate to it's help section via a carousel.
@@ -38,8 +39,13 @@ import {debounce} from 'core/utils';
  * @method showModuleHelp
  * @param {jQuery} carousel Our initialized carousel to manipulate
  * @param {Object} moduleData Data of the module to carousel to
+ * @param {jQuery} modal We need to figure out if the current modal has a footer.
  */
-const showModuleHelp = (carousel, moduleData) => {
+const showModuleHelp = (carousel, moduleData, modal = null) => {
+    // If we have a real footer then we need to change temporarily.
+    if (modal !== null && moduleData.showFooter === true) {
+        modal.setFooter(Templates.render('core_course/local/activitychooser/footer_partial', moduleData));
+    }
     const help = carousel.find(selectors.regions.help)[0];
     help.innerHTML = '';
     help.classList.add('m-auto');
@@ -107,8 +113,9 @@ const manageFavouriteState = async(modalBody, caller, partialFavourite) => {
  * @param {Promise} modal Our modal that we are working with
  * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
  * @param {Function} partialFavourite Partially applied function we need to manage favourite status
+ * @param {Object} footerData Our base footer object.
  */
-const registerListenerEvents = (modal, mappedModules, partialFavourite) => {
+const registerListenerEvents = (modal, mappedModules, partialFavourite, footerData) => {
     const bodyClickListener = async(e) => {
         if (e.target.closest(selectors.actions.optionActions.showSummary)) {
             const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
@@ -116,7 +123,9 @@ const registerListenerEvents = (modal, mappedModules, partialFavourite) => {
             const module = e.target.closest(selectors.regions.chooserOption.container);
             const moduleName = module.dataset.modname;
             const moduleData = mappedModules.get(moduleName);
-            showModuleHelp(carousel, moduleData);
+            // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.
+            moduleData.showFooter = modal.hasFooterContent();
+            showModuleHelp(carousel, moduleData, modal);
         }
 
         if (e.target.closest(selectors.actions.optionActions.manageFavourite)) {
@@ -128,7 +137,7 @@ const registerListenerEvents = (modal, mappedModules, partialFavourite) => {
             const firstChooserOption = sectionChooserOptions
                 .querySelector(selectors.regions.chooserOption.container);
             toggleFocusableChooserOption(firstChooserOption, true);
-            initChooserOptionsKeyboardNavigation(modal.getBody()[0], mappedModules, sectionChooserOptions);
+            initChooserOptionsKeyboardNavigation(modal.getBody()[0], mappedModules, sectionChooserOptions, modal);
         }
 
         // From the help screen go back to the module overview.
@@ -150,7 +159,18 @@ const registerListenerEvents = (modal, mappedModules, partialFavourite) => {
             const searchInput = modal.getBody()[0].querySelector(selectors.actions.search);
             searchInput.value = "";
             searchInput.focus();
-            toggleSearchResultsView(modal.getBody()[0], mappedModules, searchInput.value);
+            toggleSearchResultsView(modal, mappedModules, searchInput.value);
+        }
+    };
+
+    // We essentially have two types of footer.
+    // A fake one that is handled within the template for chooser_help and then all of the stuff for
+    // modal.footer. We need to ensure we know exactly what type of footer we are using so we know what we
+    // need to manage. The below code handles a real footer going to a mnet carousel item.
+    const footerClickListener = async(e) => {
+        if (footerData.footer === true) {
+            const footerjs = await getPlugin(footerData.customfooterjs);
+            await footerjs.footerClickListener(e, footerData, modal);
         }
     };
 
@@ -183,7 +203,7 @@ const registerListenerEvents = (modal, mappedModules, partialFavourite) => {
         // The search input is triggered.
         searchInput.addEventListener('input', debounce(() => {
             // Display the search results.
-            toggleSearchResultsView(body, mappedModules, searchInput.value);
+            toggleSearchResultsView(modal, mappedModules, searchInput.value);
         }, 300));
         return body;
     })
@@ -197,12 +217,22 @@ const registerListenerEvents = (modal, mappedModules, partialFavourite) => {
 
         toggleFocusableChooserOption(firstChooserOption, true);
         initTabsKeyboardNavigation(body);
-        initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions);
+        initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal);
 
         return body;
     })
     .catch();
 
+    modal.getFooterPromise()
+
+    // The return value of getBodyPromise is a jquery object containing the body NodeElement.
+    .then(footer => footer[0])
+    // Add the listener for clicks on the footer.
+    .then(footer => {
+        footer.addEventListener('click', footerClickListener);
+        return footer;
+    })
+    .catch();
 };
 
 /**
@@ -283,8 +313,9 @@ const initTabsKeyboardNavigation = (body) => {
  * @param {HTMLElement} body Our modal that we are working with
  * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
  * @param {HTMLElement} chooserOptionsContainer The section that contains the chooser items
+ * @param {Object} modal Our created modal for the section
  */
-const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOptionsContainer) => {
+const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOptionsContainer, modal = null) => {
     const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container);
 
     Array.from(chooserOptions).forEach((element) => {
@@ -303,7 +334,10 @@ const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOption
                         pause: true,
                         keyboard: false
                     });
-                    showModuleHelp(carousel, moduleData);
+
+                    // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.
+                    moduleData.showFooter = modal.hasFooterContent();
+                    showModuleHelp(carousel, moduleData, modal);
                 }
             }
 
@@ -424,11 +458,12 @@ const renderSearchResults = async(searchResultsContainer, searchResultsData) =>
  * Toggle (display/hide) the search results depending on the value of the search query
  *
  * @method toggleSearchResultsView
- * @param {HTMLElement} modalBody The body of the created modal for the section
+ * @param {Object} modal Our created modal for the section
  * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
  * @param {String} searchQuery The search query
  */
-const toggleSearchResultsView = async(modalBody, mappedModules, searchQuery) => {
+const toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {
+    const modalBody = modal.getBody()[0];
     const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults);
     const chooserContainer = modalBody.querySelector(selectors.regions.chooser);
     const clearSearchButton = modalBody.querySelector(selectors.elements.clearsearch);
@@ -443,7 +478,7 @@ const toggleSearchResultsView = async(modalBody, mappedModules, searchQuery) =>
             // Set the first result item to be focusable.
             toggleFocusableChooserOption(firstSearchResultItem, true);
             // Register keyboard events on the created search result items.
-            initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer);
+            initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal);
         }
         // Display the "clear" search button in the activity chooser search bar.
         searchIcon.classList.add('d-none');
@@ -513,7 +548,7 @@ const setupKeyboardAccessibility = (modal, mappedModules) => {
             disableFocusAllChooserOptions(prevActiveSectionChooserOptions);
             // Enable the focus of the first chooser option in the current active section.
             toggleFocusableChooserOption(firstChooserOption, true);
-            initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions);
+            initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions, modal);
         });
         return;
     }).catch(Notification.exception);
@@ -539,8 +574,9 @@ const disableFocusAllChooserOptions = (sectionChooserOptions) => {
  * @param {Promise} modalPromise Our created modal for the section
  * @param {Array} sectionModules An array of all of the built module information
  * @param {Function} partialFavourite Partially applied function we need to manage favourite status
+ * @param {Object} footerData Our base footer object.
  */
-export const displayChooser = (modalPromise, sectionModules, partialFavourite) => {
+export const displayChooser = (modalPromise, sectionModules, partialFavourite, footerData) => {
     // Make a map so we can quickly fetch a specific module's object for either rendering or searching.
     const mappedModules = new Map();
     sectionModules.forEach((module) => {
@@ -549,7 +585,7 @@ export const displayChooser = (modalPromise, sectionModules, partialFavourite) =
 
     // Register event listeners.
     modalPromise.then(modal => {
-        registerListenerEvents(modal, mappedModules, partialFavourite);
+        registerListenerEvents(modal, mappedModules, partialFavourite, footerData);
 
         // We want to focus on the first chooser option element as soon as the modal is opened.
         setupKeyboardAccessibility(modal, mappedModules);
index 52c5444..43de8c1 100644 (file)
@@ -78,3 +78,22 @@ export const unfavouriteModule = (modName, modID) => {
     };
     return ajax.call([request])[0];
 };
+
+/**
+ * Fetch all the information on modules we'll need in the activity chooser.
+ *
+ * @method fetchFooterData
+ * @param {Number} courseid What course to fetch the data for
+ * @param {Number} sectionid What section to fetch the data for
+ * @return {object} jQuery promise
+ */
+export const fetchFooterData = (courseid, sectionid) => {
+    const request = {
+        methodname: 'core_course_get_activity_chooser_footer',
+        args: {
+            courseid: courseid,
+            sectionid: sectionid,
+        },
+    };
+    return ajax.call([request])[0];
+};
diff --git a/course/classes/local/entity/activity_chooser_footer.php b/course/classes/local/entity/activity_chooser_footer.php
new file mode 100644 (file)
index 0000000..2524d24
--- /dev/null
@@ -0,0 +1,86 @@
+<?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/>.
+
+/**
+ * Activity Chooser footer data class.
+ *
+ * @package    core
+ * @subpackage course
+ * @copyright  2020 Mathew May <mathew.solutions>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_course\local\entity;
+
+/**
+ * A class to represent the Activity Chooser footer data.
+ *
+ * @package    core
+ * @subpackage course
+ * @copyright  2020 Mathew May <mathew.solutions>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class activity_chooser_footer {
+
+    /** @var string $footerjspath The path to the plugin JS file to dynamically import later. */
+    protected $footerjspath;
+
+    /** @var string $footertemplate The rendered template for the footer. */
+    protected $footertemplate;
+
+    /** @var string $carouseltemplate The rendered template for the footer. */
+    protected $carouseltemplate;
+
+    /**
+     * Constructor method.
+     *
+     * @param string $footerjspath JS file to dynamically import later.
+     * @param string $footertemplate Footer template that has been rendered.
+     * @param string|null $carouseltemplate Carousel template that may have been rendered.
+     */
+    public function __construct(string $footerjspath, string $footertemplate, ?string $carouseltemplate = '') {
+        $this->footerjspath = $footerjspath;
+        $this->footertemplate = $footertemplate;
+        $this->carouseltemplate = $carouseltemplate;
+    }
+
+    /**
+     *  Get the footer JS file path for this plugin.
+     *
+     * @return string The JS file to call functions from.
+     */
+    public function get_footer_js_file(): string {
+        return $this->footerjspath;
+    }
+
+    /**
+     * Get the footer rendered template for this plugin.
+     *
+     * @return string The template that has been rendered for the chooser footer.
+     */
+    public function get_footer_template(): string {
+        return $this->footertemplate;
+    }
+
+    /**
+     * Get the carousel rendered template for this plugin.
+     *
+     * @return string The template that has been rendered for the chooser carousel.
+     */
+    public function get_carousel_template(): string {
+        return $this->carouseltemplate;
+    }
+}
index 5ddf89e..fe3e80b 100644 (file)
@@ -4336,4 +4336,74 @@ class core_course_external extends external_api {
             ]
         );
     }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     */
+    public static function get_activity_chooser_footer_parameters() {
+        return new external_function_parameters([
+            'courseid' => new external_value(PARAM_INT, 'ID of the course', VALUE_REQUIRED),
+            'sectionid' => new external_value(PARAM_INT, 'ID of the section', VALUE_REQUIRED),
+        ]);
+    }
+
+    /**
+     * Given a course ID we need to build up a footre for the chooser.
+     *
+     * @param int $courseid The course we want to fetch the modules for
+     * @param int $sectionid The section we want to fetch the modules for
+     * @return array
+     */
+    public static function get_activity_chooser_footer(int $courseid, int $sectionid) {
+        [
+            'courseid' => $courseid,
+            'sectionid' => $sectionid,
+        ] = self::validate_parameters(self::get_activity_chooser_footer_parameters(), [
+            'courseid' => $courseid,
+            'sectionid' => $sectionid,
+        ]);
+
+        $coursecontext = context_course::instance($courseid);
+        self::validate_context($coursecontext);
+
+        $pluginswithfunction = get_plugins_with_function('custom_chooser_footer', 'lib.php');
+        if ($pluginswithfunction) {
+            foreach ($pluginswithfunction as $plugintype => $plugins) {
+                foreach ($plugins as $pluginfunction) {
+                    $footerdata = $pluginfunction($courseid, $sectionid);
+                    break; // Only a single plugin can modify the footer.
+                }
+                break; // Only a single plugin can modify the footer.
+            }
+            return [
+                'footer' => true,
+                'customfooterjs' => $footerdata->get_footer_js_file(),
+                'customfootertemplate' => $footerdata->get_footer_template(),
+                'customcarouseltemplate' => $footerdata->get_carousel_template(),
+            ];
+        } else {
+            return [
+                'footer' => false,
+            ];
+        }
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     */
+    public static function get_activity_chooser_footer_returns() {
+        return new external_single_structure(
+            [
+                'footer' => new external_value(PARAM_BOOL, 'Is a footer being return by this request?', VALUE_REQUIRED),
+                'customfooterjs' => new external_value(PARAM_RAW, 'The path to the plugin JS file', VALUE_OPTIONAL),
+                'customfootertemplate' => new external_value(PARAM_RAW, 'The prerendered footer', VALUE_OPTIONAL),
+                'customcarouseltemplate' => new external_value(PARAM_RAW, 'Either "" or the prerendered carousel page',
+                    VALUE_OPTIONAL),
+            ]
+        );
+    }
 }
index 4511df9..bf6ba5e 100644 (file)
             </div>
         </div>
         <div class="carousel-item" data-region="help"></div>
+        <!--The following div is used as a place for additional plugins to have widgets in the chooser.-->
+        <div class="carousel-item" data-region="pluginCarousel"></div>
     </div>
 </div>
diff --git a/course/templates/local/activitychooser/footer_partial.mustache b/course/templates/local/activitychooser/footer_partial.mustache
new file mode 100644 (file)
index 0000000..898d119
--- /dev/null
@@ -0,0 +1,33 @@
+{{!
+    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/local/activitychooser/footer_partial
+
+    Chooser favourite template partial.
+
+    Example context (json):
+    {
+    }
+}}
+<div class="w-100 d-flex justify-content-between" data-region="chooser-option-summary-actions-container">
+    <button data-action="close-chooser-option-summary" class="closeoptionsummary btn btn-secondary" tabindex="0" data-modname="{{componentname}}_{{link}}">
+        {{#str}} back {{/str}}
+    </button>
+    <a href="{{link}}" title="{{#str}} addnew, moodle, {{title}} {{/str}}" data-action="add-chooser-option" class="addoption btn btn-primary" tabindex="0">
+        {{#str}} add {{/str}}
+    </a>
+</div>
index 68798bc..9ddacd4 100644 (file)
             {{{help}}}
         </div>
     </div>
-    <div class="actions fixed-bottom w-100 d-flex justify-content-between position-absolute py-3 px-4" data-region="chooser-option-summary-actions-container">
-        <button data-action="close-chooser-option-summary" class="closeoptionsummary btn btn-secondary" tabindex="0" data-modname="{{componentname}}_{{link}}">
-            {{#str}} back {{/str}}
-        </button>
-        <a href="{{link}}" title="{{#str}} addnew, moodle, {{title}} {{/str}}" data-action="add-chooser-option" class="addoption btn btn-primary" tabindex="0">
-            {{#str}} add {{/str}}
-        </a>
-    </div>
+    {{^showFooter}}
+        <div class="fixed-bottom position-absolute py-3 px-4 border-top">
+            {{>core_course/local/activitychooser/footer_partial}}
+        </div>
+    {{/showFooter}}
 </div>
index b71e1e5..3748d0f 100644 (file)
@@ -14,6 +14,8 @@ Feature: Display and choose from the available activities in course
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher | C | editingteacher |
+    And the following config values are set as admin:
+      | enablemoodlenet | 0 | tool_moodlenet |
     And I log in as "teacher"
     And I am on "Course" course homepage with editing mode on
 
index d47cd39..b21142e 100644 (file)
@@ -539,8 +539,6 @@ $string['enableglobalsearch_desc'] = 'If enabled, data will be indexed and synch
 $string['enablegravatar'] = 'Enable Gravatar';
 $string['enablegravatar_help'] = 'When enabled Moodle will attempt to fetch a user profile picture from Gravatar if the user has not uploaded an image.';
 $string['enablemobilewebservice'] = 'Enable web services for mobile devices';
-$string['enablemoodlenet'] = 'Enable integration with MoodleNet instances';
-$string['enablemoodlenet_desc'] = 'If enabled, and provided the MoodleNet plugin is installed, users can import content from MoodleNet into this site.';
 $string['enablerecordcache'] = 'Enable record cache';
 $string['enablerssfeeds'] = 'Enable RSS feeds';
 $string['enablesearchareas'] = 'Enable search areas';
index 5eee2cb..ee6a9a8 100644 (file)
@@ -33,6 +33,7 @@ $string['countparticipantsfound'] = '{$a} participants found';
 $string['filtersetmatchdescription'] = 'How multiple filters should be combined';
 $string['match'] = 'Match';
 $string['matchofthefollowing'] = 'of the following:';
+$string['moodlenetprofile'] = 'MoodleNet profile';
 $string['placeholdertypeorselect'] = 'Type or select...';
 $string['placeholdertype'] = 'Type...';
 $string['privacy:courserequestpath'] = 'Requested courses';
@@ -87,6 +88,7 @@ $string['privacy:metadata:maildisplay'] = 'A preference for the user about displ
 $string['privacy:metadata:middlename'] = 'The middle name of the user';
 $string['privacy:metadata:mnethostid'] = 'An identifier for the MNet host if used';
 $string['privacy:metadata:model'] = 'The device name, occam or iPhone etc..';
+$string['privacy:metadata:moodlenetprofile'] = 'The MoodleNet profile for the user';
 $string['privacy:metadata:msn'] = 'The MSN identifier of the user';
 $string['privacy:metadata:my_pages'] = 'User pages - dashboard and profile. This table does not contain personal data and only used to link dashboard blocks to users';
 $string['privacy:metadata:my_pages:name'] = 'Page name';
index be3ac00..8f91417 100644 (file)
         <FIELD NAME="firstnamephonetic" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="First name phonetic"/>
         <FIELD NAME="middlename" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Middle name"/>
         <FIELD NAME="alternatename" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Alternate name - Useful for three-name countries."/>
+        <FIELD NAME="moodlenetprofile" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Moodle.net profile information"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index 1a9ae7f..b900c1a 100644 (file)
@@ -671,6 +671,14 @@ $functions = array(
         'type' => 'read',
         'ajax' => true,
     ),
+    'core_course_get_activity_chooser_footer' => array(
+        'classname' => 'core_course_external',
+        'methodname' => 'get_activity_chooser_footer',
+        'classpath' => 'course/externallib.php',
+        'description' => 'Fetch the data for the activity chooser footer.',
+        'type' => 'read',
+        'ajax' => true,
+    ),
     'core_course_toggle_activity_recommendation' => array(
         'classname' => 'core_course_external',
         'methodname' => 'toggle_activity_recommendation',
index d0d822d..b6348d1 100644 (file)
@@ -2427,5 +2427,19 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2020052200.01);
     }
 
+    if ($oldversion < 2020060500.01) {
+        // Define field moodlenetprofile to be added to user.
+        $table = new xmldb_table('user');
+        $field = new xmldb_field('moodlenetprofile', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'alternatename');
+
+        // Conditionally launch add field moodlenetprofile.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2020060500.01);
+    }
+
     return true;
 }
index 87c792d..730ef6e 100644 (file)
@@ -165,6 +165,12 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
         $tree->add_node($node);
     }
 
+    if (!isset($hiddenfields['moodlenetprofile']) && $user->moodlenetprofile) {
+        $node = new core_user\output\myprofile\node('contact', 'moodlenetprofile', get_string('moodlenetprofile', 'user'), null,
+                null, $user->moodlenetprofile);
+        $tree->add_node($node);
+    }
+
     if (!isset($hiddenfields['country']) && $user->country) {
         $node = new core_user\output\myprofile\node('contact', 'country', get_string('country'), null, null,
                 get_string($user->country, 'countries'));
index 23d11a0..815360d 100644 (file)
@@ -318,6 +318,7 @@ function url_dndupload_handle($uploadinfo) {
     $data->introformat = FORMAT_HTML;
     $data->externalurl = clean_param($uploadinfo->content, PARAM_URL);
     $data->timemodified = time();
+    $data->coursemodule = $uploadinfo->coursemodule;
 
     // Set the display options to the site defaults.
     $config = get_config('url');
diff --git a/pix/MoodleNet.png b/pix/MoodleNet.png
new file mode 100644 (file)
index 0000000..872756b
Binary files /dev/null and b/pix/MoodleNet.png differ
diff --git a/pix/MoodleNet.svg b/pix/MoodleNet.svg
new file mode 100644 (file)
index 0000000..8f191bf
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 934.36 169.63"><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M762.37,40.25a8.45,8.45,0,0,0-8.44,8.44v88.4l-72-91.84c-1.9-2.41-4.22-4.65-8.28-4.65H671.7A8.56,8.56,0,0,0,663.26,49V160a8.37,8.37,0,0,0,8.27,8.44A8.45,8.45,0,0,0,680,160V69.11l73.63,94.16c2.13,2.48,4.62,4.78,8.42,4.78h.7a7.83,7.83,0,0,0,7.92-8.09V48.69A8.37,8.37,0,0,0,762.37,40.25Z" style="fill:#414443"/><path d="M872.92,119.11c0-23.7-15.06-47.7-43.83-47.7a43.32,43.32,0,0,0-32.44,14.36c-8.25,9.1-12.8,21.37-12.8,34.58v.35c0,27.89,20.21,48.93,47,48.93,14.28,0,25.21-4.37,35.37-14.12a7.4,7.4,0,0,0,2.65-5.59,7.43,7.43,0,0,0-12.5-5.38c-7.52,6.92-15.51,10.15-25.17,10.15-16,0-27.93-11-30.27-27.66H865A7.92,7.92,0,0,0,872.92,119.11ZM828.74,86c11.75,0,24.9,7.35,27.31,27.83H800.93C803.23,97.36,814.46,86,828.74,86Z" style="fill:#414443"/><path d="M926.62,152.4a7.44,7.44,0,0,0-1.88.35,23.35,23.35,0,0,1-6.39.88c-9.38,0-13.75-4.31-13.75-13.57V88.47h22a7.66,7.66,0,0,0,7.74-7.56,7.75,7.75,0,0,0-7.74-7.56h-22V53.44A8.55,8.55,0,0,0,896.16,45a8.27,8.27,0,0,0-8.26,8.44V73.35h-5.82a7.66,7.66,0,0,0-7.56,7.56,7.75,7.75,0,0,0,7.56,7.56h5.82v53.35c0,17.6,9.69,27.28,27.28,27.28a35.66,35.66,0,0,0,13.94-2.57,7.23,7.23,0,0,0,4.89-6.75A7.4,7.4,0,0,0,926.62,152.4Z" style="fill:#414443"/><path d="M153.57,164.26V106.85q0-18-14.87-18t-14.88,18v57.41H94.6V106.85q0-18-14.62-18-14.88,0-14.87,18v57.41H35.88v-60.8q0-18.78,13-28.43Q60.41,66.41,80,66.41q19.83,0,29.23,10.18,8.08-10.19,29.49-10.18,19.56,0,31,8.62,13,9.64,13,28.43v60.8Z" style="fill:#f98012"/><path d="M511.75,164V0H541V164Z" style="fill:#f98012"/><path d="M474.47,164v-9.66q-3.92,5.22-13.32,8.36a49.12,49.12,0,0,1-15.93,2.87q-20.89,0-33.55-14.37T399,115.68c0-13.92,4.11-25.61,12.41-35,7.34-8.3,19.28-14.1,33-14.1,15.49,0,24.54,5.82,30,12.53V0h28.46V164Zm0-54.57q0-7.85-7.44-15t-15.28-7.19A20.73,20.73,0,0,0,434,96.36q-5.75,8.1-5.74,19.84,0,11.49,5.74,19.59,6.54,9.41,17.76,9.4,6.79,0,14.75-6.4t8-13.19Z" style="fill:#f98012"/><path d="M343.91,166.6q-22.2,0-36.69-14.1t-14.5-36.3q0-22.19,14.5-36.29T343.91,65.8q22.18,0,36.82,14.11t14.62,36.29q0,22.2-14.62,36.3T343.91,166.6Zm0-77.29q-10.57,0-16.26,8a32.07,32.07,0,0,0-5.67,19q0,11,5.28,18.63a20.35,20.35,0,0,0,33.3,0q5.54-7.6,5.54-18.63t-5.28-18.63Q354.73,89.31,343.91,89.31Z" style="fill:#f98012"/><path d="M238.15,166.6q-22.2,0-36.69-14.1T187,116.2Q187,94,201.46,79.91T238.15,65.8q22.18,0,36.82,14.11t14.62,36.29q0,22.2-14.62,36.3T238.15,166.6Zm0-77.29q-10.56,0-16.26,8a32.08,32.08,0,0,0-5.68,19q0,11,5.29,18.63a20.35,20.35,0,0,0,33.3,0q5.55-7.6,5.55-18.63t-5.29-18.63Q249,89.31,238.15,89.31Z" style="fill:#f98012"/><path d="M575.64,125.08c.62,7,9.67,21.94,24.55,21.94,14.48,0,21.33-8.36,21.67-11.75l30.81-.27c-3.36,10.28-17,32.13-53,32.13-15,0-28.68-4.66-38.52-14s-14.75-21.46-14.75-36.43q0-23.25,14.75-36.95T599.4,66.07q25.59,0,40,17,13.32,15.67,13.32,42Zm47.78-18a29.09,29.09,0,0,0-7.82-15.4,22,22,0,0,0-15.68-6.53,20.53,20.53,0,0,0-15.28,6.26,31.66,31.66,0,0,0-8.22,15.67Z" style="fill:#f98012"/><path d="M92.65,62l29-21.2-.37-1.29C68.94,45.9,45.11,50.45,0,76.6l.42,1.19,3.59,0a180.84,180.84,0,0,0-.17,26c-5,14.48-.13,24.33,4.45,35,.72-11.15.65-23.34-2.77-35.47a180.08,180.08,0,0,1,.19-25.51l29.91.29a135.7,135.7,0,0,0,.89,17.54c26.72,9.39,53.6,0,67.86-23.19C100.42,68,92.65,62,92.65,62Z" style="fill:#363636"/></g></g></svg>
\ No newline at end of file
index 2bfa166..411f762 100644 (file)
@@ -1637,6 +1637,17 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
     }
 }
 
+.modchooser .modal-footer {
+    height: 70px;
+    .moodlenet-logo {
+        .icon {
+            height: 2.5rem;
+            width: 6rem;
+            margin-bottom: .6rem;
+        }
+    }
+}
+
 .modchoosercontainer.noscroll {
     overflow-y: hidden;
 }
@@ -1684,7 +1695,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
     background-color: $white;
     overflow-x: hidden;
     overflow-y: auto;
-    min-height: 640px;
+    height: 640px;
 
     .content {
         overflow-y: auto;
@@ -2447,6 +2458,10 @@ body.h5p-embed {
     overflow-wrap: break-word !important; /* stylelint-disable-line declaration-no-important */
 }
 
+.z-index-0 {
+    z-index: 0 !important; /* stylelint-disable-line declaration-no-important */
+}
+
 .z-index-1 {
     z-index: 1 !important; /* stylelint-disable-line declaration-no-important */
 }
@@ -2653,4 +2668,4 @@ $picker-emojis-per-row: 7 !default;
         position: relative;
         z-index: inherit;
     }
-}
\ No newline at end of file
+}
index 5dda75a..b2d48fe 100644 (file)
@@ -11001,6 +11001,13 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
   .modchooser .modal-body .carousel-item .loading-icon .icon {
     margin: 1em auto; }
 
+.modchooser .modal-footer {
+  height: 70px; }
+  .modchooser .modal-footer .moodlenet-logo .icon {
+    height: 2.5rem;
+    width: 6rem;
+    margin-bottom: .6rem; }
+
 .modchoosercontainer.noscroll {
   overflow-y: hidden; }
 
@@ -11038,7 +11045,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
   background-color: #fff;
   overflow-x: hidden;
   overflow-y: auto;
-  min-height: 640px; }
+  height: 640px; }
   .modchooser .modal-body .optionsummary .content {
     overflow-y: auto; }
     .modchooser .modal-body .optionsummary .content .heading .icon {
@@ -11631,6 +11638,10 @@ body.h5p-embed .h5pmessages {
   overflow-wrap: break-word !important;
   /* stylelint-disable-line declaration-no-important */ }
 
+.z-index-0 {
+  z-index: 0 !important;
+  /* stylelint-disable-line declaration-no-important */ }
+
 .z-index-1 {
   z-index: 1 !important;
   /* stylelint-disable-line declaration-no-important */ }
index ab07c5f..e2c098d 100644 (file)
@@ -11208,6 +11208,13 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
   .modchooser .modal-body .carousel-item .loading-icon .icon {
     margin: 1em auto; }
 
+.modchooser .modal-footer {
+  height: 70px; }
+  .modchooser .modal-footer .moodlenet-logo .icon {
+    height: 2.5rem;
+    width: 6rem;
+    margin-bottom: .6rem; }
+
 .modchoosercontainer.noscroll {
   overflow-y: hidden; }
 
@@ -11245,7 +11252,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
   background-color: #fff;
   overflow-x: hidden;
   overflow-y: auto;
-  min-height: 640px; }
+  height: 640px; }
   .modchooser .modal-body .optionsummary .content {
     overflow-y: auto; }
     .modchooser .modal-body .optionsummary .content .heading .icon {
@@ -11842,6 +11849,10 @@ body.h5p-embed .h5pmessages {
   overflow-wrap: break-word !important;
   /* stylelint-disable-line declaration-no-important */ }
 
+.z-index-0 {
+  z-index: 0 !important;
+  /* stylelint-disable-line declaration-no-important */ }
+
 .z-index-1 {
   z-index: 1 !important;
   /* stylelint-disable-line declaration-no-important */ }
index 839026e..74b28a8 100644 (file)
@@ -104,7 +104,8 @@ class provider implements
             'lastnamephonetic' => 'privacy:metadata:lastnamephonetic',
             'firstnamephonetic' => 'privacy:metadata:firstnamephonetic',
             'middlename' => 'privacy:metadata:middlename',
-            'alternatename' => 'privacy:metadata:alternatename'
+            'alternatename' => 'privacy:metadata:alternatename',
+            'moodlenetprofile' => 'privacy:metadata:moodlenetprofile'
         ];
 
         $passwordhistory = [
index c8d2f68..b3ec838 100644 (file)
@@ -300,6 +300,9 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
     $mform->setDefault('maildisplay', core_user::get_property_default('maildisplay'));
     $mform->addHelpButton('maildisplay', 'emaildisplay');
 
+    $mform->addElement('text', 'moodlenetprofile', get_string('moodlenetprofile', 'user'));
+    $mform->setType('moodlenetprofile', PARAM_RAW_TRIMMED);
+
     $mform->addElement('text', 'city', get_string('city'), 'maxlength="120" size="21"');
     $mform->setType('city', PARAM_TEXT);
     if (!empty($CFG->defaultcity)) {
index f506718..797c891 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2020060200.01;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2020060500.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 $release  = '3.9dev+ (Build: 20200602)'; // Human-friendly version name