Merge branch 'install_master' of https://git.in.moodle.com/amosbot/moodle-install
authorJake Dallimore <jake@moodle.com>
Fri, 4 Dec 2020 00:42:31 +0000 (08:42 +0800)
committerJake Dallimore <jake@moodle.com>
Fri, 4 Dec 2020 00:42:31 +0000 (08:42 +0800)
30 files changed:
admin/settings/analytics.php
analytics/classes/manager.php
analytics/upgrade.txt
blocks/classes/external/fetch_addable_blocks.php [new file with mode: 0644]
customfield/externallib.php
h5p/classes/player.php
lang/en/analytics.php
lib/amd/build/addblockmodal.min.js
lib/amd/build/addblockmodal.min.js.map
lib/amd/src/addblockmodal.js
lib/classes/antivirus/scanner.php
lib/db/services.php
lib/deprecatedlib.php
lib/editor/atto/tests/behat/disablecontrol.feature
lib/editor/textarea/tests/behat/disablecontrol.feature
lib/myprofilelib.php
lib/navigationlib.php
lib/templates/add_block_body.mustache
lib/tests/behat/behat_general.php
lib/upgrade.txt
message/classes/api.php
message/tests/api_test.php
message/tests/externallib_test.php
mod/forum/classes/local/exporters/post.php
mod/forum/tests/externallib_test.php
mod/forum/upgrade.txt
mod/h5pactivity/classes/local/manager.php
question/tests/backup_test.php
user/view.php
version.php

index 30b6a22..1bf0cb3 100644 (file)
@@ -144,5 +144,20 @@ if ($hassiteconfig && \core_analytics\manager::is_analytics_enabled()) {
         $settings->add(new admin_setting_configduration('analytics/modeltimelimit', new lang_string('modeltimelimit', 'analytics'),
             new lang_string('modeltimelimitinfo', 'analytics'), 20 * MINSECS));
 
+        $options = array(
+            0    => new lang_string('neverdelete', 'analytics'),
+            1000 => new lang_string('numdays', '', 1000),
+            365  => new lang_string('numdays', '', 365),
+            180  => new lang_string('numdays', '', 180),
+            150  => new lang_string('numdays', '', 150),
+            120  => new lang_string('numdays', '', 120),
+            90   => new lang_string('numdays', '', 90),
+            60   => new lang_string('numdays', '', 60),
+            35   => new lang_string('numdays', '', 35));
+        $settings->add(new admin_setting_configselect('analytics/calclifetime',
+            new lang_string('calclifetime', 'analytics'),
+            new lang_string('configlcalclifetime', 'analytics'), 35, $options));
+
+
     }
 }
index 054a07a..8bbdf4d 100644 (file)
@@ -300,18 +300,12 @@ class manager {
     }
 
     /**
-     * Returns the enabled time splitting methods.
-     *
-     * @deprecated since Moodle 3.7
-     * @todo MDL-65086 This will be deleted in Moodle 3.11
-     * @see \core_analytics\manager::get_time_splitting_methods_for_evaluation
-     * @return \core_analytics\local\time_splitting\base[]
+     * @deprecated since Moodle 3.7 use get_time_splitting_methods_for_evaluation instead
      */
     public static function get_enabled_time_splitting_methods() {
-        debugging('This function has been deprecated. You can use self::get_time_splitting_methods_for_evaluation if ' .
+        throw new coding_exception(__FUNCTION__ . '() has been removed. You can use self::get_time_splitting_methods_for_evaluation if ' .
             'you want to get the default time splitting methods for evaluation, or you can use self::get_all_time_splittings if ' .
             'you want to get all the time splitting methods available on this site.');
-        return self::get_time_splitting_methods_for_evaluation();
     }
 
     /**
@@ -694,6 +688,13 @@ class manager {
                     $param + $idsparams);
             }
         }
+
+        // Clean up calculations table.
+        $calclifetime = get_config('analytics', 'calclifetime');
+        if (!empty($calclifetime)) {
+            $lifetime = time() - ($calclifetime * DAYSECS); // Value in days.
+            $DB->delete_records_select('analytics_indicator_calc', 'timecreated < ?', [$lifetime]);
+        }
     }
 
     /**
index 4773d4a..9501afb 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in analytics sub system,
 information provided here is intended especially for developers.
 
+=== 3.11 ===
+* Final deprecation get_enabled_time_splitting_methods. Method has been removed. Use
+  get_time_splitting_methods_for_evaluation instead.
+
 === 3.8 ===
 
 * "Time-splitting method" have been replaced by "Analysis interval" for the language strings that are
diff --git a/blocks/classes/external/fetch_addable_blocks.php b/blocks/classes/external/fetch_addable_blocks.php
new file mode 100644 (file)
index 0000000..3e14ae5
--- /dev/null
@@ -0,0 +1,118 @@
+<?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/>.
+
+/**
+ * This is the external method used for fetching the addable blocks in a given page.
+ *
+ * @package    core_block
+ * @since      Moodle 3.11
+ * @copyright  2020 Mihail Geshoski <mihail@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_block\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/externallib.php');
+
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+
+/**
+ * This is the external method used for fetching the addable blocks in a given page.
+ *
+ * @copyright  2020 Mihail Geshoski <mihail@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fetch_addable_blocks extends external_api {
+
+    /**
+     * Describes the parameters for execute.
+     *
+     * @return external_function_parameters
+     */
+    public static function execute_parameters(): external_function_parameters {
+        return new external_function_parameters(
+            [
+                'pagecontextid' => new external_value(PARAM_INT, 'The context ID of the page.'),
+                'pagetype' => new external_value(PARAM_ALPHAEXT, 'The type of the page.'),
+                'pagelayout' => new external_value(PARAM_ALPHA, 'The layout of the page.')
+            ]
+        );
+    }
+
+    /**
+     * Fetch the addable blocks in a given page.
+     *
+     * @param int $pagecontextid The context ID of the page
+     * @param string $pagetype The type of the page
+     * @param string $pagelayout The layout of the page
+     * @return array The blocks list
+     */
+    public static function execute(int $pagecontextid, string $pagetype, string $pagelayout): array {
+        global $PAGE;
+
+        $params = self::validate_parameters(self::execute_parameters(),
+            [
+                'pagecontextid' => $pagecontextid,
+                'pagetype' => $pagetype,
+                'pagelayout' => $pagelayout
+            ]
+        );
+
+        $context = \context::instance_by_id($params['pagecontextid']);
+        // Validate the context. This will also set the context in $PAGE.
+        self::validate_context($context);
+
+        // We need to manually set the page layout and page type.
+        $PAGE->set_pagelayout($params['pagelayout']);
+        $PAGE->set_pagetype($params['pagetype']);
+        // Firstly, we need to load all currently existing page blocks to later determine which blocks are addable.
+        $PAGE->blocks->load_blocks(false);
+        $PAGE->blocks->create_all_block_instances();
+
+        $addableblocks = $PAGE->blocks->get_addable_blocks();
+
+        return array_map(function($block) {
+            return [
+                'name' => $block->name,
+                'title' => get_string('pluginname', "block_{$block->name}")
+            ];
+        }, $addableblocks);
+    }
+
+    /**
+     * Describes the execute return value.
+     *
+     * @return external_multiple_structure
+     */
+    public static function execute_returns(): external_multiple_structure {
+        return new external_multiple_structure(
+            new external_single_structure(
+                [
+                    'name' => new external_value(PARAM_PLUGIN, 'The name of the block.'),
+                    'title' => new external_value(PARAM_RAW, 'The title of the block.'),
+                ]
+            ),
+            'List of addable blocks in a given page.'
+        );
+    }
+}
index 24da2bb..76a2f34 100644 (file)
@@ -118,7 +118,7 @@ class core_customfield_external extends external_api {
                 'component' => new external_value(PARAM_COMPONENT, 'component'),
                 'area' => new external_value(PARAM_ALPHANUMEXT, 'area'),
                 'itemid' => new external_value(PARAM_INT, 'itemid'),
-                'usescategories' => new external_value(PARAM_INT, 'view has categories'),
+                'usescategories' => new external_value(PARAM_BOOL, 'view has categories'),
                 'categories' => new external_multiple_structure(
                     new external_single_structure(
                         array(
index 7cc30a4..dfd6041 100644 (file)
@@ -127,7 +127,8 @@ class player {
             $url,
             $config,
             $this->factory,
-            $this->messages
+            $this->messages,
+            $this->preventredirect
         );
         if ($file) {
             $this->context = \context::instance_by_id($file->get_contextid());
index 719cc65..f3d19e8 100644 (file)
@@ -31,6 +31,8 @@ $string['analyticslogstore'] = 'Log store used for analytics';
 $string['analyticslogstore_help'] = 'The log store that will be used by the analytics API to read users\' activity.';
 $string['analyticssettings'] = 'Analytics settings';
 $string['analyticssiteinfo'] = 'Site information';
+$string['calclifetime'] = 'Keep analytics calculations for';
+$string['configlcalclifetime'] = 'This specifies the length of time you want to keep calculation data - this will not delete predictions, but deletes the data used to generate the predictions. Using the default option here is best as it keeps your disk usage under control, however if you are using calculation tables for other purposes you may want to increase this value.';
 $string['defaulttimesplittingmethods'] = 'Default analysis intervals for model\'s evaluation';
 $string['defaulttimesplittingmethods_help'] = 'The analysis interval defines when the system will calculate predictions and the portion of activity logs that will be considered for those predictions. The model evaluation process will iterate through these analysis intervals unless a specific analysis interval is specified.';
 $string['defaultpredictionsprocessor'] = 'Default predictions processor';
@@ -97,6 +99,7 @@ $string['modeloutputdirwithdefaultinfo'] = 'Directory where prediction processor
 $string['modeltimelimit'] = 'Analysis time limit per model';
 $string['modeltimelimitinfo'] = 'This setting limits the time each model spends analysing the site contents.';
 $string['neutral'] = 'Neutral';
+$string['neverdelete'] = 'Never delete calculations';
 $string['noevaluationbasedassumptions'] = 'Models based on assumptions cannot be evaluated.';
 $string['nodata'] = 'No data to analyse';
 $string['noinsightsmodel'] = 'This model does not generate insights';
index 350c005..8306787 100644 (file)
Binary files a/lib/amd/build/addblockmodal.min.js and b/lib/amd/build/addblockmodal.min.js differ
index 839aaa5..a433efc 100644 (file)
Binary files a/lib/amd/build/addblockmodal.min.js.map and b/lib/amd/build/addblockmodal.min.js.map differ
index 5aed323..03bd4c2 100644 (file)
  * @copyright  2016 Damyon Wiese <damyon@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/modal_factory', 'core/templates', 'core/str', 'core/notification'],
-       function($, ModalFactory, Templates, Str, Notification) {
 
+import ModalFactory from 'core/modal_factory';
+import Templates from 'core/templates';
+import {get_string as getString} from 'core/str';
+import Ajax from 'core/ajax';
 
-    return /** @alias module:core/addblockmodal */ {
-        /**
-         * Global init function for this module.
-         *
-         * @method init
-         * @param {Object} context The template context for rendering this modal body.
-         */
-        init: function(context) {
-            var addblocklink = $('[data-key=addblock]');
+const SELECTORS = {
+    ADD_BLOCK: '[data-key="addblock"]'
+};
 
-            // We need the fetch the names of the blocks. It was too much to send in the page.
-            var titlerequests = context.blocks.map(function(blockName) {
-                return {
-                    key: 'pluginname',
-                    component: 'block_' + blockName,
-                };
-            });
+let addBlockModal = null;
 
-            var bodyPromise = Str.get_strings(titlerequests)
-            .then(function(titles) {
-                return titles.map(function(title, index) {
-                    return {
-                        name: context.blocks[index],
-                        title: title,
-                    };
-                });
-            })
-            .then(function(blocks) {
-                context.blocks = blocks;
-                return Templates.render('core/add_block_body', context);
-            })
-            .fail(Notification.exception);
+/**
+ * Register related event listeners.
+ *
+ * @method registerListenerEvents
+ * @param {String} pageType The type of the page
+ * @param {String} pageLayout The layout of the page
+ * @param {String} addBlockUrl The add block URL
+ */
+const registerListenerEvents = (pageType, pageLayout, addBlockUrl) => {
+    document.addEventListener('click', e => {
+
+        if (e.target.closest(SELECTORS.ADD_BLOCK)) {
+            e.preventDefault();
 
-            var titlePromise = Str.get_string('addblock')
-            .fail(Notification.exception);
+            if (addBlockModal) { // The 'add block' modal has been already created.
+                // Display the 'add block' modal.
+                addBlockModal.show();
+            } else {
+                buildAddBlockModal()
+                .then(modal => {
+                    addBlockModal = modal;
+                    const modalBody = renderBlocks(addBlockUrl, pageType, pageLayout);
+                    modal.setBody(modalBody);
+                    modal.show();
 
-            ModalFactory.create({
-                title: titlePromise,
-                body: bodyPromise,
-                type: 'CANCEL',
-            }, addblocklink);
+                    return modalBody;
+                })
+                .catch(() => {
+                    addBlockModal.destroy();
+                    // Unset the addBlockModal in case this is a transient error and it goes away on a relaunch.
+                    addBlockModal = null;
+                });
+            }
         }
+    });
+};
+
+/**
+ * Method that creates the 'add block' modal.
+ *
+ * @method buildAddBlockModal
+ * @return {Promise} The modal promise (modal's body will be rendered later).
+ */
+const buildAddBlockModal = () => {
+    return ModalFactory.create({
+        type: ModalFactory.types.CANCEL,
+        title: getString('addblock')
+    });
+};
+
+/**
+ * Method that renders the list of available blocks.
+ *
+ * @method renderBlocks
+ * @param {String} addBlockUrl The add block URL
+ * @param {String} pageType The type of the page
+ * @param {String} pageLayout The layout of the page
+ * @return {Promise}
+ */
+const renderBlocks = async(addBlockUrl, pageType, pageLayout) => {
+    // Fetch all addable blocks in the given page.
+    const blocks = await getAddableBlocks(pageType, pageLayout);
+
+    return Templates.render('core/add_block_body', {
+        blocks: blocks,
+        url: addBlockUrl
+    });
+};
+
+/**
+ * Method that fetches all addable blocks in a given page.
+ *
+ * @method getAddableBlocks
+ * @param {String} pageType The type of the page
+ * @param {String} pageLayout The layout of the page
+ * @return {Promise}
+ */
+const getAddableBlocks = async(pageType, pageLayout) => {
+    const request = {
+        methodname: 'core_block_fetch_addable_blocks',
+        args: {
+            pagecontextid: M.cfg.contextid,
+            pagetype: pageType,
+            pagelayout: pageLayout
+        },
     };
-});
+
+    return Ajax.call([request])[0];
+};
+
+/**
+ * Set up the actions.
+ *
+ * @method init
+ * @param {String} pageType The type of the page
+ * @param {String} pageLayout The layout of the page
+ * @param {String} addBlockUrl The add block URL
+ */
+export const init = (pageType, pageLayout, addBlockUrl) => {
+    registerListenerEvents(pageType, pageLayout, addBlockUrl);
+};
index 462684f..c12d6a5 100644 (file)
@@ -25,7 +25,7 @@
 namespace core\antivirus;
 
 defined('MOODLE_INTERNAL') || die();
-require_once(__DIR__ . '../../../../iplookup/lib.php');
+require_once($CFG->dirroot . '/iplookup/lib.php');
 
 /**
  * Base abstract antivirus scanner class.
index ef653f0..815404f 100644 (file)
@@ -2609,6 +2609,16 @@ $functions = array(
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
 
+    'core_block_fetch_addable_blocks' => array(
+        'classname'     => 'core_block\external\fetch_addable_blocks',
+        'methodname'    => 'execute',
+        'description'   => 'Returns all addable blocks in a given page.',
+        'type'          => 'read',
+        'capabilities'  => 'moodle/site:manageblocks',
+        'ajax'          => true,
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
+
     // Filters functions.
     'core_filters_get_available_in_context' => array(
         'classname'   => 'core_filters\external',
index 61db87c..b02ddc0 100644 (file)
@@ -2753,70 +2753,13 @@ function message_get_contact() {
 }
 
 /**
- * Returns list of courses, for whole site, or category
- *
- * Similar to get_courses, but allows paging
- * Important: Using c.* for fields is extremely expensive because
- *            we are using distinct. You almost _NEVER_ need all the fields
- *            in such a large SELECT
- *
  * @deprecated since Moodle 3.7
- * @todo The final deprecation of this function will take place in Moodle 3.11 - see MDL-65319.
- *
- * @param string|int $categoryid Either a category id or 'all' for everything
- * @param string $sort A field and direction to sort by
- * @param string $fields The additional fields to return
- * @param int $totalcount Reference for the number of courses
- * @param string $limitfrom The course to start from
- * @param string $limitnum The number of courses to limit to
- * @return array Array of courses
- */
-function get_courses_page($categoryid="all", $sort="c.sortorder ASC", $fields="c.*",
-                          &$totalcount, $limitfrom="", $limitnum="") {
-    debugging('Function get_courses_page() is deprecated. Please use core_course_category::get_courses() ' .
-        'or core_course_category::search_courses()', DEBUG_DEVELOPER);
-    global $USER, $CFG, $DB;
-
-    $params = array();
-
-    $categoryselect = "";
-    if ($categoryid !== "all" && is_numeric($categoryid)) {
-        $categoryselect = "WHERE c.category = :catid";
-        $params['catid'] = $categoryid;
-    } else {
-        $categoryselect = "";
-    }
-
-    $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
-    $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
-    $params['contextlevel'] = CONTEXT_COURSE;
-
-    $totalcount = 0;
-    if (!$limitfrom) {
-        $limitfrom = 0;
-    }
-    $visiblecourses = array();
-
-    $sql = "SELECT $fields $ccselect
-              FROM {course} c
-              $ccjoin
-           $categoryselect
-          ORDER BY $sort";
-
-    // Pull out all course matching the cat.
-    $rs = $DB->get_recordset_sql($sql, $params);
-    // Iteration will have to be done inside loop to keep track of the limitfrom and limitnum.
-    foreach ($rs as $course) {
-        context_helper::preload_from_record($course);
-        if (core_course_category::can_view_course_info($course)) {
-            $totalcount++;
-            if ($totalcount > $limitfrom && (!$limitnum or count($visiblecourses) < $limitnum)) {
-                $visiblecourses [$course->id] = $course;
-            }
-        }
-    }
-    $rs->close();
-    return $visiblecourses;
+ */
+function get_courses_page() {
+    throw new coding_exception(
+        'Function get_courses_page() has been removed. Please use core_course_category::get_courses() ' .
+        'or core_course_category::search_courses()'
+    );
 }
 
 /**
index 4e1d16a..893d241 100644 (file)
@@ -19,27 +19,27 @@ Feature: Atto with enable/disable function.
   @javascript
   Scenario: Check disable Atto editor.
     When I set the field "mycontrol" to "Disable"
-    Then the "disabled" attribute of "button.atto_collapse_button" "css_element" should contain "disabled"
-    And the "disabled" attribute of "button.atto_title_button" "css_element" should contain "disabled"
-    And the "disabled" attribute of "button.atto_bold_button" "css_element" should contain "disabled"
-    And the "disabled" attribute of "button.atto_italic_button" "css_element" should contain "disabled"
-    And the "disabled" attribute of "button.atto_unorderedlist_button_insertUnorderedList" "css_element" should contain "disabled"
-    And the "disabled" attribute of "button.atto_orderedlist_button_insertOrderedList" "css_element" should contain "disabled"
-    And the "disabled" attribute of "button.atto_link_button" "css_element" should contain "disabled"
-    And the "disabled" attribute of "button.atto_link_button_unlink" "css_element" should contain "disabled"
-    And the "disabled" attribute of "button.atto_image_button" "css_element" should contain "disabled"
+    Then the "disabled" attribute of "button.atto_collapse_button" "css_element" should be set
+    And the "disabled" attribute of "button.atto_title_button" "css_element" should be set
+    And the "disabled" attribute of "button.atto_bold_button" "css_element" should be set
+    And the "disabled" attribute of "button.atto_italic_button" "css_element" should be set
+    And the "disabled" attribute of "button.atto_unorderedlist_button_insertUnorderedList" "css_element" should be set
+    And the "disabled" attribute of "button.atto_orderedlist_button_insertOrderedList" "css_element" should be set
+    And the "disabled" attribute of "button.atto_link_button" "css_element" should be set
+    And the "disabled" attribute of "button.atto_link_button_unlink" "css_element" should be set
+    And the "disabled" attribute of "button.atto_image_button" "css_element" should be set
     And the "contenteditable" attribute of "div#id_myeditoreditable" "css_element" should contain "false"
 
   @javascript
   Scenario: Check enable Atto editor.
     When I set the field "mycontrol" to "Enable"
-    Then "button.atto_collapse_button[disabled]" "css_element" should not exist
-    And "button.atto_title_button[disabled]" "css_element" should not exist
-    And "button.atto_bold_button[disabled]" "css_element" should not exist
-    And "button.atto_italic_button[disabled]" "css_element" should not exist
-    And "button.atto_unorderedlist_button_insertUnorderedList[disabled]" "css_element" should not exist
-    And "button.atto_orderedlist_button_insertOrderedList[disabled]" "css_element" should not exist
-    And "button.atto_link_button[disabled]" "css_element" should not exist
-    And "button.atto_link_button_unlink[disabled]" "css_element" should not exist
-    And "button.atto_image_button[disabled]" "css_element" should not exist
+    Then the "disabled" attribute of "button.atto_collapse_button" "css_element" should not be set
+    And the "disabled" attribute of "button.atto_title_button" "css_element" should not be set
+    And the "disabled" attribute of "button.atto_bold_button" "css_element" should not be set
+    And the "disabled" attribute of "button.atto_italic_button" "css_element" should not be set
+    And the "disabled" attribute of "button.atto_unorderedlist_button_insertUnorderedList" "css_element" should not be set
+    And the "disabled" attribute of "button.atto_orderedlist_button_insertOrderedList" "css_element" should not be set
+    And the "disabled" attribute of "button.atto_link_button" "css_element" should not be set
+    And the "disabled" attribute of "button.atto_link_button_unlink" "css_element" should not be set
+    And the "disabled" attribute of "button.atto_image_button" "css_element" should not be set
     And the "contenteditable" attribute of "div#id_myeditoreditable" "css_element" should contain "true"
index 0d82331..9e48205 100644 (file)
@@ -23,9 +23,9 @@ Feature: Text area with enable/disable function.
   @javascript
   Scenario: Check disable Text area editor.
     When I set the field "mycontrol" to "Disable"
-    Then the "readonly" attribute of "textarea#id_myeditor" "css_element" should contain "readonly"
+    Then the "readonly" attribute of "textarea#id_myeditor" "css_element" should be set
 
   @javascript
   Scenario: Check enable Text area editor.
     When I set the field "mycontrol" to "Enable"
-    Then "textarea#id_myeditor[readonly]" "css_element" should not exist
+    Then the "readonly" attribute of "textarea#id_myeditor" "css_element" should not be set
index 19c3675..9b99d8b 100644 (file)
@@ -329,8 +329,8 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
     }
 
     if ($user->icq && !isset($hiddenfields['icqnumber'])) {
-        $imurl = new moodle_url('http://web.icq.com/wwp', array('uin' => $user->icq) );
-        $iconurl = new moodle_url('http://web.icq.com/whitepages/online', array('icq' => $user->icq, 'img' => '5'));
+        $imurl = new moodle_url('https://web.icq.com/wwp', array('uin' => $user->icq) );
+        $iconurl = new moodle_url('https://web.icq.com/whitepages/online', array('icq' => $user->icq, 'img' => '5'));
         $statusicon = html_writer::tag('img', '',
                 array('src' => $iconurl, 'class' => 'icon icon-post', 'alt' => get_string('status')));
         $node = new core_user\output\myprofile\node('contact', 'icqnumber', get_string('icqnumber'), null, null,
@@ -354,7 +354,7 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
         $tree->add_node($node);
     }
     if ($user->yahoo && !isset($hiddenfields['yahooid'])) {
-        $imurl = new moodle_url('http://edit.yahoo.com/config/send_webmesg', array('.target' => $user->yahoo, '.src' => 'pg'));
+        $imurl = new moodle_url('https://edit.yahoo.com/config/send_webmesg', array('.target' => $user->yahoo, '.src' => 'pg'));
         $iconurl = new moodle_url('http://opi.yahoo.com/online', array('u' => $user->yahoo, 'm' => 'g', 't' => '0'));
         $statusicon = html_writer::tag('img', '',
             array('src' => $iconurl, 'class' => 'iconsmall icon-post', 'alt' => get_string('status')));
index d579e8e..1af265b 100644 (file)
@@ -4065,8 +4065,7 @@ class flat_navigation extends navigation_node_collection {
         // Add-a-block in editing mode.
         if (isset($this->page->theme->addblockposition) &&
                 $this->page->theme->addblockposition == BLOCK_ADDBLOCK_POSITION_FLATNAV &&
-                $PAGE->user_is_editing() && $PAGE->user_can_edit_blocks() &&
-                ($addable = $PAGE->blocks->get_addable_blocks())) {
+                $PAGE->user_is_editing() && $PAGE->user_can_edit_blocks()) {
             $url = new moodle_url($PAGE->url, ['bui_addblock' => '', 'sesskey' => sesskey()]);
             $addablock = navigation_node::create(get_string('addblock'), $url);
             $flat = new flat_navigation_node($addablock, 0);
@@ -4074,12 +4073,11 @@ class flat_navigation extends navigation_node_collection {
             $flat->key = 'addblock';
             $flat->icon = new pix_icon('i/addblock', '');
             $this->add($flat);
-            $blocks = [];
-            foreach ($addable as $block) {
-                $blocks[] = $block->name;
-            }
-            $params = array('blocks' => $blocks, 'url' => '?' . $url->get_query_string(false));
-            $PAGE->requires->js_call_amd('core/addblockmodal', 'init', array($params));
+
+            $addblockurl = "?{$url->get_query_string(false)}";
+
+            $PAGE->requires->js_call_amd('core/addblockmodal', 'init',
+                [$PAGE->pagetype, $PAGE->pagelayout, $addblockurl]);
         }
     }
 
index 21dbe12..f650951 100644 (file)
@@ -34,4 +34,9 @@
 {{#blocks}}
     <a href="{{url}}&amp;bui_addblock={{name}}" class="list-group-item list-group-item-action">{{title}}</a>
 {{/blocks}}
+{{^blocks}}
+    <div class="alert alert-primary" role="alert">
+        {{#str}} noblockstoaddhere {{/str}}
+    </div>
+{{/blocks}}
 </div>
index dacbabc..ec1e3a4 100644 (file)
@@ -945,13 +945,7 @@ EOF;
      * @param string $selectortype The type of element where we are looking in.
      */
     public function the_element_should_be_disabled($element, $selectortype) {
-
-        // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
-        $node = $this->get_selected_node($selectortype, $element);
-
-        if (!$node->hasAttribute('disabled')) {
-            throw new ExpectationException('The element "' . $element . '" is not disabled', $this->getSession());
-        }
+        $this->the_attribute_of_should_be_set("disabled", $element, $selectortype, false);
     }
 
     /**
@@ -963,13 +957,7 @@ EOF;
      * @param string $selectortype The type of where we look
      */
     public function the_element_should_be_enabled($element, $selectortype) {
-
-        // Transforming from steps definitions selector/locator format to mink format and getting the NodeElement.
-        $node = $this->get_selected_node($selectortype, $element);
-
-        if ($node->hasAttribute('disabled')) {
-            throw new ExpectationException('The element "' . $element . '" is not enabled', $this->getSession());
-        }
+        $this->the_attribute_of_should_be_set("disabled", $element, $selectortype, true);
     }
 
     /**
@@ -981,12 +969,7 @@ EOF;
      * @param string $selectortype The type of element where we are looking in.
      */
     public function the_element_should_be_readonly($element, $selectortype) {
-        // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
-        $node = $this->get_selected_node($selectortype, $element);
-
-        if (!$node->hasAttribute('readonly')) {
-            throw new ExpectationException('The element "' . $element . '" is not readonly', $this->getSession());
-        }
+        $this->the_attribute_of_should_be_set("readonly", $element, $selectortype, false);
     }
 
     /**
@@ -998,12 +981,7 @@ EOF;
      * @param string $selectortype The type of element where we are looking in.
      */
     public function the_element_should_not_be_readonly($element, $selectortype) {
-        // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
-        $node = $this->get_selected_node($selectortype, $element);
-
-        if ($node->hasAttribute('readonly')) {
-            throw new ExpectationException('The element "' . $element . '" is readonly', $this->getSession());
-        }
+        $this->the_attribute_of_should_be_set("readonly", $element, $selectortype, true);
     }
 
     /**
@@ -1251,6 +1229,39 @@ EOF;
         $this->resize_window($windowsize, $windowviewport === 'viewport');
     }
 
+    /**
+     * Checks whether there the specified attribute is set or not.
+     *
+     * @Then the :attribute attribute of :element :selectortype should be set
+     * @Then the :attribute attribute of :element :selectortype should :not be set
+     *
+     * @throws ExpectationException
+     * @param string $attribute Name of attribute
+     * @param string $element The locator of the specified selector
+     * @param string $selectortype The selector type
+     * @param string $not
+     */
+    public function the_attribute_of_should_be_set($attribute, $element, $selectortype, $not = null) {
+        // Get the container node (exception if it doesn't exist).
+        $containernode = $this->get_selected_node($selectortype, $element);
+        $hasattribute = $containernode->hasAttribute($attribute);
+
+        if ($not && $hasattribute) {
+            $value = $containernode->getAttribute($attribute);
+            // Should not be set but is.
+            throw new ExpectationException(
+                "The attribute \"{$attribute}\" should not be set but has a value of '{$value}'",
+                $this->getSession()
+            );
+        } else if (!$not && !$hasattribute) {
+            // Should be set but is not.
+            throw new ExpectationException(
+                "The attribute \"{$attribute}\" should be set but is not",
+                $this->getSession()
+            );
+        }
+    }
+
     /**
      * Checks whether there is an attribute on the given element that contains the specified text.
      *
index ab7b3a5..3cb62ad 100644 (file)
@@ -5,6 +5,8 @@ information provided here is intended especially for developers.
 * New optional parameter $extracontent for print_collapsible_region_start(). This allows developers to add interactive HTML elements
   (e.g. a help icon) after the collapsible region's toggle link.
 * Final deprecation i_dock_block() in behat_deprecated.php
+* Final deprecation of get_courses_page. Function has been removed and core_course_category::get_courses() should be
+  used instead.
 
 === 3.10 ===
 * PHPUnit has been upgraded to 8.5. That comes with a few changes:
index dd6bea6..69d6a07 100644 (file)
@@ -1114,7 +1114,7 @@ class api {
             // The last known message time is earlier than the one being requested so we can
             // just return an empty result set rather than having to query the DB.
             if ($lastcreated && $lastcreated < $timefrom) {
-                return [];
+                return helper::format_conversation_messages($userid, $convid, []);
             }
         }
 
index 0705be4..585cc2a 100644 (file)
@@ -6589,6 +6589,47 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertNotEquals($mid1, $convmessages2['messages'][0]->id);
     }
 
+    /**
+     * Test retrieving conversation messages by providing a timefrom higher than last message timecreated. It should return no
+     * messages but keep the return structure to not break when called from the ws.
+     */
+    public function test_get_conversation_messages_timefrom_higher_than_last_timecreated() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+        $user4 = self::getDataGenerator()->create_user();
+
+        // Create group conversation.
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id, $user3->id, $user4->id]
+        );
+
+        // The person doing the search.
+        $this->setUser($user1);
+
+        // Send some messages back and forth.
+        $time = 1;
+        testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 1', $time + 1);
+        testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Message 2', $time + 2);
+        testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 3', $time + 3);
+        testhelper::send_fake_message_to_conversation($user3, $conversation->id, 'Message 4', $time + 4);
+
+        // Retrieve the messages from $time + 5, which should return no messages.
+        $convmessages = \core_message\api::get_conversation_messages($user1->id, $conversation->id, 0, 0, '', $time + 5);
+
+        // Confirm the conversation id is correct.
+        $this->assertEquals($conversation->id, $convmessages['id']);
+
+        // Confirm the message data is correct.
+        $messages = $convmessages['messages'];
+        $this->assertEquals(0, count($messages));
+
+        // Confirm that members key is present.
+        $this->assertArrayHasKey('members', $convmessages);
+    }
+
     /**
      * Helper to seed the database with initial state with data.
      */
index 95985a3..9424606 100644 (file)
@@ -5727,6 +5727,52 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         core_message_external::delete_message_for_all_users($messageid, $user1->id);
     }
 
+    /**
+     * Test retrieving conversation messages by providing a timefrom higher than last message timecreated. It should return no
+     * messages but keep the return structure to not break when called from the ws.
+     */
+    public function test_get_conversation_messages_timefrom_higher_than_last_timecreated() {
+        $this->resetAfterTest(true);
+
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+        $user4 = self::getDataGenerator()->create_user();
+
+        // Create group conversation.
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id, $user3->id, $user4->id]
+        );
+
+        // The person asking for the messages for another user.
+        $this->setUser($user1);
+
+        // Send some messages back and forth.
+        $time = 1;
+        testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 1', $time + 1);
+        testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Message 2', $time + 2);
+        testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 3', $time + 3);
+        testhelper::send_fake_message_to_conversation($user3, $conversation->id, 'Message 4', $time + 4);
+
+        // Retrieve the messages.
+        $result = core_message_external::get_conversation_messages($user1->id, $conversation->id, 0, 0, '', $time + 5);
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(core_message_external::get_conversation_messages_returns(), $result);
+
+        // Check the results are correct.
+        $this->assertEquals($conversation->id, $result['id']);
+
+        // Confirm the message data is correct.
+        $messages = $result['messages'];
+        $this->assertEquals(0, count($messages));
+
+        // Confirm that members key is present.
+        $this->assertArrayHasKey('members', $result);
+    }
+
     /**
      * Helper to seed the database with initial state with data.
      */
index 6b361c5..e9c445e 100644 (file)
@@ -106,6 +106,7 @@ class post extends exporter {
                 'null' => NULL_ALLOWED
             ],
             'timecreated' => ['type' => PARAM_INT],
+            'timemodified' => ['type' => PARAM_INT],
             'unread' => [
                 'type' => PARAM_BOOL,
                 'optional' => true,
@@ -437,6 +438,7 @@ class post extends exporter {
             'hasparent' => $post->has_parent(),
             'parentid' => $post->has_parent() ? $post->get_parent_id() : null,
             'timecreated' => $timecreated,
+            'timemodified' => $post->get_time_modified(),
             'unread' => ($loadcontent && $readreceiptcollection) ? !$readreceiptcollection->has_user_read_post($user, $post) : null,
             'isdeleted' => $isdeleted,
             'isprivatereply' => $isprivatereply,
index 62e33a3..0e47747 100644 (file)
@@ -685,6 +685,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
             'parentid' => $discussion1reply2->parent,
             'hasparent' => true,
             'timecreated' => $discussion1reply2->created,
+            'timemodified' => $discussion1reply2->modified,
             'subject' => $discussion1reply2->subject,
             'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply2->subject}",
             'message' => $message,
@@ -742,6 +743,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
             'parentid' => $discussion1reply1->parent,
             'hasparent' => true,
             'timecreated' => $discussion1reply1->created,
+            'timemodified' => $discussion1reply1->modified,
             'subject' => $discussion1reply1->subject,
             'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply1->subject}",
             'message' => $message,
@@ -2761,6 +2763,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                         'parentid' => $discussion1reply1->parent,
                         'hasparent' => true,
                         'timecreated' => $discussion1reply1->created,
+                        'timemodified' => $discussion1reply1->modified,
                         'subject' => $discussion1reply1->subject,
                         'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply1->subject}",
                         'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
@@ -2825,6 +2828,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                         'parentid' => null,
                         'hasparent' => false,
                         'timecreated' => $discussion1firstpostobject->created,
+                        'timemodified' => $discussion1firstpostobject->modified,
                         'subject' => $discussion1firstpostobject->subject,
                         'replysubject' => get_string('re', 'mod_forum') . " {$discussion1firstpostobject->subject}",
                         'message' => file_rewrite_pluginfile_urls($discussion1firstpostobject->message, 'pluginfile.php',
@@ -2900,6 +2904,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                         'parentid' => $discussion2reply1->parent,
                         'hasparent' => true,
                         'timecreated' => $discussion2reply1->created,
+                        'timemodified' => $discussion2reply1->modified,
                         'subject' => $discussion2reply1->subject,
                         'replysubject' => get_string('re', 'mod_forum') . " {$discussion2reply1->subject}",
                         'message' => file_rewrite_pluginfile_urls($discussion2reply1->message, 'pluginfile.php',
@@ -2964,6 +2969,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                         'parentid' => null,
                         'hasparent' => false,
                         'timecreated' => $discussion2firstpostobject->created,
+                        'timemodified' => $discussion2firstpostobject->modified,
                         'subject' => $discussion2firstpostobject->subject,
                         'replysubject' => get_string('re', 'mod_forum') . " {$discussion2firstpostobject->subject}",
                         'message' => file_rewrite_pluginfile_urls($discussion2firstpostobject->message, 'pluginfile.php',
index 154c1f1..d990e9c 100644 (file)
@@ -1,6 +1,14 @@
 This files describes API changes in /mod/forum/*,
 information provided here is intended especially for developers.
 
+=== 3.11 ===
+
+* The forum post exporter now includes a "timemodified" field for each post, which is included in several WS methods:
+    * mod_forum_get_discussion_posts
+    * get_discussion_posts_by_userid
+    * get_discussion_post
+    * add_discussion_post
+
 === 3.10 ===
 
 * Changes in external function mod_forum_external::get_discussion_posts_by_userid
index 840de03..54f94cd 100644 (file)
@@ -387,7 +387,7 @@ class manager {
         if ($this->can_view_all_attempts()) {
             $user = core_user::get_user($userid);
         } else if ($this->can_view_own_attempts()) {
-            $user = $USER;
+            $user = core_user::get_user($USER->id);
             if ($userid && $user->id != $userid) {
                 return null;
             }
index f5a4dba..91cafae 100644 (file)
@@ -225,7 +225,8 @@ class core_question_backup_testcase extends advanced_testcase {
         // Create a question.
         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
         $questioncategory = $questiongenerator->create_question_category();
-        $overrides = ['category' => $questioncategory->id, 'createdby' => $user->id, 'modifiedby' => $user->id];
+        $overrides = ['name' => 'Test question', 'category' => $questioncategory->id,
+                'createdby' => $user->id, 'modifiedby' => $user->id];
         $question = $questiongenerator->create_question('truefalse', null, $overrides);
 
         // Create a quiz and a questions.
@@ -261,7 +262,7 @@ class core_question_backup_testcase extends advanced_testcase {
         $rc->destroy();
 
         // Test the question author.
-        $questions = $DB->get_records('question');
+        $questions = $DB->get_records('question', ['name' => 'Test question']);
         $this->assertCount(1, $questions);
         $question3 = array_shift($questions);
         $this->assertEquals($user->id, $question3->createdby);
@@ -285,7 +286,8 @@ class core_question_backup_testcase extends advanced_testcase {
         // Create a question.
         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
         $questioncategory = $questiongenerator->create_question_category();
-        $overrides = ['category' => $questioncategory->id, 'createdby' => $user->id, 'modifiedby' => $user->id];
+        $overrides = ['name' => 'Test question', 'category' => $questioncategory->id,
+                'createdby' => $user->id, 'modifiedby' => $user->id];
         $question = $questiongenerator->create_question('truefalse', null, $overrides);
 
         // Create a quiz and a questions.
@@ -317,7 +319,7 @@ class core_question_backup_testcase extends advanced_testcase {
         $rc->destroy();
 
         // Test the question author.
-        $questions = $DB->get_records('question');
+        $questions = $DB->get_records('question', ['name' => 'Test question']);
         $this->assertCount(1, $questions);
         $question = array_shift($questions);
         $this->assertEquals($user->id, $question->createdby);
@@ -341,7 +343,8 @@ class core_question_backup_testcase extends advanced_testcase {
         // Create a question.
         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
         $questioncategory = $questiongenerator->create_question_category();
-        $overrides = ['category' => $questioncategory->id, 'createdby' => $user->id, 'modifiedby' => $user->id];
+        $overrides = ['name' => 'Test question', 'category' => $questioncategory->id,
+                'createdby' => $user->id, 'modifiedby' => $user->id];
         $question = $questiongenerator->create_question('truefalse', null, $overrides);
 
         // Create a quiz and a questions.
@@ -376,7 +379,7 @@ class core_question_backup_testcase extends advanced_testcase {
         $rc->destroy();
 
         // Test the question author.
-        $questions = $DB->get_records('question');
+        $questions = $DB->get_records('question', ['name' => 'Test question']);
         $this->assertCount(1, $questions);
         $question = array_shift($questions);
         $this->assertEquals($USER->id, $question->createdby);
index 0829ca8..921a28c 100644 (file)
@@ -159,7 +159,7 @@ if ($currentuser) {
 
 $PAGE->set_title("$course->fullname: $strpersonalprofile: $fullname");
 $PAGE->set_heading($course->fullname);
-$PAGE->set_pagelayout('standard');
+$PAGE->set_pagelayout('mypublic');
 
 // Locate the users settings in the settings navigation and force it open.
 // This MUST be done after we've set up the page as it is going to cause theme and output to initialise.
index 99e6aae..024d93e 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2021052500.44;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2021052500.45;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 $release  = '4.0dev (Build: 20201127)'; // Human-friendly version name