Merge branch 'MDL-69389-master' of git://github.com/junpataleta/moodle into master
authorSara Arjona <sara@moodle.com>
Wed, 26 Aug 2020 16:02:50 +0000 (18:02 +0200)
committerSara Arjona <sara@moodle.com>
Wed, 26 Aug 2020 16:02:50 +0000 (18:02 +0200)
41 files changed:
admin/classes/task_log_table.php
admin/cli/adhoc_task.php
admin/cli/cron.php
admin/cli/restore_backup.php [new file with mode: 0644]
admin/cli/scheduled_task.php
admin/settings/server.php
admin/tool/task/classes/running_tasks_table.php [new file with mode: 0644]
admin/tool/task/lang/en/tool_task.php
admin/tool/task/renderer.php
admin/tool/task/runningtasks.php [new file with mode: 0644]
admin/tool/task/scheduledtasks.php
admin/tool/task/settings.php
admin/tool/task/styles.css
admin/tool/task/tests/behat/cron_disabled.feature [new file with mode: 0644]
admin/tool/task/tests/behat/running_tasks.feature [new file with mode: 0644]
admin/tool/task/tests/generator/behat_tool_task_generator.php [new file with mode: 0644]
admin/tool/task/tests/generator/lib.php [new file with mode: 0644]
admin/tool/task/version.php
cohort/index.php
h5p/classes/api.php
h5p/classes/editor_framework.php
h5p/classes/framework.php
h5p/tests/generator_test.php
lang/en/admin.php
lang/en/backup.php
lib/classes/task/database_logger.php
lib/classes/task/manager.php
lib/classes/task/task_base.php
lib/cronlib.php
lib/db/install.xml
lib/db/upgrade.php
lib/outputlib.php
lib/pagelib.php
lib/tests/moodle_page_test.php
lib/tests/task_running_test.php [new file with mode: 0644]
lib/upgrade.txt
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/classic/scss/preset/default.scss
theme/classic/style/moodle.css
version.php

index 949e757..673e9a1 100644 (file)
@@ -57,6 +57,8 @@ class task_log_table extends \table_sql {
             'userid'     => get_string('user', 'admin'),
             'timestart'  => get_string('task_starttime', 'admin'),
             'duration'   => get_string('task_duration', 'admin'),
+            'hostname'   => get_string('hostname', 'tool_task'),
+            'pid'        => get_string('pid', 'tool_task'),
             'db'         => get_string('task_dbstats', 'admin'),
             'result'     => get_string('task_result', 'admin'),
             'actions'    => '',
@@ -132,6 +134,7 @@ class task_log_table extends \table_sql {
 
         $sql = "SELECT
                     tl.id, tl.type, tl.component, tl.classname, tl.userid, tl.timestart, tl.timeend,
+                    tl.hostname, tl.pid,
                     tl.dbreads, tl.dbwrites, tl.result,
                     tl.dbreads + tl.dbwrites AS db,
                     tl.timeend - tl.timestart AS duration,
index b0ed21d..dd1cc14 100644 (file)
@@ -37,11 +37,13 @@ list($options, $unrecognized) = cli_get_params(
         'showsql' => false,
         'showdebugging' => false,
         'ignorelimits' => false,
+        'force' => false,
     ], [
         'h' => 'help',
         'e' => 'execute',
         'k' => 'keep-alive',
         'i' => 'ignorelimits',
+        'f' => 'force',
     ]
 );
 
@@ -61,6 +63,7 @@ Options:
  -e, --execute             Run all queued adhoc tasks
  -k, --keep-alive=N        Keep this script alive for N seconds and poll for new adhoc tasks
  -i  --ignorelimits        Ignore task_adhoc_concurrency_limit and task_adhoc_max_runtime limits
+ -f, --force               Run even if cron is disabled
 
 Example:
 \$sudo -u www-data /usr/bin/php admin/cli/adhoc_task.php --execute
@@ -92,6 +95,12 @@ if (moodle_needs_upgrading()) {
 if (empty($options['execute'])) {
     exit(0);
 }
+
+if (!get_config('core', 'cron_enabled') && !$options['force']) {
+    mtrace('Cron is disabled. Use --force to override.');
+    exit(1);
+}
+
 if (empty($options['keep-alive'])) {
     $options['keep-alive'] = 0;
 }
index fe72683..958062a 100644 (file)
@@ -36,14 +36,23 @@ require_once($CFG->libdir.'/cronlib.php');
 
 // now get cli options
 list($options, $unrecognized) = cli_get_params(
-    array(
+    [
         'help' => false,
         'stop' => false,
-    ),
-    array(
+        'list' => false,
+        'force' => false,
+        'enable' => false,
+        'disable' => false,
+        'disable-wait' => false,
+    ], [
         'h' => 'help',
         's' => 'stop',
-    )
+        'l' => 'list',
+        'f' => 'force',
+        'e' => 'enable',
+        'd' => 'disable',
+        'w' => 'disable-wait',
+    ]
 );
 
 if ($unrecognized) {
@@ -56,8 +65,13 @@ if ($options['help']) {
 "Execute periodic cron actions.
 
 Options:
--h, --help            Print out this help
--s, --stop            Notify all other running cron processes to stop after the current task
+-h, --help               Print out this help
+-s, --stop               Notify all other running cron processes to stop after the current task
+-l, --list               Show the list of currently running tasks and how long they have been running
+-f, --force              Execute task even if cron is disabled
+-e, --enable             Enable cron
+-d, --disable            Disable cron
+-w, --disable-wait=600   Disable cron and wait until all tasks finished or fail after N seconds (optional param)
 
 Example:
 \$sudo -u www-data /usr/bin/php admin/cli/cron.php
@@ -74,6 +88,91 @@ if ($options['stop']) {
     die;
 }
 
+if ($options['enable']) {
+    set_config('cron_enabled', 1);
+    mtrace('Cron has been enabled for the site.');
+    exit(0);
+}
+
+if ($options['disable']) {
+    set_config('cron_enabled', 0);
+    \core\task\manager::clear_static_caches();
+    mtrace('Cron has been disabled for the site.');
+    exit(0);
+}
+
+if ($options['list']) {
+    $tasks = \core\task\manager::get_running_tasks();
+    mtrace('The list of currently running tasks:');
+    $format = "%7s %-12s %-9s %-20s %-52s\n";
+    printf ($format,
+        'PID',
+        'HOST',
+        'TYPE',
+        'TIME',
+        'CLASSNAME'
+    );
+    foreach ($tasks as $task) {
+        printf ($format,
+            $task->pid,
+            substr($task->hostname, 0, 12),
+            $task->type,
+            format_time(time() - $task->timestarted),
+            substr($task->classname, 0, 52)
+        );
+    }
+    exit(0);
+}
+
+if ($wait = $options['disable-wait']) {
+    $started = time();
+    if (true === $wait) {
+        // Default waiting time.
+        $waitsec = 600;
+    } else {
+        $waitsec = $wait;
+        $wait = true;
+    }
+
+    set_config('cron_enabled', 0);
+    \core\task\manager::clear_static_caches();
+    mtrace('Cron has been disabled for the site.');
+    mtrace('Allocating '. format_time($waitsec) . ' for the tasks to finish.');
+
+    $lastcount = 0;
+    while ($wait) {
+        $tasks = \core\task\manager::get_running_tasks();
+
+        if (count($tasks) == 0) {
+            mtrace('');
+            mtrace('All scheduled and adhoc tasks finished.');
+            exit(0);
+        }
+
+        if (time() - $started >= $waitsec) {
+            mtrace('');
+            mtrace('Wait time ('. format_time($waitsec) . ') elapsed, but ' . count($tasks) . ' task(s) still running.');
+            mtrace('Exiting with code 1.');
+            exit(1);
+        }
+
+        if (count($tasks) !== $lastcount) {
+            mtrace('');
+            mtrace(count($tasks) . " tasks currently running.", '');
+            $lastcount = count($tasks);
+        } else {
+            mtrace('.', '');
+        }
+
+        sleep(1);
+    }
+}
+
+if (!get_config('core', 'cron_enabled') && !$options['force']) {
+    mtrace('Cron is disabled. Use --force to override.');
+    exit(1);
+}
+
 \core\local\cli\shutdown::script_supports_graceful_exit();
 
 cron_run();
diff --git a/admin/cli/restore_backup.php b/admin/cli/restore_backup.php
new file mode 100644 (file)
index 0000000..6f5de42
--- /dev/null
@@ -0,0 +1,109 @@
+<?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 script allows to restore a course from CLI.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', 1);
+
+require(__DIR__ . '/../../config.php');
+require_once($CFG->libdir . '/clilib.php');
+require_once($CFG->dirroot . "/backup/util/includes/restore_includes.php");
+
+list($options, $unrecognized) = cli_get_params([
+    'file' => '',
+    'categoryid' => '',
+    'showdebugging' => false,
+    'help' => false,
+], [
+    'f' => 'file',
+    'c' => 'categoryid',
+    's' => 'showdebugging',
+    'h' => 'help',
+]);
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help'] || !($options['file']) || !($options['categoryid'])) {
+    $help = <<<EOL
+Restore backup into provided category.
+
+Options:
+-f, --file=STRING           Path to the backup file.
+-c, --categoryid=INT        ID of the category to restore too.
+-s, --showdebugging         Show developer level debugging information
+-h, --help                  Print out this help.
+
+Example:
+\$sudo -u www-data /usr/bin/php admin/cli/restore_backup.php --file=/path/to/backup/file.mbz --categoryid=1\n
+EOL;
+
+    echo $help;
+    exit(0);
+}
+
+if ($options['showdebugging']) {
+    set_debugging(DEBUG_DEVELOPER, true);
+}
+
+if (!$admin = get_admin()) {
+    print_error('noadmins');
+}
+
+if (!file_exists($options['file'])) {
+    print_error('filenotfound');
+}
+
+if (!$category = $DB->get_record('course_categories', ['id' => $options['categoryid']], 'id')) {
+    print_error('invalidcategoryid');
+}
+
+$backupdir = "restore_" . uniqid();
+$path = $CFG->tempdir . DIRECTORY_SEPARATOR . "backup" . DIRECTORY_SEPARATOR . $backupdir;
+
+cli_heading(get_string('extractingbackupfileto', 'backup', $path));
+$fp = get_file_packer('application/vnd.moodle.backup');
+$fp->extract_to_pathname($options['file'], $path);
+
+cli_heading(get_string('preprocessingbackupfile'));
+try {
+    list($fullname, $shortname) = restore_dbops::calculate_course_names(0, get_string('restoringcourse', 'backup'),
+        get_string('restoringcourseshortname', 'backup'));
+
+    $courseid = restore_dbops::create_new_course($fullname, $shortname, $category->id);
+
+    $rc = new restore_controller($backupdir, $courseid, backup::INTERACTIVE_NO,
+        backup::MODE_GENERAL, $admin->id, backup::TARGET_NEW_COURSE);
+    $rc->execute_precheck();
+    $rc->execute_plan();
+    $rc->destroy();
+} catch (Exception $e) {
+    cli_heading(get_string('cleaningtempdata'));
+    fulldelete($path);
+    print_error('generalexceptionmessage', 'error', '', $e->getMessage());
+}
+
+cli_heading(get_string('restoredcourseid', 'backup', $courseid));
+exit(0);
index f825f46..b181b15 100644 (file)
@@ -30,8 +30,17 @@ require_once("$CFG->libdir/clilib.php");
 require_once("$CFG->libdir/cronlib.php");
 
 list($options, $unrecognized) = cli_get_params(
-    array('help' => false, 'list' => false, 'execute' => false, 'showsql' => false, 'showdebugging' => false),
-    array('h' => 'help')
+    [
+        'help' => false,
+        'list' => false,
+        'execute' => false,
+        'showsql' => false,
+        'showdebugging' => false,
+        'force' => false,
+    ], [
+        'h' => 'help',
+        'f' => 'force',
+    ]
 );
 
 if ($unrecognized) {
@@ -49,6 +58,7 @@ if ($options['help'] or (!$options['list'] and !$options['execute'])) {
     --showsql             Show sql queries before they are executed
     --showdebugging       Show developer level debugging information
     -h, --help            Print out this help
+    -f, --force           Execute task even if cron is disabled
 
     Example:
     \$sudo -u www-data /usr/bin/php admin/cli/scheduled_task.php --execute=\\core\\task\\session_cleanup_task
@@ -121,6 +131,13 @@ if ($execute = $options['execute']) {
         exit(1);
     }
 
+    if (!get_config('core', 'cron_enabled') && !$options['force']) {
+        mtrace('Cron is disabled. Use --force to override.');
+        exit(1);
+    }
+
+    \core\task\manager::scheduled_task_starting($task);
+
     // Increase memory limit.
     raise_memory_limit(MEMORY_EXTRA);
 
index e86bbc9..e9fac46 100644 (file)
@@ -216,6 +216,16 @@ $ADMIN->add('server', $temp);
 
 $ADMIN->add('server', new admin_category('taskconfig', new lang_string('taskadmintitle', 'admin')));
 $temp = new admin_settingpage('taskprocessing', new lang_string('taskprocessing','admin'));
+
+$setting = new admin_setting_configcheckbox(
+    'cron_enabled',
+    new lang_string('cron_enabled', 'admin'),
+    new lang_string('cron_enabled_desc', 'admin'),
+    1
+);
+$setting->set_updatedcallback('theme_reset_static_caches');
+$temp->add($setting);
+
 $temp->add(
     new admin_setting_configtext(
         'task_scheduled_concurrency_limit',
diff --git a/admin/tool/task/classes/running_tasks_table.php b/admin/tool/task/classes/running_tasks_table.php
new file mode 100644 (file)
index 0000000..d08f458
--- /dev/null
@@ -0,0 +1,141 @@
+<?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/>.
+
+/**
+ * Running tasks table.
+ *
+ * @package    tool_task
+ * @copyright  2019 The Open University
+ * @copyright  2020 Mikhail Golenkov <golenkovm@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_task;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/tablelib.php');
+
+/**
+ * Table to display list of running task.
+ *
+ * @copyright  2019 The Open University
+ * @copyright  2020 Mikhail Golenkov <golenkovm@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class running_tasks_table extends \table_sql {
+
+    /**
+     * Constructor for the running tasks table.
+     */
+    public function __construct() {
+        parent::__construct('runningtasks');
+
+        $columnheaders = [
+            'classname'    => get_string('classname', 'tool_task'),
+            'type'         => get_string('tasktype', 'admin'),
+            'time'         => get_string('time'),
+            'timestarted'  => get_string('started', 'tool_task'),
+            'hostname'     => get_string('hostname', 'tool_task'),
+            'pid'          => get_string('pid', 'tool_task'),
+        ];
+        $this->define_columns(array_keys($columnheaders));
+        $this->define_headers(array_values($columnheaders));
+
+        // The name column is a header.
+        $this->define_header_column('classname');
+
+        // This table is not collapsible.
+        $this->collapsible(false);
+
+        // Allow pagination.
+        $this->pageable(true);
+    }
+
+    /**
+     * Query the db. Store results in the table object for use by build_table.
+     *
+     * @param int $pagesize size of page for paginated displayed table.
+     * @param bool $useinitialsbar do you want to use the initials bar. Bar
+     * will only be used if there is a fullname column defined for the table.
+     * @throws \dml_exception
+     */
+    public function query_db($pagesize, $useinitialsbar = true) {
+        $sort = $this->get_sql_sort();
+        $this->rawdata = \core\task\manager::get_running_tasks($sort);
+    }
+
+    /**
+     * Format the classname cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_classname($row) : string {
+        $output = $row->classname;
+        if ($row->type == 'scheduled') {
+            if (class_exists($row->classname)) {
+                $task = new $row->classname;
+                if ($task instanceof \core\task\scheduled_task) {
+                    $output .= \html_writer::tag('div', $task->get_name(), ['class' => 'task-class']);
+                }
+            }
+        } else if ($row->type == 'adhoc') {
+            $output .= \html_writer::tag('div',
+                get_string('adhoctaskid', 'tool_task', $row->id), ['class' => 'task-class']);
+        }
+        return $output;
+    }
+
+    /**
+     * Format the type cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     * @throws  \coding_exception
+     */
+    public function col_type($row) : string {
+        if ($row->type == 'scheduled') {
+            $output = \html_writer::span(get_string('scheduled', 'tool_task'), 'badge badge-primary');
+        } else if ($row->type == 'adhoc') {
+            $output = \html_writer::span(get_string('adhoc', 'tool_task'), 'badge badge-warning');
+        } else {
+            // This shouldn't ever happen.
+            $output = '';
+        }
+        return $output;
+    }
+
+    /**
+     * Format the time cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_time($row) : string {
+        return format_time($row->time);
+    }
+
+    /**
+     * Format the timestarted cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_timestarted($row) : string {
+        return userdate($row->timestarted);
+    }
+}
index 299603f..08cf819 100644 (file)
@@ -22,6 +22,9 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['adhoc'] = 'Ad-hoc';
+$string['adhoctaskid'] = 'Ad-hoc task id: {$a}';
+$string['adhoctasks'] = 'Ad-hoc tasks';
 $string['asap'] = 'ASAP';
 $string['adhocempty'] = 'Ad hoc task queue is empty';
 $string['adhocqueuesize'] = 'Ad hoc task queue has {$a} tasks';
@@ -32,9 +35,11 @@ $string['cannotfindthepathtothecli'] = 'Cannot find the path to the PHP CLI exec
 $string['checkadhocqueue'] = 'Ad hoc task queue';
 $string['checkcronrunning'] = 'Cron running';
 $string['checkmaxfaildelay'] = 'Tasks max fail delay';
+$string['classname'] = 'Class name';
 $string['clearfaildelay_confirm'] = 'Are you sure you want to clear the fail delay for task \'{$a}\'? After clearing the delay, the task will run according to its normal schedule.';
 $string['component'] = 'Component';
 $string['corecomponent'] = 'Core';
+$string['crondisabled'] = 'Cron is disabled. No new tasks will be started. The system will not operate properly until it is enabled again.';
 $string['cronok'] = 'Cron is running frequently';
 $string['default'] = 'Default';
 $string['defaultx'] = 'Default: {$a}';
@@ -45,18 +50,24 @@ $string['enablerunnow'] = 'Allow \'Run now\' for scheduled tasks';
 $string['enablerunnow_desc'] = 'Allows administrators to run a single scheduled task immediately, rather than waiting for it to run as scheduled. The feature requires \'Path to PHP CLI\' (pathtophp) to be set in System paths. The task runs on the web server, so you may wish to disable this feature to avoid potential performance issues.';
 $string['faildelay'] = 'Fail delay';
 $string['fromcomponent'] = 'From component: {$a}';
+$string['hostname'] = 'Host name';
 $string['lastruntime'] = 'Last run';
+$string['lastupdated'] = 'Last updated {$a}.';
 $string['nextruntime'] = 'Next run';
+$string['pid'] = 'PID';
 $string['plugindisabled'] = 'Plugin disabled';
 $string['pluginname'] = 'Scheduled task configuration';
 $string['resettasktodefaults'] = 'Reset task schedule to defaults';
 $string['resettasktodefaults_help'] = 'This will discard any local changes and revert the schedule for this task back to its original settings.';
+$string['runningtasks'] = 'Tasks running now';
 $string['runnow'] = 'Run now';
 $string['runagain'] = 'Run again';
 $string['runnow_confirm'] = 'Are you sure you want to run this task \'{$a}\' now? The task will run on the web server and may take some time to complete.';
 $string['runpattern'] = 'Run pattern';
+$string['scheduled'] = 'Scheduled';
 $string['scheduledtasks'] = 'Scheduled tasks';
 $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
+$string['started'] = 'Started';
 $string['taskdisabled'] = 'Task disabled';
 $string['taskfailures'] = '{$a} task(s) failing';
 $string['tasklogs'] = 'Task logs';
index ec0bb0c..ec85231 100644 (file)
@@ -261,6 +261,16 @@ class tool_task_renderer extends plugin_renderer_base {
         return $cell;
     }
 
+    /**
+     * Displays a warning on the page if cron is disabled.
+     *
+     * @return string HTML code for information about cron being disabled
+     * @throws moodle_exception
+     */
+    public function cron_disabled(): string {
+        return $this->output->notification(get_string('crondisabled', 'tool_task'), 'warning');
+    }
+
     /**
      * Renders a link back to the scheduled tasks page (used from the 'run now' screen).
      *
diff --git a/admin/tool/task/runningtasks.php b/admin/tool/task/runningtasks.php
new file mode 100644 (file)
index 0000000..05829a3
--- /dev/null
@@ -0,0 +1,51 @@
+<?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/>.
+
+/**
+ * Running task admin page.
+ *
+ * @package    tool_task
+ * @copyright  2019 The Open University
+ * @copyright  2020 Mikhail Golenkov <golenkovm@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/tablelib.php');
+
+$pageurl = new \moodle_url('/admin/tool/task/runningtasks.php');
+$heading = get_string('runningtasks', 'tool_task');
+$PAGE->set_url($pageurl);
+$PAGE->set_context(context_system::instance());
+$PAGE->set_pagelayout('admin');
+$PAGE->set_title($heading);
+$PAGE->set_heading($heading);
+
+admin_externalpage_setup('runningtasks');
+
+echo $OUTPUT->header();
+
+if (!get_config('core', 'cron_enabled')) {
+    $renderer = $PAGE->get_renderer('tool_task');
+    echo $renderer->cron_disabled();
+}
+
+$table = new \tool_task\running_tasks_table();
+$table->baseurl = $pageurl;
+$table->out(100, false);
+
+echo $OUTPUT->footer();
index d256f3d..243039e 100644 (file)
@@ -95,6 +95,9 @@ if ($mform && ($mform->is_cancelled() || !empty($CFG->preventscheduledtaskchange
 
 } else {
     echo $OUTPUT->header();
+    if (!get_config('core', 'cron_enabled')) {
+        echo $renderer->cron_disabled();
+    }
     $tasks = core\task\manager::get_all_scheduled_tasks();
     echo $renderer->scheduled_tasks_table($tasks, $lastchanged);
     echo $OUTPUT->footer();
index ac9858e..3fd9669 100644 (file)
@@ -33,4 +33,13 @@ if ($hassiteconfig) {
             "$CFG->wwwroot/$CFG->admin/tool/task/scheduledtasks.php"
         )
     );
+
+    $ADMIN->add(
+        'taskconfig',
+        new admin_externalpage(
+            'runningtasks',
+            new lang_string('runningtasks', 'tool_task'),
+            "$CFG->wwwroot/$CFG->admin/tool/task/runningtasks.php"
+        )
+    );
 }
index 0e846ce..f297fd6 100644 (file)
@@ -1,4 +1,5 @@
-#page-admin-tool-task-scheduledtasks .task-class {
+#page-admin-tool-task-scheduledtasks .task-class,
+#page-admin-tool-task-runningtasks .task-class {
     display: block;
     padding: 0 0.5em;
     color: #888;
diff --git a/admin/tool/task/tests/behat/cron_disabled.feature b/admin/tool/task/tests/behat/cron_disabled.feature
new file mode 100644 (file)
index 0000000..24c7e87
--- /dev/null
@@ -0,0 +1,20 @@
+@tool @tool_task
+Feature: See warning message if cron is disabled
+  In order to manage scheduled tasks
+  As a Moodle Administrator
+  I need to be able to view a warning message if cron is disabled
+
+  Background:
+    Given I log in as "admin"
+
+  Scenario: If cron is disabled, I should see the message
+    When the following config values are set as admin:
+      | cron_enabled | 0 |
+    And I navigate to "Server > Tasks > Scheduled tasks" in site administration
+    Then I should see "Cron is disabled"
+
+  Scenario: If cron is enabled, I should not see the message
+    When the following config values are set as admin:
+      | cron_enabled | 1 |
+    And I navigate to "Server > Tasks > Scheduled tasks" in site administration
+    Then I should not see "Cron is disabled"
diff --git a/admin/tool/task/tests/behat/running_tasks.feature b/admin/tool/task/tests/behat/running_tasks.feature
new file mode 100644 (file)
index 0000000..e5db69c
--- /dev/null
@@ -0,0 +1,40 @@
+@tool @tool_task
+Feature: See running scheduled tasks
+  In order to configure scheduled tasks
+  As an admin
+  I need to see if tasks are running
+
+  Background:
+    Given I log in as "admin"
+
+  Scenario: If no task is running, I should see the corresponding message
+    Given I navigate to "Server > Tasks > Tasks running now" in site administration
+    Then I should see "Nothing to display"
+
+  Scenario: If tasks are running, I should see task details
+    Given the following "tool_task > scheduled tasks" exist:
+      | classname                            | seconds | hostname     | pid  |
+      | \core\task\automated_backup_task     | 121     | c69335460f7f | 1914 |
+    And the following "tool_task > adhoc tasks" exist:
+      | classname                            | seconds | hostname     | pid  |
+      | \core\task\asynchronous_backup_task  | 7201    | c69335460f7f | 1915 |
+      | \core\task\asynchronous_restore_task | 172800  | c69335460f7f | 1916 |
+    And I navigate to "Server > Tasks > Tasks running now" in site administration
+
+    # Check the scheduled task details.
+    Then I should see "Scheduled" in the "\core\task\automated_backup_task" "table_row"
+    And I should see "2 mins" in the "Automated backups" "table_row"
+    And I should see "c69335460f7f" in the "Automated backups" "table_row"
+    And I should see "1914" in the "Automated backups" "table_row"
+
+    # Check the "asynchronous_backup_task" adhoc task details.
+    And I should see "Ad-hoc" in the "\core\task\asynchronous_backup_task" "table_row"
+    And I should see "2 hours" in the "core\task\asynchronous_backup_task" "table_row"
+    And I should see "c69335460f7f" in the "core\task\asynchronous_backup_task" "table_row"
+    And I should see "1915" in the "core\task\asynchronous_backup_task" "table_row"
+
+    # Check the "asynchronous_restore_task" adhoc task details.
+    And I should see "Ad-hoc" in the "\core\task\asynchronous_restore_task" "table_row"
+    And I should see "2 days" in the "core\task\asynchronous_restore_task" "table_row"
+    And I should see "c69335460f7f" in the "core\task\asynchronous_restore_task" "table_row"
+    And I should see "1916" in the "core\task\asynchronous_restore_task" "table_row"
diff --git a/admin/tool/task/tests/generator/behat_tool_task_generator.php b/admin/tool/task/tests/generator/behat_tool_task_generator.php
new file mode 100644 (file)
index 0000000..307ad81
--- /dev/null
@@ -0,0 +1,57 @@
+<?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/>.
+
+/**
+ * Behat data generator for tool_task.
+ *
+ * @package   tool_task
+ * @category  test
+ * @copyright 2020 Mikhail Golenkov <golenkovm@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Behat data generator for tool_task.
+ *
+ * @package   tool_task
+ * @category  test
+ * @copyright 2020 Mikhail Golenkov <golenkovm@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_tool_task_generator extends behat_generator_base {
+
+    /**
+     * Get a list of the entities that can be created.
+
+     * @return array entity name => information about how to generate.
+     */
+    protected function get_creatable_entities(): array {
+        return [
+            'scheduled tasks' => [
+                'singular' => 'scheduled task',
+                'datagenerator' => 'scheduled_tasks',
+                'required' => ['classname', 'seconds', 'hostname', 'pid'],
+            ],
+            'adhoc tasks' => [
+                'singular' => 'adhoc task',
+                'datagenerator' => 'adhoc_tasks',
+                'required' => ['classname', 'seconds', 'hostname', 'pid'],
+            ],
+        ];
+    }
+}
diff --git a/admin/tool/task/tests/generator/lib.php b/admin/tool/task/tests/generator/lib.php
new file mode 100644 (file)
index 0000000..b9324c5
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+
+/**
+ * Tool task test data generator class
+ *
+ * @package tool_task
+ * @copyright 2020 Mikhail Golenkov <golenkovm@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tool task test data generator class
+ *
+ * @package tool_task
+ * @copyright 2020 Mikhail Golenkov <golenkovm@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_task_generator extends testing_module_generator {
+
+    /**
+     * Mark a scheduled task as running.
+     *
+     * @param array $data Scheduled task properties
+     * @throws dml_exception
+     */
+    public function create_scheduled_tasks($data) {
+        global $DB;
+        $conditions = ['classname' => $data['classname']];
+        $record = $DB->get_record('task_scheduled', $conditions, '*', MUST_EXIST);
+        $record->timestarted = time() - $data['seconds'];
+        $record->hostname = $data['hostname'];
+        $record->pid = $data['pid'];
+        $DB->update_record('task_scheduled', $record);
+    }
+
+    /**
+     * Mark an adhoc task as running.
+     *
+     * @param array $data Adhoc task properties
+     * @throws dml_exception
+     */
+    public function create_adhoc_tasks($data) {
+        global $DB;
+        $adhoctask = (object)[
+            'classname' => $data['classname'],
+            'nextruntime' => 0,
+            'timestarted' => time() - $data['seconds'],
+            'hostname' => $data['hostname'],
+            'pid' => $data['pid'],
+        ];
+        $DB->insert_record('task_adhoc', $adhoctask);
+    }
+}
index 1823273..151504c 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2021052500; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2021052501; // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2021052500; // Requires this Moodle version
 $plugin->component = 'tool_task'; // Full name of the plugin (used for diagnostics)
 
index f611f98..0341bf9 100644 (file)
@@ -154,7 +154,7 @@ foreach($cohorts['cohorts'] as $cohort) {
         $cohortmanager = has_capability('moodle/cohort:manage', $cohortcontext);
         $cohortcanassign = has_capability('moodle/cohort:assign', $cohortcontext);
 
-        $urlparams = array('id' => $cohort->id, 'returnurl' => $baseurl->out_as_local_url());
+        $urlparams = array('id' => $cohort->id, 'returnurl' => $baseurl->out_as_local_url(false));
         $showhideurl = new moodle_url('/cohort/edit.php', $urlparams + array('sesskey' => sesskey()));
         if ($cohortmanager) {
             if ($cohort->visible) {
index 6cd3484..cab3476 100644 (file)
@@ -164,6 +164,7 @@ class api {
             unset($library->major_version);
             $library->minorVersion = (int) $library->minorversion;
             unset($library->minorversion);
+            $library->metadataSettings = json_decode($library->metadatasettings);
 
             // If we already add this library means that it is an old version,as the previous query was sorted by version.
             if (isset($added[$library->name])) {
index c4b575c..8d7f9b4 100644 (file)
@@ -228,7 +228,7 @@ class editor_framework implements H5peditorStorage {
         if ($libraries !== null) {
             // Get details for the specified libraries.
             $librariesin = [];
-            $fields = 'title, runnable';
+            $fields = 'title, runnable, metadatasettings';
 
             foreach ($libraries as $library) {
                 $params = [
@@ -242,11 +242,12 @@ class editor_framework implements H5peditorStorage {
                 if ($details) {
                     $library->title = $details->title;
                     $library->runnable = $details->runnable;
+                    $library->metadataSettings = json_decode($details->metadatasettings);
                     $librariesin[] = $library;
                 }
             }
         } else {
-            $fields = 'id, machinename as name, title, majorversion, minorversion';
+            $fields = 'id, machinename as name, title, majorversion, minorversion, metadatasettings';
             $librariesin = api::get_contenttype_libraries($fields);
         }
 
index 2e4a2ca..4b05422 100644 (file)
@@ -685,6 +685,9 @@ class framework implements \H5PFrameworkInterface {
      *                           - dropLibraryCss(optional): list of associative arrays containing:
      *                             - machineName: machine name for the librarys that are to drop their css
      *                           - semantics(optional): Json describing the content structure for the library
+     *                           - metadataSettings(optional): object containing:
+     *                             - disable: 1 if metadata is disabled completely
+     *                             - disableExtraTitleField: 1 if the title field is hidden in the form
      * @param bool $new Whether it is a new or existing library.
      */
     public function saveLibraryData(&$librarydata, $new = true) {
@@ -722,6 +725,7 @@ class framework implements \H5PFrameworkInterface {
             'addto' => isset($librarydata['addTo']) ? json_encode($librarydata['addTo']) : null,
             'coremajor' => isset($librarydata['coreApi']['majorVersion']) ? $librarydata['coreApi']['majorVersion'] : null,
             'coreminor' => isset($librarydata['coreApi']['majorVersion']) ? $librarydata['coreApi']['minorVersion'] : null,
+            'metadatasettings' => isset($librarydata['metadataSettings']) ? $librarydata['metadataSettings'] : null,
         );
 
         if ($new) {
index 478ab57..dc266f5 100644 (file)
@@ -246,6 +246,7 @@ class generator_testcase extends \advanced_testcase {
             'addto' => '/regex11/',
             'coremajor' => null,
             'coreminor' => null,
+            'metadatasettings' => null,
         ];
 
         $this->assertEquals($expected, $data);
index 5cb42af..2066a80 100644 (file)
@@ -424,6 +424,8 @@ $string['courseswithsummarieslimit'] = 'Courses with summaries limit';
 $string['creatornewroleid'] = 'Creators\' role in new courses';
 $string['creatornewroleid_help'] = 'If the user does not already have the permission to manage the new course, the user is automatically enrolled using this role.';
 $string['cron'] = 'Cron';
+$string['cron_enabled'] = 'Enable cron';
+$string['cron_enabled_desc'] = 'If disabled prevents the system from starting new background tasks. This option is intended for temporary use only, e.g. before a restart. Leaving it off for a long time will prevent important functionality from working.';
 $string['cron_help'] = 'The cron.php script runs a number of tasks at different scheduled intervals, such as sending forum post notification emails. The script should be run regularly - ideally every minute.';
 $string['cron_link'] = 'admin/cron';
 $string['cronclionly'] = 'Cron execution via command line only';
index 4ff7fce..4bf99b4 100644 (file)
@@ -200,6 +200,7 @@ $string['errorinvalidformat'] = 'Unknown backup format';
 $string['errorinvalidformatinfo'] = 'The selected file is not a valid Moodle backup file and can\'t be restored.';
 $string['errorrestorefrontpagebackup'] = 'You can only restore front page backups on the front page';
 $string['executionsuccess'] = 'The backup file was successfully created.';
+$string['extractingbackupfileto'] = 'Extracting backup file to: {$a}';
 $string['failed'] = 'Backup failed';
 $string['filename'] = 'Filename';
 $string['filealiasesrestorefailures'] = 'Aliases restore failures';
@@ -302,6 +303,7 @@ $string['questionegorycannotberestored'] = 'The questions "{$a->name}" cannot be
 $string['restoreactivity'] = 'Restore activity';
 $string['restorecourse'] = 'Restore course';
 $string['restorecoursesettings'] = 'Course settings';
+$string['restoredcourseid'] = 'Restored course id: {$a}';
 $string['restoreexecutionsuccess'] = 'The course was restored successfully, clicking the continue button below will take you to view the course you restored.';
 $string['restorefileweremissing'] = 'Some files could not be restored because they were missing in the backup.';
 $string['restorenewcoursefullname'] = 'New course name';
index 4e25171..d1978fd 100644 (file)
@@ -75,6 +75,8 @@ class database_logger implements task_logger {
             'dbwrites' => $dbwrites,
             'result' => (int) $failed,
             'output' => file_get_contents($logpath),
+            'hostname' => $task->get_hostname(),
+            'pid' => $task->get_pid(),
         ];
 
         if (is_a($task, adhoc_task::class) && $userid = $task->get_userid()) {
index 552cd49..08b6242 100644 (file)
@@ -255,6 +255,9 @@ class manager {
         $record->dayofweek = $task->get_day_of_week();
         $record->month = $task->get_month();
         $record->disabled = $task->get_disabled();
+        $record->timestarted = $task->get_timestarted();
+        $record->hostname = $task->get_hostname();
+        $record->pid = $task->get_pid();
 
         return $record;
     }
@@ -276,6 +279,9 @@ class manager {
         $record->customdata = $task->get_custom_data_as_string();
         $record->userid = $task->get_userid();
         $record->timecreated = time();
+        $record->timestarted = $task->get_timestarted();
+        $record->hostname = $task->get_hostname();
+        $record->pid = $task->get_pid();
 
         return $record;
     }
@@ -313,6 +319,15 @@ class manager {
         if (isset($record->userid)) {
             $task->set_userid($record->userid);
         }
+        if (isset($record->timestarted)) {
+            $task->set_timestarted($record->timestarted);
+        }
+        if (isset($record->hostname)) {
+            $task->set_hostname($record->hostname);
+        }
+        if (isset($record->pid)) {
+            $task->set_pid($record->pid);
+        }
 
         return $task;
     }
@@ -367,6 +382,15 @@ class manager {
         if (isset($record->disabled)) {
             $task->set_disabled($record->disabled);
         }
+        if (isset($record->timestarted)) {
+            $task->set_timestarted($record->timestarted);
+        }
+        if (isset($record->hostname)) {
+            $task->set_hostname($record->hostname);
+        }
+        if (isset($record->pid)) {
+            $task->set_pid($record->pid);
+        }
 
         return $task;
     }
@@ -709,6 +733,9 @@ class manager {
      */
     public static function adhoc_task_failed(adhoc_task $task) {
         global $DB;
+        // Finalise the log output.
+        logmanager::finalise_log(true);
+
         $delay = $task->get_fail_delay();
 
         // Reschedule task with exponential fall off for failing tasks.
@@ -724,6 +751,9 @@ class manager {
         }
 
         // Reschedule and then release the locks.
+        $task->set_timestarted();
+        $task->set_hostname();
+        $task->set_pid();
         $task->set_next_run_time(time() + $delay);
         $task->set_fail_delay($delay);
         $record = self::record_from_adhoc_task($task);
@@ -734,9 +764,31 @@ class manager {
             $task->get_cron_lock()->release();
         }
         $task->get_lock()->release();
+    }
 
-        // Finalise the log output.
-        logmanager::finalise_log(true);
+    /**
+     * Records that a adhoc task is starting to run.
+     *
+     * @param adhoc_task $task Task that is starting
+     * @param int $time Start time (leave blank for now)
+     * @throws \dml_exception
+     * @throws \coding_exception
+     */
+    public static function adhoc_task_starting(adhoc_task $task, int $time = 0) {
+        global $DB;
+        $pid = (int)getmypid();
+        $hostname = (string)gethostname();
+
+        if (empty($time)) {
+            $time = time();
+        }
+
+        $task->set_timestarted($time);
+        $task->set_hostname($hostname);
+        $task->set_pid($pid);
+
+        $record = self::record_from_adhoc_task($task);
+        $DB->update_record('task_adhoc', $record);
     }
 
     /**
@@ -749,6 +801,9 @@ class manager {
 
         // Finalise the log output.
         logmanager::finalise_log();
+        $task->set_timestarted();
+        $task->set_hostname();
+        $task->set_pid();
 
         // Delete the adhoc task record - it is finished.
         $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
@@ -768,6 +823,8 @@ class manager {
      */
     public static function scheduled_task_failed(scheduled_task $task) {
         global $DB;
+        // Finalise the log output.
+        logmanager::finalise_log(true);
 
         $delay = $task->get_fail_delay();
 
@@ -783,20 +840,24 @@ class manager {
             $delay = 86400;
         }
 
+        $task->set_timestarted();
+        $task->set_hostname();
+        $task->set_pid();
+
         $classname = self::get_canonical_class_name($task);
 
         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
         $record->nextruntime = time() + $delay;
         $record->faildelay = $delay;
+        $record->timestarted = null;
+        $record->hostname = null;
+        $record->pid = null;
         $DB->update_record('task_scheduled', $record);
 
         if ($task->is_blocking()) {
             $task->get_cron_lock()->release();
         }
         $task->get_lock()->release();
-
-        // Finalise the log output.
-        logmanager::finalise_log(true);
     }
 
     /**
@@ -816,6 +877,34 @@ class manager {
         $DB->update_record('task_scheduled', $record);
     }
 
+    /**
+     * Records that a scheduled task is starting to run.
+     *
+     * @param scheduled_task $task Task that is starting
+     * @param int $time Start time (0 = current)
+     * @throws \dml_exception If the task doesn't exist
+     */
+    public static function scheduled_task_starting(scheduled_task $task, int $time = 0) {
+        global $DB;
+        $pid = (int)getmypid();
+        $hostname = (string)gethostname();
+
+        if (!$time) {
+            $time = time();
+        }
+
+        $task->set_timestarted($time);
+        $task->set_hostname($hostname);
+        $task->set_pid($pid);
+
+        $classname = self::get_canonical_class_name($task);
+        $record = $DB->get_record('task_scheduled', ['classname' => $classname], '*', MUST_EXIST);
+        $record->timestarted = $time;
+        $record->hostname = $hostname;
+        $record->pid = $pid;
+        $DB->update_record('task_scheduled', $record);
+    }
+
     /**
      * This function indicates that a scheduled task was completed successfully and should be rescheduled.
      *
@@ -826,6 +915,9 @@ class manager {
 
         // Finalise the log output.
         logmanager::finalise_log();
+        $task->set_timestarted();
+        $task->set_hostname();
+        $task->set_pid();
 
         $classname = self::get_canonical_class_name($task);
         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
@@ -833,6 +925,9 @@ class manager {
             $record->lastruntime = time();
             $record->faildelay = 0;
             $record->nextruntime = $task->get_next_scheduled_time();
+            $record->timestarted = null;
+            $record->hostname = null;
+            $record->pid = null;
 
             $DB->update_record('task_scheduled', $record);
         }
@@ -844,6 +939,47 @@ class manager {
         $task->get_lock()->release();
     }
 
+    /**
+     * Gets a list of currently-running tasks.
+     *
+     * @param  string $sort Sorting method
+     * @return array Array of scheduled and adhoc tasks
+     * @throws \dml_exception
+     */
+    public static function get_running_tasks($sort = ''): array {
+        global $DB;
+        if (empty($sort)) {
+            $sort = 'timestarted ASC, classname ASC';
+        }
+        $params = ['now1' => time(), 'now2' => time()];
+
+        $sql = "SELECT subquery.*
+                  FROM (SELECT concat('s', ts.id) as uniqueid,
+                               ts.id,
+                               'scheduled' as type,
+                               ts.classname,
+                               (:now1 - ts.timestarted) as time,
+                               ts.timestarted,
+                               ts.hostname,
+                               ts.pid
+                          FROM {task_scheduled} ts
+                         WHERE ts.timestarted IS NOT NULL
+                         UNION ALL
+                        SELECT concat('a', ta.id) as uniqueid,
+                               ta.id,
+                               'adhoc' as type,
+                               ta.classname,
+                               (:now2 - ta.timestarted) as time,
+                               ta.timestarted,
+                               ta.hostname,
+                               ta.pid
+                          FROM {task_adhoc} ta
+                         WHERE ta.timestarted IS NOT NULL) subquery
+              ORDER BY " . $sort;
+
+        return $DB->get_records_sql($sql, $params);
+    }
+
     /**
      * This function is used to indicate that any long running cron processes should exit at the
      * next opportunity and restart. This is because something (e.g. DB changes) has changed and
@@ -959,7 +1095,7 @@ class manager {
 
             // Shell-escaped task name.
             $classname = get_class($task);
-            $taskarg   = escapeshellarg("--execute={$classname}");
+            $taskarg   = escapeshellarg("--execute={$classname}") . " " . escapeshellarg("--force");
 
             // Build the CLI command.
             $command = "{$phpbinary} {$scriptpath} {$taskarg}";
index 05e68a5..bffd736 100644 (file)
@@ -50,6 +50,15 @@ abstract class task_base {
     /** @var int $nextruntime - When this task is due to run next */
     private $nextruntime = 0;
 
+    /** @var int $timestarted - When this task was started */
+    private $timestarted = null;
+
+    /** @var string $hostname - Hostname where this task was started and PHP process ID */
+    private $hostname = null;
+
+    /** @var int $pid - PHP process ID that is running the task */
+    private $pid = null;
+
     /**
      * Set the current lock for this task.
      * @param \core\lock\lock $lock
@@ -151,4 +160,52 @@ abstract class task_base {
      * Throw exceptions on errors (the job will be retried).
      */
     public abstract function execute();
+
+    /**
+     * Setter for $timestarted.
+     * @param int $timestarted
+     */
+    public function set_timestarted($timestarted = null) {
+        $this->timestarted = $timestarted;
+    }
+
+    /**
+     * Getter for $timestarted.
+     * @return int
+     */
+    public function get_timestarted() {
+        return $this->timestarted;
+    }
+
+    /**
+     * Setter for $hostname.
+     * @param string $hostname
+     */
+    public function set_hostname($hostname = null) {
+        $this->hostname = $hostname;
+    }
+
+    /**
+     * Getter for $hostname.
+     * @return string
+     */
+    public function get_hostname() {
+        return $this->hostname;
+    }
+
+    /**
+     * Setter for $pid.
+     * @param int $pid
+     */
+    public function set_pid($pid = null) {
+        $this->pid = $pid;
+    }
+
+    /**
+     * Getter for $pid.
+     * @return int
+     */
+    public function get_pid() {
+        return $this->pid;
+    }
 }
index 74a83d5..4190bf2 100644 (file)
@@ -237,6 +237,7 @@ function cron_run_adhoc_tasks(int $timenow, $keepalive = 0, $checklimits = true)
 function cron_run_inner_scheduled_task(\core\task\task_base $task) {
     global $CFG, $DB;
 
+    \core\task\manager::scheduled_task_starting($task);
     \core\task\logmanager::start_logging($task);
 
     $fullname = $task->get_name() . ' (' . get_class($task) . ')';
@@ -295,6 +296,7 @@ function cron_run_inner_scheduled_task(\core\task\task_base $task) {
 function cron_run_inner_adhoc_task(\core\task\adhoc_task $task) {
     global $DB, $CFG;
 
+    \core\task\manager::adhoc_task_starting($task);
     \core\task\logmanager::start_logging($task);
 
     mtrace("Execute adhoc task: " . get_class($task));
index e50236e..e40f3eb 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20200707" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20200804" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <FIELD NAME="faildelay" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="customised" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Used on upgrades to prevent overwriting custom schedules."/>
         <FIELD NAME="disabled" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="1 means do not run from cron"/>
+        <FIELD NAME="timestarted" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Time when the task was started"/>
+        <FIELD NAME="hostname" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Hostname where the task is running"/>
+        <FIELD NAME="pid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="PHP process ID that is running the task"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="blocking" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Timestamp of adhoc task creation"/>
+        <FIELD NAME="timestarted" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Time when the task was started"/>
+        <FIELD NAME="hostname" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Hostname where the task is running"/>
+        <FIELD NAME="pid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="PHP process ID that is running the task"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <FIELD NAME="dbwrites" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The number of DB writes performed during the task."/>
         <FIELD NAME="result" TYPE="int" LENGTH="2" NOTNULL="true" SEQUENCE="false" COMMENT="Whether the task was successful or not. 0 = pass; 1 = fail."/>
         <FIELD NAME="output" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="hostname" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Hostname where the task was executed"/>
+        <FIELD NAME="pid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="PHP process ID that was running the task"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <FIELD NAME="addto" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Plugin configuration data"/>
         <FIELD NAME="coremajor" TYPE="int" LENGTH="4" NOTNULL="false" SEQUENCE="false" COMMENT="H5P core API major version required"/>
         <FIELD NAME="coreminor" TYPE="int" LENGTH="4" NOTNULL="false" SEQUENCE="false" COMMENT="H5P core API minor version required"/>
+        <FIELD NAME="metadatasettings" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Library metadata settings"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index d3acfec..7ebbef2 100644 (file)
@@ -2575,5 +2575,91 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2021052500.02);
     }
 
+    if ($oldversion < 2021052500.04) {
+        // Define field metadatasettings to be added to h5p_libraries.
+        $table = new xmldb_table('h5p_libraries');
+        $field = new xmldb_field('metadatasettings', XMLDB_TYPE_TEXT, null, null, null, null, null, 'coreminor');
+
+        // Conditionally launch add field metadatasettings.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Get installed library files that have no metadata settings value.
+        $params = [
+            'component' => 'core_h5p',
+            'filearea' => 'libraries',
+            'filename' => 'library.json',
+        ];
+        $sql = "SELECT l.id, f.id as fileid
+                  FROM {files} f
+             LEFT JOIN {h5p_libraries} l ON f.itemid = l.id
+                 WHERE f.component = :component
+                       AND f.filearea = :filearea
+                       AND f.filename = :filename";
+        $libraries = $DB->get_records_sql($sql, $params);
+
+        // Update metadatasettings field when the attribute is present in the library.json file.
+        $fs = get_file_storage();
+        foreach ($libraries as $library) {
+            $jsonfile = $fs->get_file_by_id($library->fileid);
+            $jsoncontent = json_decode($jsonfile->get_content());
+            if (isset($jsoncontent->metadataSettings)) {
+                unset($library->fileid);
+                $library->metadatasettings = json_encode($jsoncontent->metadataSettings);
+                $DB->update_record('h5p_libraries', $library);
+            }
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2021052500.04);
+    }
+
+    if ($oldversion < 2021052500.05) {
+        // Define fields to be added to task_scheduled.
+        $table = new xmldb_table('task_scheduled');
+        $field = new xmldb_field('timestarted', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'disabled');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        $field = new xmldb_field('hostname', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'timestarted');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        $field = new xmldb_field('pid', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'hostname');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Define fields to be added to task_adhoc.
+        $table = new xmldb_table('task_adhoc');
+        $field = new xmldb_field('timestarted', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'blocking');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        $field = new xmldb_field('hostname', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'timestarted');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        $field = new xmldb_field('pid', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'hostname');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Define fields to be added to task_log.
+        $table = new xmldb_table('task_log');
+        $field = new xmldb_field('hostname', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'output');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        $field = new xmldb_field('pid', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'hostname');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2021052500.05);
+    }
+
     return true;
 }
index fb4a29c..f324c4b 100644 (file)
@@ -270,6 +270,16 @@ function theme_reset_all_caches() {
     }
 }
 
+/**
+ * Reset static caches.
+ *
+ * This method indicates that all running cron processes should exit at the
+ * next opportunity.
+ */
+function theme_reset_static_caches() {
+    \core\task\manager::clear_static_caches();
+}
+
 /**
  * Enable or disable theme designer mode.
  *
index 99366fc..8378da8 100644 (file)
@@ -1241,9 +1241,10 @@ class moodle_page {
      * This is normally used as the main heading at the top of the content.
      *
      * @param string $heading the main heading that should be displayed at the top of the <body>.
+     * @param bool $applyformatting apply format_string() - by default true.
      */
-    public function set_heading($heading) {
-        $this->_heading = format_string($heading);
+    public function set_heading($heading, bool $applyformatting = true) {
+        $this->_heading = $applyformatting ? format_string($heading) : clean_text($heading);
     }
 
     /**
index a801a62..bd68624 100644 (file)
@@ -311,6 +311,14 @@ class core_moodle_page_testcase extends advanced_testcase {
         $this->testpage->set_heading('a heading');
         // Validated.
         $this->assertSame('a heading', $this->testpage->heading);
+
+        // By default formatting is applied and tags are removed.
+        $this->testpage->set_heading('a heading <a href="#">edit</a><p>');
+        $this->assertSame('a heading edit', $this->testpage->heading);
+
+        // Without formatting the tags are preserved but cleaned.
+        $this->testpage->set_heading('a heading <a href="#">edit</a><p>', false);
+        $this->assertSame('a heading <a href="#">edit</a><p></p>', $this->testpage->heading);
     }
 
     public function test_set_title() {
diff --git a/lib/tests/task_running_test.php b/lib/tests/task_running_test.php
new file mode 100644 (file)
index 0000000..f533938
--- /dev/null
@@ -0,0 +1,185 @@
+<?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 file contains unit tests for the 'task running' data.
+ *
+ * @package core
+ * @copyright 2019 The Open University
+ * @copyright 2020 Mikhail Golenkov <golenkovm@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/fixtures/task_fixtures.php');
+
+/**
+ * This file contains unit tests for the 'task running' data.
+ *
+ * @copyright 2019 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class task_running_testcase extends \advanced_testcase {
+
+    /**
+     * Test for ad-hoc tasks.
+     */
+    public function test_adhoc_task_running() {
+        $this->resetAfterTest();
+
+        // Specify lock factory. The reason is that Postgres locks don't work within a single
+        // process (i.e. if you try to get a lock that you already locked, it will just let you)
+        // which is usually OK but not here where we are simulating running two tasks at once in
+        // the same process.
+        set_config('lock_factory', '\core\lock\db_record_lock_factory');
+
+        // Create and queue 2 new ad-hoc tasks.
+        $task1 = new adhoc_test_task();
+        $task1->set_next_run_time(time() - 20);
+        manager::queue_adhoc_task($task1);
+        $task2 = new adhoc_test2_task();
+        $task2->set_next_run_time(time() - 10);
+        manager::queue_adhoc_task($task2);
+
+        // Check no tasks are marked running.
+        $running = manager::get_running_tasks();
+        $this->assertEmpty($running);
+
+        // Mark the first task running and check results. Because adhoc tasks are pseudo-randomly
+        // shuffled, it is safer if we can cope with either of them being first.
+        $before = time();
+        $next1 = manager::get_next_adhoc_task(time());
+        $task2goesfirst = get_class($next1) === 'core\task\adhoc_test2_task';
+        manager::adhoc_task_starting($next1);
+        $after = time();
+        $running = manager::get_running_tasks();
+        $this->assertCount(1, $running);
+        foreach ($running as $item) {
+            $this->assertEquals('adhoc', $item->type);
+            $this->assertEquals($task2goesfirst ? '\core\task\adhoc_test2_task' : '\core\task\adhoc_test_task',
+                $item->classname);
+            $this->assertLessThanOrEqual($after, $item->timestarted);
+            $this->assertGreaterThanOrEqual($before, $item->timestarted);
+        }
+
+        // Mark the second task running and check results.
+        $next2 = manager::get_next_adhoc_task(time());
+        manager::adhoc_task_starting($next2);
+        $running = manager::get_running_tasks();
+        $this->assertCount(2, $running);
+        if ($task2goesfirst) {
+            $item = array_shift($running);
+            $this->assertEquals('\core\task\adhoc_test2_task', $item->classname);
+            $item = array_shift($running);
+            $this->assertEquals('\core\task\adhoc_test_task', $item->classname);
+        } else {
+            $item = array_shift($running);
+            $this->assertEquals('\core\task\adhoc_test_task', $item->classname);
+            $item = array_shift($running);
+            $this->assertEquals('\core\task\adhoc_test2_task', $item->classname);
+        }
+
+        // Second task completes successfully.
+        manager::adhoc_task_complete($next2);
+        $running = manager::get_running_tasks();
+        $this->assertCount(1, $running);
+        foreach ($running as $item) {
+            $this->assertEquals($task2goesfirst ? '\core\task\adhoc_test2_task' : '\core\task\adhoc_test_task',
+                $item->classname);
+        }
+
+        // First task fails.
+        manager::adhoc_task_failed($next1);
+        $running = manager::get_running_tasks();
+        $this->assertCount(0, $running);
+    }
+
+    /**
+     * Test for scheduled tasks.
+     */
+    public function test_scheduled_task_running() {
+        global $DB;
+        $this->resetAfterTest();
+
+        // Check no tasks are marked running.
+        $running = manager::get_running_tasks();
+        $this->assertEmpty($running);
+
+        // Disable all the tasks, except two, and set those two due to run.
+        $DB->set_field_select('task_scheduled', 'disabled', 1, 'classname != ? AND classname != ?',
+                ['\core\task\session_cleanup_task', '\core\task\file_trash_cleanup_task']);
+        $DB->set_field('task_scheduled', 'nextruntime', 1,
+                ['classname' => '\core\task\session_cleanup_task']);
+        $DB->set_field('task_scheduled', 'nextruntime', 1,
+                ['classname' => '\core\task\file_trash_cleanup_task']);
+        $DB->set_field('task_scheduled', 'lastruntime', time() - 1000,
+                ['classname' => '\core\task\session_cleanup_task']);
+        $DB->set_field('task_scheduled', 'lastruntime', time() - 500,
+                ['classname' => '\core\task\file_trash_cleanup_task']);
+
+        // Get the first task and start it off.
+        $next1 = manager::get_next_scheduled_task(time());
+        $before = time();
+        manager::scheduled_task_starting($next1);
+        $after = time();
+        $running = manager::get_running_tasks();
+        $this->assertCount(1, $running);
+        foreach ($running as $item) {
+            $this->assertLessThanOrEqual($after, $item->timestarted);
+            $this->assertGreaterThanOrEqual($before, $item->timestarted);
+            $this->assertEquals('\core\task\session_cleanup_task', $item->classname);
+        }
+
+        // Mark the second task running and check results. We have to change the times so the other
+        // one comes up first, otherwise it repeats the same one.
+        $DB->set_field('task_scheduled', 'lastruntime', time() - 1500,
+                ['classname' => '\core\task\file_trash_cleanup_task']);
+
+        // Make sure that there is a time gap between task to sort them as expected.
+        sleep(1);
+        $next2 = manager::get_next_scheduled_task(time());
+        manager::scheduled_task_starting($next2);
+
+        // Check default sorting by timestarted.
+        $running = manager::get_running_tasks();
+        $this->assertCount(2, $running);
+        $item = array_shift($running);
+        $this->assertEquals('\core\task\session_cleanup_task', $item->classname);
+        $item = array_shift($running);
+        $this->assertEquals('\core\task\file_trash_cleanup_task', $item->classname);
+
+        // Check sorting by time ASC.
+        $running = manager::get_running_tasks('time ASC');
+        $this->assertCount(2, $running);
+        $item = array_shift($running);
+        $this->assertEquals('\core\task\file_trash_cleanup_task', $item->classname);
+        $item = array_shift($running);
+        $this->assertEquals('\core\task\session_cleanup_task', $item->classname);
+
+        // Complete the file trash one.
+        manager::scheduled_task_complete($next2);
+        $running = manager::get_running_tasks();
+        $this->assertCount(1, $running);
+
+        // Other task fails.
+        manager::scheduled_task_failed($next1);
+        $running = manager::get_running_tasks();
+        $this->assertCount(0, $running);
+    }
+}
index 60a675e..fef4e50 100644 (file)
@@ -36,6 +36,9 @@ information provided here is intended especially for developers.
   a callback function instead of an array of options.
 * Admin setting admin_setting_configselect now supports validating the selection by supplying a
   callback function.
+* The task system has new functions adhoc_task_starting() and scheduled_task_starting() which must
+  be called before executing a task, and a new function \core\task\manager::get_running_tasks()
+  returns information about currently-running tasks.
 
 === 3.9 ===
 * Following function has been deprecated, please use \core\task\manager::run_from_cli().
index 3dbb632..bf407ec 100644 (file)
@@ -20,7 +20,7 @@ $orange:  #f0ad4e !default;
 $yellow:  #ff7518 !default;
 $green:   #398439 !default;
 $teal:    #20c997 !default;
-$cyan:    #5bc0de !default;
+$cyan:    #008196 !default;
 
 $primary:       $blue !default;
 $success:       $green !default;
index 46c723c..eb140de 100644 (file)
   --yellow: #ff7518;
   --green: #398439;
   --teal: #20c997;
-  --cyan: #5bc0de;
+  --cyan: #008196;
   --white: #fff;
   --gray: #6c757d;
   --gray-dark: #343a40;
   --primary: #1177d1;
   --secondary: #ced4da;
   --success: #398439;
-  --info: #5bc0de;
+  --info: #008196;
   --warning: #f0ad4e;
   --danger: #d43f3a;
   --light: #f8f9fa;
@@ -3605,19 +3605,19 @@ pre {
 .table-info,
 .table-info > th,
 .table-info > td {
-  background-color: #d1edf6; }
+  background-color: #b8dce2; }
 
 .table-info th,
 .table-info td,
 .table-info thead th,
 .table-info tbody + tbody {
-  border-color: #aadeee; }
+  border-color: #7abdc8; }
 
 .table-hover .table-info:hover {
-  background-color: #bce5f2; }
+  background-color: #a6d3db; }
   .table-hover .table-info:hover > td,
   .table-hover .table-info:hover > th {
-    background-color: #bce5f2; }
+    background-color: #a6d3db; }
 
 .table-warning,
 .table-warning > th,
@@ -4243,30 +4243,30 @@ fieldset:disabled a.btn {
       box-shadow: 0 0 0 0.2rem rgba(87, 150, 87, 0.5); }
 
 .btn-info {
-  color: #212529;
-  background-color: #5bc0de;
-  border-color: #5bc0de; }
+  color: #fff;
+  background-color: #008196;
+  border-color: #008196; }
   .btn-info:hover {
     color: #fff;
-    background-color: #3bb4d8;
-    border-color: #31b0d5; }
+    background-color: #006070;
+    border-color: #005563; }
   .btn-info:focus, .btn-info.focus {
     color: #fff;
-    background-color: #3bb4d8;
-    border-color: #31b0d5;
-    box-shadow: 0 0 0 0.2rem rgba(82, 169, 195, 0.5); }
+    background-color: #006070;
+    border-color: #005563;
+    box-shadow: 0 0 0 0.2rem rgba(38, 148, 166, 0.5); }
   .btn-info.disabled, .btn-info:disabled {
-    color: #212529;
-    background-color: #5bc0de;
-    border-color: #5bc0de; }
+    color: #fff;
+    background-color: #008196;
+    border-color: #008196; }
   .btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,
   .show > .btn-info.dropdown-toggle {
     color: #fff;
-    background-color: #31b0d5;
-    border-color: #2aaacf; }
+    background-color: #005563;
+    border-color: #004a56; }
     .btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,
     .show > .btn-info.dropdown-toggle:focus {
-      box-shadow: 0 0 0 0.2rem rgba(82, 169, 195, 0.5); }
+      box-shadow: 0 0 0 0.2rem rgba(38, 148, 166, 0.5); }
 
 .btn-warning {
   color: #212529;
@@ -4436,25 +4436,25 @@ fieldset:disabled a.btn {
       box-shadow: 0 0 0 0.2rem rgba(57, 132, 57, 0.5); }
 
 .btn-outline-info {
-  color: #5bc0de;
-  border-color: #5bc0de; }
+  color: #008196;
+  border-color: #008196; }
   .btn-outline-info:hover {
-    color: #212529;
-    background-color: #5bc0de;
-    border-color: #5bc0de; }
+    color: #fff;
+    background-color: #008196;
+    border-color: #008196; }
   .btn-outline-info:focus, .btn-outline-info.focus {
-    box-shadow: 0 0 0 0.2rem rgba(91, 192, 222, 0.5); }
+    box-shadow: 0 0 0 0.2rem rgba(0, 129, 150, 0.5); }
   .btn-outline-info.disabled, .btn-outline-info:disabled {
-    color: #5bc0de;
+    color: #008196;
     background-color: transparent; }
   .btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,
   .show > .btn-outline-info.dropdown-toggle {
-    color: #212529;
-    background-color: #5bc0de;
-    border-color: #5bc0de; }
+    color: #fff;
+    background-color: #008196;
+    border-color: #008196; }
     .btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,
     .show > .btn-outline-info.dropdown-toggle:focus {
-      box-shadow: 0 0 0 0.2rem rgba(91, 192, 222, 0.5); }
+      box-shadow: 0 0 0 0.2rem rgba(0, 129, 150, 0.5); }
 
 .btn-outline-warning {
   color: #f0ad4e;
@@ -5897,14 +5897,14 @@ input[type="button"].btn-block {
     box-shadow: 0 0 0 0.2rem rgba(57, 132, 57, 0.5); }
 
 .badge-info {
-  color: #212529;
-  background-color: #5bc0de; }
+  color: #fff;
+  background-color: #008196; }
   a.badge-info:hover, a.badge-info:focus {
-    color: #212529;
-    background-color: #31b0d5; }
+    color: #fff;
+    background-color: #005563; }
   a.badge-info:focus, a.badge-info.focus {
     outline: 0;
-    box-shadow: 0 0 0 0.2rem rgba(91, 192, 222, 0.5); }
+    box-shadow: 0 0 0 0.2rem rgba(0, 129, 150, 0.5); }
 
 .badge-warning {
   color: #212529;
@@ -6007,13 +6007,13 @@ input[type="button"].btn-block {
     color: #0f210f; }
 
 .alert-info {
-  color: #2f6473;
-  background-color: #def2f8;
-  border-color: #d1edf6; }
+  color: #00434e;
+  background-color: #cce6ea;
+  border-color: #b8dce2; }
   .alert-info hr {
-    border-top-color: #bce5f2; }
+    border-top-color: #a6d3db; }
   .alert-info .alert-link {
-    color: #20454f; }
+    color: #00171b; }
 
 .alert-warning {
   color: #7d5a29;
@@ -6234,15 +6234,15 @@ input[type="button"].btn-block {
     border-color: #1e451e; }
 
 .list-group-item-info {
-  color: #2f6473;
-  background-color: #d1edf6; }
+  color: #00434e;
+  background-color: #b8dce2; }
   .list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {
-    color: #2f6473;
-    background-color: #bce5f2; }
+    color: #00434e;
+    background-color: #a6d3db; }
   .list-group-item-info.list-group-item-action.active {
     color: #fff;
-    background-color: #2f6473;
-    border-color: #2f6473; }
+    background-color: #00434e;
+    border-color: #00434e; }
 
 .list-group-item-warning {
   color: #7d5a29;
@@ -6929,12 +6929,12 @@ button.bg-success:focus {
   background-color: #2a602a !important; }
 
 .bg-info {
-  background-color: #5bc0de !important; }
+  background-color: #008196 !important; }
 
 a.bg-info:hover, a.bg-info:focus,
 button.bg-info:hover,
 button.bg-info:focus {
-  background-color: #31b0d5 !important; }
+  background-color: #005563 !important; }
 
 .bg-warning {
   background-color: #f0ad4e !important; }
@@ -7014,7 +7014,7 @@ button.bg-dark:focus {
   border-color: #398439 !important; }
 
 .border-info {
-  border-color: #5bc0de !important; }
+  border-color: #008196 !important; }
 
 .border-warning {
   border-color: #f0ad4e !important; }
@@ -9484,10 +9484,10 @@ a.text-success:hover, a.text-success:focus {
   color: #224f22 !important; }
 
 .text-info {
-  color: #5bc0de !important; }
+  color: #008196 !important; }
 
 a.text-info:hover, a.text-info:focus {
-  color: #28a1c5 !important; }
+  color: #003f4a !important; }
 
 .text-warning {
   color: #f0ad4e !important; }
@@ -9860,7 +9860,7 @@ div.dropdown-item:focus-within {
   color: #398439; }
 
 .highlight {
-  color: #5bc0de; }
+  color: #008196; }
 
 .fitem.advanced .text-info {
   font-weight: bold; }
@@ -11754,7 +11754,7 @@ input[disabled] {
   color: #0f210f; }
 
 .alert-info a {
-  color: #20454f; }
+  color: #00171b; }
 
 .alert-warning a {
   color: #573e1c; }
@@ -12266,11 +12266,11 @@ input[disabled] {
   width: 4em; }
 
 #adminthemeselector .selectedtheme td.c0 {
-  border: 1px solid #d1edf6;
+  border: 1px solid #b8dce2;
   border-right-width: 0; }
 
 #adminthemeselector .selectedtheme td.c1 {
-  border: 1px solid #d1edf6;
+  border: 1px solid #b8dce2;
   border-left-width: 0; }
 
 .admin_colourpicker,
@@ -12289,12 +12289,12 @@ input[disabled] {
     box-sizing: content-box; }
   .admin_colourpicker .colourdialogue {
     float: left;
-    border: 1px solid #d1edf6; }
+    border: 1px solid #b8dce2; }
   .admin_colourpicker .previewcolour {
-    border: 1px solid #d1edf6;
+    border: 1px solid #b8dce2;
     margin-left: 301px; }
   .admin_colourpicker .currentcolour {
-    border: 1px solid #d1edf6;
+    border: 1px solid #b8dce2;
     margin-left: 301px;
     border-top-width: 0; } }
 
@@ -12367,7 +12367,7 @@ input[disabled] {
 
 #plugins-check-page .pluginupdateinfo,
 #plugins-control-panel .pluginupdateinfo {
-  background-color: #def2f8;
+  background-color: #cce6ea;
   padding: 5px;
   margin: 10px 0; }
   #plugins-check-page .pluginupdateinfo.maturity50,
@@ -13512,11 +13512,11 @@ span.editinstructions {
   margin-left: 30px;
   font-size: 0.8203125rem;
   padding: .1em .4em;
-  background-color: #def2f8;
-  color: #5bc0de;
+  background-color: #cce6ea;
+  color: #008196;
   text-decoration: none;
   z-index: 9999;
-  border: 1px solid #d1edf6; }
+  border: 1px solid #b8dce2; }
 
 /* Course drag and drop upload styles */
 #dndupload-status {
@@ -13525,10 +13525,10 @@ span.editinstructions {
   width: 40%;
   margin: 0 30%;
   padding: 6px;
-  border: 1px solid #d1edf6;
+  border: 1px solid #b8dce2;
   text-align: center;
-  background: #def2f8;
-  color: #5bc0de;
+  background: #cce6ea;
+  color: #008196;
   z-index: 1; }
 
 .dndupload-preview {
@@ -13847,33 +13847,33 @@ span.editinstructions {
   #course-category-listings .listing-pagination {
     text-align: center; }
     #course-category-listings .listing-pagination .yui3-button {
-      color: #212529;
-      background-color: #5bc0de;
-      border-color: #5bc0de;
+      color: #fff;
+      background-color: #008196;
+      border-color: #008196;
       border: 0;
       margin: 0.4rem 0.2rem 0.45rem;
       font-size: 10.4px; }
       #course-category-listings .listing-pagination .yui3-button:hover {
         color: #fff;
-        background-color: #3bb4d8;
-        border-color: #31b0d5; }
+        background-color: #006070;
+        border-color: #005563; }
       #course-category-listings .listing-pagination .yui3-button:focus, #course-category-listings .listing-pagination .yui3-button.focus {
         color: #fff;
-        background-color: #3bb4d8;
-        border-color: #31b0d5;
-        box-shadow: 0 0 0 0.2rem rgba(82, 169, 195, 0.5); }
+        background-color: #006070;
+        border-color: #005563;
+        box-shadow: 0 0 0 0.2rem rgba(38, 148, 166, 0.5); }
       #course-category-listings .listing-pagination .yui3-button.disabled, #course-category-listings .listing-pagination .yui3-button:disabled {
-        color: #212529;
-        background-color: #5bc0de;
-        border-color: #5bc0de; }
+        color: #fff;
+        background-color: #008196;
+        border-color: #008196; }
       #course-category-listings .listing-pagination .yui3-button:not(:disabled):not(.disabled):active, #course-category-listings .listing-pagination .yui3-button:not(:disabled):not(.disabled).active,
       .show > #course-category-listings .listing-pagination .yui3-button.dropdown-toggle {
         color: #fff;
-        background-color: #31b0d5;
-        border-color: #2aaacf; }
+        background-color: #005563;
+        border-color: #004a56; }
         #course-category-listings .listing-pagination .yui3-button:not(:disabled):not(.disabled):active:focus, #course-category-listings .listing-pagination .yui3-button:not(:disabled):not(.disabled).active:focus,
         .show > #course-category-listings .listing-pagination .yui3-button.dropdown-toggle:focus {
-          box-shadow: 0 0 0 0.2rem rgba(82, 169, 195, 0.5); }
+          box-shadow: 0 0 0 0.2rem rgba(38, 148, 166, 0.5); }
       #course-category-listings .listing-pagination .yui3-button.active-page {
         color: #fff;
         background-color: #1177d1;
@@ -15625,14 +15625,14 @@ body.path-question-type {
     color: #573e1c; }
 
 .que .formulation {
-  color: #2f6473;
-  background-color: #def2f8;
-  border-color: #d1edf6;
+  color: #00434e;
+  background-color: #cce6ea;
+  border-color: #b8dce2;
   /* stylelint-disable-line max-line-length */ }
   .que .formulation hr {
-    border-top-color: #bce5f2; }
+    border-top-color: #a6d3db; }
   .que .formulation .alert-link {
-    color: #20454f; }
+    color: #00171b; }
 
 .que.multichoice .answer div.r0 .icon.fa-check,
 .que.multichoice .answer div.r1 .icon.fa-check,
@@ -17571,14 +17571,14 @@ div#dock {
   padding: 0.75rem 1.25rem;
   margin-bottom: 1rem;
   border: 0 solid transparent;
-  color: #2f6473;
-  background-color: #def2f8;
-  border-color: #d1edf6;
+  color: #00434e;
+  background-color: #cce6ea;
+  border-color: #b8dce2;
   /* stylelint-disable-line max-line-length */ }
   .assignfeedback_editpdf_widget .label hr {
-    border-top-color: #bce5f2; }
+    border-top-color: #a6d3db; }
   .assignfeedback_editpdf_widget .label .alert-link {
-    color: #20454f; }
+    color: #00171b; }
 
 .assignfeedback_editpdf_menu {
   padding: 0; }
@@ -17990,7 +17990,7 @@ p.arrow_button {
   box-shadow: inset 0 0 0 2px #fff; }
 
 .btn-info:focus, .btn-info.focus {
-  outline: 0.2rem solid #124a5b;
+  outline: 0.2rem solid black;
   box-shadow: inset 0 0 0 2px #fff; }
 
 .btn-warning:focus, .btn-warning.focus {
@@ -18022,7 +18022,7 @@ p.arrow_button {
   box-shadow: inset 0 0 0 2px #343a40; }
 
 .btn-outline-info:focus, .btn-outline-info.focus {
-  outline: 0.2rem solid #124a5b;
+  outline: 0.2rem solid black;
   box-shadow: inset 0 0 0 2px #343a40; }
 
 .btn-outline-warning:focus, .btn-outline-warning.focus {
index af153de..59eb010 100644 (file)
@@ -20,7 +20,7 @@ $orange:  #f0ad4e !default;
 $yellow:  #ff7518 !default;
 $green:   #398439 !default;
 $teal:    #20c997 !default;
-$cyan:    #5bc0de !default;
+$cyan:    #008196 !default;
 
 $primary:       $blue !default;
 $success:       $green !default;
index b506b4a..452d83c 100644 (file)
   --yellow: #ff7518;
   --green: #398439;
   --teal: #20c997;
-  --cyan: #5bc0de;
+  --cyan: #008196;
   --white: #fff;
   --gray: #6c757d;
   --gray-dark: #343a40;
   --primary: #1177d1;
   --secondary: #ced4da;
   --success: #398439;
-  --info: #5bc0de;
+  --info: #008196;
   --warning: #f0ad4e;
   --danger: #d43f3a;
   --light: #f8f9fa;
@@ -3607,19 +3607,19 @@ pre {
 .table-info,
 .table-info > th,
 .table-info > td {
-  background-color: #d1edf6; }
+  background-color: #b8dce2; }
 
 .table-info th,
 .table-info td,
 .table-info thead th,
 .table-info tbody + tbody {
-  border-color: #aadeee; }
+  border-color: #7abdc8; }
 
 .table-hover .table-info:hover {
-  background-color: #bce5f2; }
+  background-color: #a6d3db; }
   .table-hover .table-info:hover > td,
   .table-hover .table-info:hover > th {
-    background-color: #bce5f2; }
+    background-color: #a6d3db; }
 
 .table-warning,
 .table-warning > th,
@@ -4249,30 +4249,30 @@ fieldset:disabled a.btn {
       box-shadow: 0 0 0 0.2rem rgba(87, 150, 87, 0.5); }
 
 .btn-info {
-  color: #212529;
-  background-color: #5bc0de;
-  border-color: #5bc0de; }
+  color: #fff;
+  background-color: #008196;
+  border-color: #008196; }
   .btn-info:hover {
     color: #fff;
-    background-color: #3bb4d8;
-    border-color: #31b0d5; }
+    background-color: #006070;
+    border-color: #005563; }
   .btn-info:focus, .btn-info.focus {
     color: #fff;
-    background-color: #3bb4d8;
-    border-color: #31b0d5;
-    box-shadow: 0 0 0 0.2rem rgba(82, 169, 195, 0.5); }
+    background-color: #006070;
+    border-color: #005563;
+    box-shadow: 0 0 0 0.2rem rgba(38, 148, 166, 0.5); }
   .btn-info.disabled, .btn-info:disabled {
-    color: #212529;
-    background-color: #5bc0de;
-    border-color: #5bc0de; }
+    color: #fff;
+    background-color: #008196;
+    border-color: #008196; }
   .btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,
   .show > .btn-info.dropdown-toggle {
     color: #fff;
-    background-color: #31b0d5;
-    border-color: #2aaacf; }
+    background-color: #005563;
+    border-color: #004a56; }
     .btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,
     .show > .btn-info.dropdown-toggle:focus {
-      box-shadow: 0 0 0 0.2rem rgba(82, 169, 195, 0.5); }
+      box-shadow: 0 0 0 0.2rem rgba(38, 148, 166, 0.5); }
 
 .btn-warning {
   color: #212529;
@@ -4442,25 +4442,25 @@ fieldset:disabled a.btn {
       box-shadow: 0 0 0 0.2rem rgba(57, 132, 57, 0.5); }
 
 .btn-outline-info {
-  color: #5bc0de;
-  border-color: #5bc0de; }
+  color: #008196;
+  border-color: #008196; }
   .btn-outline-info:hover {
-    color: #212529;
-    background-color: #5bc0de;
-    border-color: #5bc0de; }
+    color: #fff;
+    background-color: #008196;
+    border-color: #008196; }
   .btn-outline-info:focus, .btn-outline-info.focus {
-    box-shadow: 0 0 0 0.2rem rgba(91, 192, 222, 0.5); }
+    box-shadow: 0 0 0 0.2rem rgba(0, 129, 150, 0.5); }
   .btn-outline-info.disabled, .btn-outline-info:disabled {
-    color: #5bc0de;
+    color: #008196;
     background-color: transparent; }
   .btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,
   .show > .btn-outline-info.dropdown-toggle {
-    color: #212529;
-    background-color: #5bc0de;
-    border-color: #5bc0de; }
+    color: #fff;
+    background-color: #008196;
+    border-color: #008196; }
     .btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,
     .show > .btn-outline-info.dropdown-toggle:focus {
-      box-shadow: 0 0 0 0.2rem rgba(91, 192, 222, 0.5); }
+      box-shadow: 0 0 0 0.2rem rgba(0, 129, 150, 0.5); }
 
 .btn-outline-warning {
   color: #f0ad4e;
@@ -6047,14 +6047,14 @@ input[type="button"].btn-block {
     box-shadow: 0 0 0 0.2rem rgba(57, 132, 57, 0.5); }
 
 .badge-info {
-  color: #212529;
-  background-color: #5bc0de; }
+  color: #fff;
+  background-color: #008196; }
   a.badge-info:hover, a.badge-info:focus {
-    color: #212529;
-    background-color: #31b0d5; }
+    color: #fff;
+    background-color: #005563; }
   a.badge-info:focus, a.badge-info.focus {
     outline: 0;
-    box-shadow: 0 0 0 0.2rem rgba(91, 192, 222, 0.5); }
+    box-shadow: 0 0 0 0.2rem rgba(0, 129, 150, 0.5); }
 
 .badge-warning {
   color: #212529;
@@ -6160,13 +6160,13 @@ input[type="button"].btn-block {
     color: #0f210f; }
 
 .alert-info {
-  color: #2f6473;
-  background-color: #def2f8;
-  border-color: #d1edf6; }
+  color: #00434e;
+  background-color: #cce6ea;
+  border-color: #b8dce2; }
   .alert-info hr {
-    border-top-color: #bce5f2; }
+    border-top-color: #a6d3db; }
   .alert-info .alert-link {
-    color: #20454f; }
+    color: #00171b; }
 
 .alert-warning {
   color: #7d5a29;
@@ -6427,15 +6427,15 @@ input[type="button"].btn-block {
     border-color: #1e451e; }
 
 .list-group-item-info {
-  color: #2f6473;
-  background-color: #d1edf6; }
+  color: #00434e;
+  background-color: #b8dce2; }
   .list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {
-    color: #2f6473;
-    background-color: #bce5f2; }
+    color: #00434e;
+    background-color: #a6d3db; }
   .list-group-item-info.list-group-item-action.active {
     color: #fff;
-    background-color: #2f6473;
-    border-color: #2f6473; }
+    background-color: #00434e;
+    border-color: #00434e; }
 
 .list-group-item-warning {
   color: #7d5a29;
@@ -7132,12 +7132,12 @@ button.bg-success:focus {
   background-color: #2a602a !important; }
 
 .bg-info {
-  background-color: #5bc0de !important; }
+  background-color: #008196 !important; }
 
 a.bg-info:hover, a.bg-info:focus,
 button.bg-info:hover,
 button.bg-info:focus {
-  background-color: #31b0d5 !important; }
+  background-color: #005563 !important; }
 
 .bg-warning {
   background-color: #f0ad4e !important; }
@@ -7217,7 +7217,7 @@ button.bg-dark:focus {
   border-color: #398439 !important; }
 
 .border-info {
-  border-color: #5bc0de !important; }
+  border-color: #008196 !important; }
 
 .border-warning {
   border-color: #f0ad4e !important; }
@@ -9687,10 +9687,10 @@ a.text-success:hover, a.text-success:focus {
   color: #224f22 !important; }
 
 .text-info {
-  color: #5bc0de !important; }
+  color: #008196 !important; }
 
 a.text-info:hover, a.text-info:focus {
-  color: #28a1c5 !important; }
+  color: #003f4a !important; }
 
 .text-warning {
   color: #f0ad4e !important; }
@@ -10064,7 +10064,7 @@ div.dropdown-item:focus-within {
   color: #398439; }
 
 .highlight {
-  color: #5bc0de; }
+  color: #008196; }
 
 .fitem.advanced .text-info {
   font-weight: bold; }
@@ -11967,7 +11967,7 @@ input[disabled] {
   color: #0f210f; }
 
 .alert-info a {
-  color: #20454f; }
+  color: #00171b; }
 
 .alert-warning a {
   color: #573e1c; }
@@ -12479,11 +12479,11 @@ input[disabled] {
   width: 4em; }
 
 #adminthemeselector .selectedtheme td.c0 {
-  border: 1px solid #d1edf6;
+  border: 1px solid #b8dce2;
   border-right-width: 0; }
 
 #adminthemeselector .selectedtheme td.c1 {
-  border: 1px solid #d1edf6;
+  border: 1px solid #b8dce2;
   border-left-width: 0; }
 
 .admin_colourpicker,
@@ -12502,12 +12502,12 @@ input[disabled] {
     box-sizing: content-box; }
   .admin_colourpicker .colourdialogue {
     float: left;
-    border: 1px solid #d1edf6; }
+    border: 1px solid #b8dce2; }
   .admin_colourpicker .previewcolour {
-    border: 1px solid #d1edf6;
+    border: 1px solid #b8dce2;
     margin-left: 301px; }
   .admin_colourpicker .currentcolour {
-    border: 1px solid #d1edf6;
+    border: 1px solid #b8dce2;
     margin-left: 301px;
     border-top-width: 0; } }
 
@@ -12580,7 +12580,7 @@ input[disabled] {
 
 #plugins-check-page .pluginupdateinfo,
 #plugins-control-panel .pluginupdateinfo {
-  background-color: #def2f8;
+  background-color: #cce6ea;
   padding: 5px;
   margin: 10px 0;
   border-radius: 5px; }
@@ -13726,11 +13726,11 @@ span.editinstructions {
   margin-left: 30px;
   font-size: 0.8203125rem;
   padding: .1em .4em;
-  background-color: #def2f8;
-  color: #5bc0de;
+  background-color: #cce6ea;
+  color: #008196;
   text-decoration: none;
   z-index: 9999;
-  border: 1px solid #d1edf6; }
+  border: 1px solid #b8dce2; }
 
 /* Course drag and drop upload styles */
 #dndupload-status {
@@ -13739,10 +13739,10 @@ span.editinstructions {
   width: 40%;
   margin: 0 30%;
   padding: 6px;
-  border: 1px solid #d1edf6;
+  border: 1px solid #b8dce2;
   text-align: center;
-  background: #def2f8;
-  color: #5bc0de;
+  background: #cce6ea;
+  color: #008196;
   z-index: 1;
   border-radius: 8px; }
 
@@ -14063,33 +14063,33 @@ span.editinstructions {
   #course-category-listings .listing-pagination {
     text-align: center; }
     #course-category-listings .listing-pagination .yui3-button {
-      color: #212529;
-      background-color: #5bc0de;
-      border-color: #5bc0de;
+      color: #fff;
+      background-color: #008196;
+      border-color: #008196;
       border: 0;
       margin: 0.4rem 0.2rem 0.45rem;
       font-size: 10.4px; }
       #course-category-listings .listing-pagination .yui3-button:hover {
         color: #fff;
-        background-color: #3bb4d8;
-        border-color: #31b0d5; }
+        background-color: #006070;
+        border-color: #005563; }
       #course-category-listings .listing-pagination .yui3-button:focus, #course-category-listings .listing-pagination .yui3-button.focus {
         color: #fff;
-        background-color: #3bb4d8;
-        border-color: #31b0d5;
-        box-shadow: 0 0 0 0.2rem rgba(82, 169, 195, 0.5); }
+        background-color: #006070;
+        border-color: #005563;
+        box-shadow: 0 0 0 0.2rem rgba(38, 148, 166, 0.5); }
       #course-category-listings .listing-pagination .yui3-button.disabled, #course-category-listings .listing-pagination .yui3-button:disabled {
-        color: #212529;
-        background-color: #5bc0de;
-        border-color: #5bc0de; }
+        color: #fff;
+        background-color: #008196;
+        border-color: #008196; }
       #course-category-listings .listing-pagination .yui3-button:not(:disabled):not(.disabled):active, #course-category-listings .listing-pagination .yui3-button:not(:disabled):not(.disabled).active,
       .show > #course-category-listings .listing-pagination .yui3-button.dropdown-toggle {
         color: #fff;
-        background-color: #31b0d5;
-        border-color: #2aaacf; }
+        background-color: #005563;
+        border-color: #004a56; }
         #course-category-listings .listing-pagination .yui3-button:not(:disabled):not(.disabled):active:focus, #course-category-listings .listing-pagination .yui3-button:not(:disabled):not(.disabled).active:focus,
         .show > #course-category-listings .listing-pagination .yui3-button.dropdown-toggle:focus {
-          box-shadow: 0 0 0 0.2rem rgba(82, 169, 195, 0.5); }
+          box-shadow: 0 0 0 0.2rem rgba(38, 148, 166, 0.5); }
       #course-category-listings .listing-pagination .yui3-button.active-page {
         color: #fff;
         background-color: #1177d1;
@@ -15847,14 +15847,14 @@ body.path-question-type {
     color: #573e1c; }
 
 .que .formulation {
-  color: #2f6473;
-  background-color: #def2f8;
-  border-color: #d1edf6;
+  color: #00434e;
+  background-color: #cce6ea;
+  border-color: #b8dce2;
   /* stylelint-disable-line max-line-length */ }
   .que .formulation hr {
-    border-top-color: #bce5f2; }
+    border-top-color: #a6d3db; }
   .que .formulation .alert-link {
-    color: #20454f; }
+    color: #00171b; }
 
 .que.multichoice .answer div.r0 .icon.fa-check,
 .que.multichoice .answer div.r1 .icon.fa-check,
@@ -17801,14 +17801,14 @@ div#dock {
   margin-bottom: 1rem;
   border: 0 solid transparent;
   border-radius: 0.25rem;
-  color: #2f6473;
-  background-color: #def2f8;
-  border-color: #d1edf6;
+  color: #00434e;
+  background-color: #cce6ea;
+  border-color: #b8dce2;
   /* stylelint-disable-line max-line-length */ }
   .assignfeedback_editpdf_widget .label hr {
-    border-top-color: #bce5f2; }
+    border-top-color: #a6d3db; }
   .assignfeedback_editpdf_widget .label .alert-link {
-    color: #20454f; }
+    color: #00171b; }
 
 .assignfeedback_editpdf_menu {
   padding: 0; }
@@ -18223,7 +18223,7 @@ p.arrow_button {
   box-shadow: inset 0 0 0 2px #fff; }
 
 .btn-info:focus, .btn-info.focus {
-  outline: 0.2rem solid #124a5b;
+  outline: 0.2rem solid black;
   box-shadow: inset 0 0 0 2px #fff; }
 
 .btn-warning:focus, .btn-warning.focus {
@@ -18255,7 +18255,7 @@ p.arrow_button {
   box-shadow: inset 0 0 0 2px #343a40; }
 
 .btn-outline-info:focus, .btn-outline-info.focus {
-  outline: 0.2rem solid #124a5b;
+  outline: 0.2rem solid black;
   box-shadow: inset 0 0 0 2px #343a40; }
 
 .btn-outline-warning:focus, .btn-outline-warning.focus {
index 0d1f7ba..0eb7541 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2021052500.03;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2021052500.05;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 $release  = '4.0dev (Build: 20200822)'; // Human-friendly version name