Merge branch 'MDL-69262-master-2' of git://github.com/junpataleta/moodle into master
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 7 Sep 2020 21:42:45 +0000 (23:42 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 7 Sep 2020 21:42:45 +0000 (23:42 +0200)
251 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/language.php
admin/settings/plugins.php
admin/settings/server.php
admin/tool/dataprivacy/lang/en/deprecated.txt [deleted file]
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/log/upgrade.txt
admin/tool/lp/tests/behat/course_competencies.feature
admin/tool/moodlenet/classes/external.php
admin/tool/moodlenet/classes/profile_manager.php
admin/tool/moodlenet/db/upgrade.php
admin/tool/moodlenet/version.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
admin/tool/uploadcourse/classes/step2_form.php
admin/tool/usertours/classes/manager.php
auth/db/auth.php
auth/ldap/auth.php
auth/mnet/auth.php
auth/tests/behat/login.feature
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/upgrade.txt
backup/util/dbops/restore_dbops.class.php
backup/util/ui/classes/copy/copy.php
backup/util/ui/classes/output/copy_form.php
badges/criteria/award_criteria_manual.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lang/en/deprecated.txt
cache/upgrade.txt
calendar/classes/local/event/forms/eventtype.php
calendar/tests/behat/calendar.feature
calendar/upgrade.txt
cohort/edit_form.php
cohort/index.php
cohort/tests/behat/add_cohort.feature
comment/classes/external.php
comment/comment_ajax.php
comment/lib.php
comment/tests/context_freeze_test.php [new file with mode: 0644]
competency/classes/api.php
contentbank/classes/content.php
contentbank/classes/contentbank.php
contentbank/classes/contenttype.php
contentbank/contenttype/h5p/classes/contenttype.php
contentbank/contenttype/h5p/tests/behat/admin_replace_content.feature [new file with mode: 0644]
contentbank/contenttype/h5p/tests/behat/teacher_replace_content.feature [new file with mode: 0644]
contentbank/contenttype/h5p/tests/contenttype_h5p_test.php
contentbank/files_form.php
contentbank/tests/behat/download_content.feature [new file with mode: 0644]
contentbank/tests/content_test.php
contentbank/tests/contentbank_test.php
contentbank/tests/contenttype_test.php
contentbank/upload.php
contentbank/view.php
course/classes/deletecategory_form.php
course/classes/editcategory_form.php
course/edit_form.php
course/request_form.php
course/switchrole.php
course/tests/behat/category_management.feature
course/tests/behat/course_request.feature
course/tests/behat/coursetags.feature
course/tests/behat/rename_roles.feature
course/tests/behat/role_renaming.feature
course/upgrade.txt
customfield/field/select/classes/data_controller.php
customfield/field/select/classes/field_controller.php
enrol/cohort/lib.php
enrol/manual/classes/enrol_users_form.php
filter/activitynames/filter.php
filter/activitynames/tests/filter_test.php
grade/edit/tree/category.php
grade/edit/tree/lib.php
grade/grading/form/upgrade.txt
grade/report/grader/lib.php
grade/report/singleview/classes/local/screen/grade.php
grade/report/singleview/classes/local/ui/finalgrade.php
grade/report/singleview/tests/behat/bulk_insert_grades.feature
grade/report/singleview/tests/behat/singleview.feature
group/autogroup.php
h5p/classes/api.php
h5p/classes/editor_framework.php
h5p/classes/framework.php
h5p/classes/player.php
h5p/tests/generator_test.php
h5p/upgrade.txt
install/lang/eu/admin.php
install/lang/kaa/langconfig.php [new file with mode: 0644]
install/lang/prs/moodle.php [new file with mode: 0644]
lang/en/admin.php
lang/en/antivirus.php
lang/en/backup.php
lang/en/badges.php
lang/en/contentbank.php
lang/en/deprecated.txt
lang/en/error.php
lang/en/message.php
lang/en/moodle.php
lang/en/role.php
lang/en/user.php
lib/accesslib.php
lib/adminlib.php
lib/antivirus/clamav/classes/scanner.php
lib/behat/behat_base.php
lib/classes/antivirus/manager.php
lib/classes/antivirus/quarantine.php [new file with mode: 0644]
lib/classes/antivirus/scanner.php
lib/classes/dataformat.php
lib/classes/event/virus_infected_data_detected.php [new file with mode: 0644]
lib/classes/event/virus_infected_file_detected.php [new file with mode: 0644]
lib/classes/grades_external.php
lib/classes/plugin_manager.php
lib/classes/task/antivirus_cleanup_task.php [new file with mode: 0644]
lib/classes/task/asynchronous_copy_task.php
lib/classes/task/backup_cleanup_task.php
lib/classes/task/database_logger.php
lib/classes/task/manager.php
lib/classes/task/task_base.php
lib/classes/user.php
lib/cronlib.php
lib/db/access.php
lib/db/install.xml
lib/db/messages.php
lib/db/services.php
lib/db/tasks.php
lib/db/upgrade.php
lib/form/duration.php
lib/form/tests/duration_test.php
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-debug.js
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-min.js
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector.js
lib/form/yui/src/dateselector/js/calendar.js
lib/grade/grade_grade.php
lib/grade/tests/grade_grade_test.php
lib/moodlelib.php
lib/outputlib.php
lib/pagelib.php
lib/templates/infected_file_email.mustache [new file with mode: 0644]
lib/tests/antivirus_test.php
lib/tests/dataformat_test.php
lib/tests/grades_externallib_test.php
lib/tests/moodle_page_test.php
lib/tests/moodlelib_test.php
lib/tests/scheduled_task_test.php
lib/tests/task_running_test.php [new file with mode: 0644]
lib/upgrade.txt
message/amd/build/message_repository.min.js
message/amd/build/message_repository.min.js.map
message/amd/build/toggle_contact_button.min.js
message/amd/build/toggle_contact_button.min.js.map
message/amd/src/message_repository.js
message/amd/src/toggle_contact_button.js
message/classes/api.php
message/classes/helper.php
message/classes/output/messagearea/contact.php [deleted file]
message/classes/output/messagearea/contacts.php [deleted file]
message/classes/output/messagearea/message.php [deleted file]
message/classes/output/messagearea/message_area.php [deleted file]
message/classes/output/messagearea/messages.php [deleted file]
message/classes/output/messagearea/profile.php [deleted file]
message/classes/output/messagearea/user_search_results.php [deleted file]
message/externallib.php
message/lib.php
message/tests/api_test.php
message/tests/events_test.php
message/tests/externallib_test.php
message/tests/messagelib_test.php
message/upgrade.txt
mod/assign/feedback/file/importziplib.php
mod/assign/feedback/file/tests/importziplib_test.php [new file with mode: 0644]
mod/choice/lang/en/choice.php
mod/feedback/classes/complete_form.php
mod/feedback/classes/responses_table.php
mod/feedback/lang/en/feedback.php
mod/forum/externallib.php
mod/forum/tests/externallib_test.php
mod/h5pactivity/mod_form.php
mod/h5pactivity/view.php
mod/lesson/lang/en/lesson.php
mod/lti/view.php
pix/i/completion-auto-enabled.png
pix/i/completion-auto-enabled.svg
pix/i/completion-auto-fail.png
pix/i/completion-auto-fail.svg
pix/i/completion-auto-n.png
pix/i/completion-auto-n.svg
pix/i/completion-auto-pass.png
pix/i/completion-auto-pass.svg
pix/i/completion-auto-y-override.png
pix/i/completion-auto-y-override.svg
pix/i/completion-auto-y.png
pix/i/completion-auto-y.svg
pix/i/completion-manual-enabled.png
pix/i/completion-manual-enabled.svg
pix/i/completion-manual-n.png
pix/i/completion-manual-n.svg
pix/i/completion-manual-y-override.png
pix/i/completion-manual-y-override.svg
pix/i/completion-manual-y.png
pix/i/completion-manual-y.svg
question/behaviour/behaviourbase.php
question/behaviour/upgrade.txt
question/engine/lib.php
question/engine/questionattempt.php
question/engine/tests/questionattempt_with_steps_test.php
report/infectedfiles/classes/output/renderer.php [new file with mode: 0644]
report/infectedfiles/classes/privacy/provider.php [new file with mode: 0644]
report/infectedfiles/classes/table/infectedfiles_table.php [new file with mode: 0644]
report/infectedfiles/index.php [new file with mode: 0644]
report/infectedfiles/lang/en/report_infectedfiles.php [new file with mode: 0644]
report/infectedfiles/settings.php [new file with mode: 0644]
report/infectedfiles/version.php [new file with mode: 0644]
report/insights/classes/output/insights_list.php
repository/equella/lib.php
search/upgrade.txt
theme/boost/amd/build/form-display-errors.min.js
theme/boost/amd/build/form-display-errors.min.js.map
theme/boost/amd/src/form-display-errors.js
theme/boost/classes/autoprefixer.php
theme/boost/config.php
theme/boost/lang/en/theme_boost.php
theme/boost/lib.php
theme/boost/scss/moodle.scss
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/prefixes.scss [new file with mode: 0644]
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/classic/scss/preset/default.scss
theme/classic/style/moodle.css
theme/upgrade.txt
user/classes/output/participants_filter.php
user/classes/output/user_roles_editable.php
user/classes/table/participants.php
user/editlib.php
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 1477082..c865dfb 100644 (file)
@@ -8,6 +8,8 @@ if ($hassiteconfig) {
     $temp = new admin_settingpage('langsettings', new lang_string('languagesettings', 'admin'));
     $temp->add(new admin_setting_configcheckbox('autolang', new lang_string('autolang', 'admin'), new lang_string('configautolang', 'admin'), 1));
     $temp->add(new admin_setting_configselect('lang', new lang_string('lang', 'admin'), new lang_string('configlang', 'admin'), current_language(), get_string_manager()->get_list_of_translations())); // $CFG->lang might be set in installer already, default en is in setup.php
+    $temp->add(new admin_setting_configcheckbox('autolangusercreation', new lang_string('autolangusercreation', 'admin'),
+        new lang_string('configautolangusercreation', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('langmenu', new lang_string('langmenu', 'admin'), new lang_string('configlangmenu', 'admin'), 1));
     $temp->add(new admin_setting_langlist());
     $temp->add(new admin_setting_configcheckbox('langcache', new lang_string('langcache', 'admin'), new lang_string('langcache_desc', 'admin'), 1));
index 3e7c68c..249bd53 100644 (file)
@@ -167,6 +167,42 @@ if ($hassiteconfig) {
     $ADMIN->add('modules', new admin_category('antivirussettings', new lang_string('antiviruses', 'antivirus')));
     $temp = new admin_settingpage('manageantiviruses', new lang_string('antivirussettings', 'antivirus'));
     $temp->add(new admin_setting_manageantiviruses());
+
+    // Common settings.
+    $temp->add(new admin_setting_heading('antiviruscommonsettings', new lang_string('antiviruscommonsettings', 'antivirus'), ''));
+
+    // Alert email.
+    $temp->add(
+        new admin_setting_configtext(
+            'antivirus/notifyemail',
+            new lang_string('notifyemail', 'antivirus'),
+            new lang_string('notifyemail_help', 'antivirus'),
+            '',
+            PARAM_EMAIL
+        )
+    );
+
+    // Enable quarantine.
+    $temp->add(
+        new admin_setting_configcheckbox(
+            'antivirus/enablequarantine',
+            new lang_string('enablequarantine', 'antivirus'),
+            new lang_string('enablequarantine_help', 'antivirus',
+            \core\antivirus\quarantine::DEFAULT_QUARANTINE_FOLDER),
+            0
+        )
+    );
+
+    // Quarantine time.
+    $temp->add(
+        new admin_setting_configduration(
+            'antivirus/quarantinetime',
+            new lang_string('quarantinetime', 'antivirus'),
+            new lang_string('quarantinetime_desc', 'antivirus'),
+            \core\antivirus\quarantine::DEFAULT_QUARANTINE_TIME
+        )
+    );
+
     $ADMIN->add('antivirussettings', $temp);
     $plugins = core_plugin_manager::instance()->get_plugins_of_type('antivirus');
     core_collator::asort_objects_by_property($plugins, 'displayname');
index e86bbc9..1c3ee18 100644 (file)
 <?php
+// This file is part of Moodle - https://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/>.
+
+/**
+ * Defines settingpages and externalpages under the "server" category.
+ *
+ * @package     core
+ * @category    admin
+ * @copyright   2006 Martin Dougiamas
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($hassiteconfig) {
+    // System paths.
+    $temp = new admin_settingpage('systempaths', new lang_string('systempaths', 'admin'));
+    $temp->add(new admin_setting_configexecutable('pathtophp', new lang_string('pathtophp', 'admin'),
+        new lang_string('configpathtophp', 'admin'), ''));
+    $temp->add(new admin_setting_configexecutable('pathtodu', new lang_string('pathtodu', 'admin'),
+        new lang_string('configpathtodu', 'admin'), ''));
+    $temp->add(new admin_setting_configexecutable('aspellpath', new lang_string('aspellpath', 'admin'),
+        new lang_string('edhelpaspellpath'), ''));
+    $temp->add(new admin_setting_configexecutable('pathtodot', new lang_string('pathtodot', 'admin'),
+        new lang_string('pathtodot_help', 'admin'), ''));
+    $temp->add(new admin_setting_configexecutable('pathtogs', new lang_string('pathtogs', 'admin'),
+        new lang_string('pathtogs_help', 'admin'), '/usr/bin/gs'));
+    $temp->add(new admin_setting_configexecutable('pathtopython', new lang_string('pathtopython', 'admin'),
+        new lang_string('pathtopythondesc', 'admin'), ''));
+    $ADMIN->add('server', $temp);
 
-// This file defines settingpages and externalpages under the "server" category
-
-if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
-
-// "systempaths" settingpage
-$temp = new admin_settingpage('systempaths', new lang_string('systempaths','admin'));
-$temp->add(new admin_setting_configexecutable('pathtophp', new lang_string('pathtophp', 'admin'),
-    new lang_string('configpathtophp', 'admin'), ''));
-$temp->add(new admin_setting_configexecutable('pathtodu', new lang_string('pathtodu', 'admin'), new lang_string('configpathtodu', 'admin'), ''));
-$temp->add(new admin_setting_configexecutable('aspellpath', new lang_string('aspellpath', 'admin'), new lang_string('edhelpaspellpath'), ''));
-$temp->add(new admin_setting_configexecutable('pathtodot', new lang_string('pathtodot', 'admin'), new lang_string('pathtodot_help', 'admin'), ''));
-$temp->add(new admin_setting_configexecutable('pathtogs', new lang_string('pathtogs', 'admin'), new lang_string('pathtogs_help', 'admin'), '/usr/bin/gs'));
-$temp->add(new admin_setting_configexecutable('pathtopython', new lang_string('pathtopython', 'admin'),
-    new lang_string('pathtopythondesc', 'admin'), ''));
-$ADMIN->add('server', $temp);
-
-
-
-// "supportcontact" settingpage
-$temp = new admin_settingpage('supportcontact', new lang_string('supportcontact','admin'));
-$primaryadmin = get_admin();
-if ($primaryadmin) {
-    $primaryadminemail = $primaryadmin->email;
-    $primaryadminname  = fullname($primaryadmin, true);
-} else {
-    // no defaults during installation - admin user must be created first
-    $primaryadminemail = NULL;
-    $primaryadminname  = NULL;
-}
-$temp->add(new admin_setting_configtext('supportname', new lang_string('supportname', 'admin'),
-    new lang_string('configsupportname', 'admin'), $primaryadminname, PARAM_NOTAGS));
-$setting = new admin_setting_configtext('supportemail', new lang_string('supportemail', 'admin'),
-    new lang_string('configsupportemail', 'admin'), $primaryadminemail, PARAM_EMAIL);
-$setting->set_force_ltr(true);
-$temp->add($setting);
-$temp->add(new admin_setting_configtext('supportpage', new lang_string('supportpage', 'admin'), new lang_string('configsupportpage', 'admin'), '', PARAM_URL));
-$ADMIN->add('server', $temp);
-
-
-// "sessionhandling" settingpage
-$temp = new admin_settingpage('sessionhandling', new lang_string('sessionhandling', 'admin'));
-if (empty($CFG->session_handler_class) and $DB->session_lock_supported()) {
-    $temp->add(new admin_setting_configcheckbox('dbsessions', new lang_string('dbsessions', 'admin'), new lang_string('configdbsessions', 'admin'), 0));
-}
+    // Support contact.
+    $temp = new admin_settingpage('supportcontact', new lang_string('supportcontact', 'admin'));
+    $primaryadmin = get_admin();
+    if ($primaryadmin) {
+        $primaryadminemail = $primaryadmin->email;
+        $primaryadminname = fullname($primaryadmin, true);
+    } else {
+        // No defaults during installation - admin user must be created first.
+        $primaryadminemail = null;
+        $primaryadminname = null;
+    }
+    $temp->add(new admin_setting_configtext('supportname', new lang_string('supportname', 'admin'),
+        new lang_string('configsupportname', 'admin'), $primaryadminname, PARAM_NOTAGS));
+    $setting = new admin_setting_configtext('supportemail', new lang_string('supportemail', 'admin'),
+        new lang_string('configsupportemail', 'admin'), $primaryadminemail, PARAM_EMAIL);
+    $setting->set_force_ltr(true);
+    $temp->add($setting);
+    $temp->add(new admin_setting_configtext('supportpage', new lang_string('supportpage', 'admin'),
+        new lang_string('configsupportpage', 'admin'), '', PARAM_URL));
+    $ADMIN->add('server', $temp);
 
-$temp->add(new admin_setting_configduration('sessiontimeout', new lang_string('sessiontimeout', 'admin'),
-    new lang_string('configsessiontimeout', 'admin'), 8 * 60 * 60));
-
-$temp->add(new admin_setting_configtext('sessioncookie', new lang_string('sessioncookie', 'admin'), new lang_string('configsessioncookie', 'admin'), '', PARAM_ALPHANUM));
-$temp->add(new admin_setting_configtext('sessioncookiepath', new lang_string('sessioncookiepath', 'admin'), new lang_string('configsessioncookiepath', 'admin'), '', PARAM_RAW));
-$temp->add(new admin_setting_configtext('sessioncookiedomain', new lang_string('sessioncookiedomain', 'admin'), new lang_string('configsessioncookiedomain', 'admin'), '', PARAM_RAW, 50));
-$ADMIN->add('server', $temp);
-
-
-// "stats" settingpage
-$temp = new admin_settingpage('stats', new lang_string('stats'), 'moodle/site:config', empty($CFG->enablestats));
-$temp->add(new admin_setting_configselect('statsfirstrun', new lang_string('statsfirstrun', 'admin'), new lang_string('configstatsfirstrun', 'admin'), 'none', array('none' => new lang_string('none'),
-                                                                                                                                                           60*60*24*7 => new lang_string('numweeks','moodle',1),
-                                                                                                                                                           60*60*24*14 => new lang_string('numweeks','moodle',2),
-                                                                                                                                                           60*60*24*21 => new lang_string('numweeks','moodle',3),
-                                                                                                                                                           60*60*24*28 => new lang_string('nummonths','moodle',1),
-                                                                                                                                                           60*60*24*56 => new lang_string('nummonths','moodle',2),
-                                                                                                                                                           60*60*24*84 => new lang_string('nummonths','moodle',3),
-                                                                                                                                                           60*60*24*112 => new lang_string('nummonths','moodle',4),
-                                                                                                                                                           60*60*24*140 => new lang_string('nummonths','moodle',5),
-                                                                                                                                                           60*60*24*168 => new lang_string('nummonths','moodle',6),
-                                                                                                                                                           'all' => new lang_string('all') )));
-$temp->add(new admin_setting_configselect('statsmaxruntime', new lang_string('statsmaxruntime', 'admin'), new lang_string('configstatsmaxruntime3', 'admin'), 0, array(0 => new lang_string('untilcomplete'),
-                                                                                                                                                            60*30 => '10 '.new lang_string('minutes'),
-                                                                                                                                                            60*30 => '30 '.new lang_string('minutes'),
-                                                                                                                                                            60*60 => '1 '.new lang_string('hour'),
-                                                                                                                                                            60*60*2 => '2 '.new lang_string('hours'),
-                                                                                                                                                            60*60*3 => '3 '.new lang_string('hours'),
-                                                                                                                                                            60*60*4 => '4 '.new lang_string('hours'),
-                                                                                                                                                            60*60*5 => '5 '.new lang_string('hours'),
-                                                                                                                                                            60*60*6 => '6 '.new lang_string('hours'),
-                                                                                                                                                            60*60*7 => '7 '.new lang_string('hours'),
-                                                                                                                                                            60*60*8 => '8 '.new lang_string('hours') )));
-$temp->add(new admin_setting_configtext('statsruntimedays', new lang_string('statsruntimedays', 'admin'), new lang_string('configstatsruntimedays', 'admin'), 31, PARAM_INT));
-$temp->add(new admin_setting_configtext('statsuserthreshold', new lang_string('statsuserthreshold', 'admin'), new lang_string('configstatsuserthreshold', 'admin'), 0, PARAM_INT));
-$ADMIN->add('server', $temp);
-
-
-// "http" settingpage
-$temp = new admin_settingpage('http', new lang_string('http', 'admin'));
-$temp->add(new admin_setting_configcheckbox('slasharguments', new lang_string('slasharguments', 'admin'), new lang_string('configslasharguments', 'admin'), 1));
-$temp->add(new admin_setting_heading('reverseproxy', new lang_string('reverseproxy', 'admin'), '', ''));
-$options = array(
-    0 => 'HTTP_CLIENT_IP, HTTP_X_FORWARDED_FOR, REMOTE_ADDR',
-    GETREMOTEADDR_SKIP_HTTP_CLIENT_IP => 'HTTP_X_FORWARDED_FOR, REMOTE_ADDR',
-    GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR => 'HTTP_CLIENT, REMOTE_ADDR',
-    GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP => 'REMOTE_ADDR');
-$temp->add(new admin_setting_configselect('getremoteaddrconf', new lang_string('getremoteaddrconf', 'admin'),
-    new lang_string('configgetremoteaddrconf', 'admin'),
-    GETREMOTEADDR_SKIP_DEFAULT, $options));
-$temp->add(new admin_setting_configtext('reverseproxyignore', new lang_string('reverseproxyignore', 'admin'), new lang_string('configreverseproxyignore', 'admin'), ''));
-
-$temp->add(new admin_setting_heading('webproxy', new lang_string('webproxy', 'admin'), new lang_string('webproxyinfo', 'admin')));
-$temp->add(new admin_setting_configtext('proxyhost', new lang_string('proxyhost', 'admin'), new lang_string('configproxyhost', 'admin'), '', PARAM_HOST));
-$temp->add(new admin_setting_configtext('proxyport', new lang_string('proxyport', 'admin'), new lang_string('configproxyport', 'admin'), 0, PARAM_INT));
-$options = array('HTTP'=>'HTTP');
-if (defined('CURLPROXY_SOCKS5')) {
-    $options['SOCKS5'] = 'SOCKS5';
-}
-$temp->add(new admin_setting_configselect('proxytype', new lang_string('proxytype', 'admin'), new lang_string('configproxytype','admin'), 'HTTP', $options));
-$temp->add(new admin_setting_configtext('proxyuser', new lang_string('proxyuser', 'admin'), new lang_string('configproxyuser', 'admin'), ''));
-$temp->add(new admin_setting_configpasswordunmask('proxypassword', new lang_string('proxypassword', 'admin'), new lang_string('configproxypassword', 'admin'), ''));
-$temp->add(new admin_setting_configtext('proxybypass', new lang_string('proxybypass', 'admin'), new lang_string('configproxybypass', 'admin'), 'localhost, 127.0.0.1'));
-$ADMIN->add('server', $temp);
-
-$temp = new admin_settingpage('maintenancemode', new lang_string('sitemaintenancemode', 'admin'));
-$options = array(0=>new lang_string('disable'), 1=>new lang_string('enable'));
-$temp->add(new admin_setting_configselect('maintenance_enabled', new lang_string('sitemaintenancemode', 'admin'),
-                                          new lang_string('helpsitemaintenance', 'admin'), 0, $options));
-$temp->add(new admin_setting_confightmleditor('maintenance_message', new lang_string('optionalmaintenancemessage', 'admin'),
-                                              '', ''));
-$ADMIN->add('server', $temp);
-
-$temp = new admin_settingpage('cleanup', new lang_string('cleanup', 'admin'));
-$temp->add(new admin_setting_configselect('deleteunconfirmed', new lang_string('deleteunconfirmed', 'admin'), new lang_string('configdeleteunconfirmed', 'admin'), 168, array(0 => new lang_string('never'),
-                                                                                                                                                                    168 => new lang_string('numdays', '', 7),
-                                                                                                                                                                    144 => new lang_string('numdays', '', 6),
-                                                                                                                                                                    120 => new lang_string('numdays', '', 5),
-                                                                                                                                                                    96 => new lang_string('numdays', '', 4),
-                                                                                                                                                                    72 => new lang_string('numdays', '', 3),
-                                                                                                                                                                    48 => new lang_string('numdays', '', 2),
-                                                                                                                                                                    24 => new lang_string('numdays', '', 1),
-                                                                                                                                                                    12 => new lang_string('numhours', '', 12),
-                                                                                                                                                                    6 => new lang_string('numhours', '', 6),
-                                                                                                                                                                    1 => new lang_string('numhours', '', 1))));
-
-$temp->add(new admin_setting_configselect('deleteincompleteusers', new lang_string('deleteincompleteusers', 'admin'), new lang_string('configdeleteincompleteusers', 'admin'), 0, array(0 => new lang_string('never'),
-                                                                                                                                                                    168 => new lang_string('numdays', '', 7),
-                                                                                                                                                                    144 => new lang_string('numdays', '', 6),
-                                                                                                                                                                    120 => new lang_string('numdays', '', 5),
-                                                                                                                                                                    96 => new lang_string('numdays', '', 4),
-                                                                                                                                                                    72 => new lang_string('numdays', '', 3),
-                                                                                                                                                                    48 => new lang_string('numdays', '', 2),
-                                                                                                                                                                    24 => new lang_string('numdays', '', 1))));
-
-
-$temp->add(new admin_setting_configcheckbox('disablegradehistory', new lang_string('disablegradehistory', 'grades'),
-                                            new lang_string('disablegradehistory_help', 'grades'), 0));
-
-$temp->add(new admin_setting_configselect('gradehistorylifetime', new lang_string('gradehistorylifetime', 'grades'),
-                                          new lang_string('gradehistorylifetime_help', 'grades'), 0, array(0 => new lang_string('neverdeletehistory', 'grades'),
-                                                                                                   1000 => new lang_string('numdays', '', 1000),
-                                                                                                    365 => new lang_string('numdays', '', 365),
-                                                                                                    180 => new lang_string('numdays', '', 180),
-                                                                                                    150 => new lang_string('numdays', '', 150),
-                                                                                                    120 => new lang_string('numdays', '', 120),
-                                                                                                     90 => new lang_string('numdays', '', 90),
-                                                                                                     60 => new lang_string('numdays', '', 60),
-                                                                                                     30 => new lang_string('numdays', '', 30))));
-
-$temp->add(new admin_setting_configselect('tempdatafoldercleanup', new lang_string('tempdatafoldercleanup', 'admin'),
-        new lang_string('configtempdatafoldercleanup', 'admin'), 168, array(
+    // Session handling.
+    $temp = new admin_settingpage('sessionhandling', new lang_string('sessionhandling', 'admin'));
+    if (empty($CFG->session_handler_class) and $DB->session_lock_supported()) {
+        $temp->add(new admin_setting_configcheckbox('dbsessions', new lang_string('dbsessions', 'admin'),
+            new lang_string('configdbsessions', 'admin'), 0));
+    }
+
+    $temp->add(new admin_setting_configduration('sessiontimeout', new lang_string('sessiontimeout', 'admin'),
+        new lang_string('configsessiontimeout', 'admin'), 8 * 60 * 60));
+
+    $temp->add(new admin_setting_configtext('sessioncookie', new lang_string('sessioncookie', 'admin'),
+        new lang_string('configsessioncookie', 'admin'), '', PARAM_ALPHANUM));
+    $temp->add(new admin_setting_configtext('sessioncookiepath', new lang_string('sessioncookiepath', 'admin'),
+        new lang_string('configsessioncookiepath', 'admin'), '', PARAM_RAW));
+    $temp->add(new admin_setting_configtext('sessioncookiedomain', new lang_string('sessioncookiedomain', 'admin'),
+        new lang_string('configsessioncookiedomain', 'admin'), '', PARAM_RAW, 50));
+    $ADMIN->add('server', $temp);
+
+    // Statistics.
+    $temp = new admin_settingpage('stats', new lang_string('stats'), 'moodle/site:config', empty($CFG->enablestats));
+    $temp->add(new admin_setting_configselect('statsfirstrun', new lang_string('statsfirstrun', 'admin'),
+        new lang_string('configstatsfirstrun', 'admin'), 'none',
+        [
+            'none' => new lang_string('none'),
+            60 * 60 * 24 * 7 => new lang_string('numweeks', 'moodle', 1),
+            60 * 60 * 24 * 14 => new lang_string('numweeks', 'moodle', 2),
+            60 * 60 * 24 * 21 => new lang_string('numweeks', 'moodle', 3),
+            60 * 60 * 24 * 28 => new lang_string('nummonths', 'moodle', 1),
+            60 * 60 * 24 * 56 => new lang_string('nummonths', 'moodle', 2),
+            60 * 60 * 24 * 84 => new lang_string('nummonths', 'moodle', 3),
+            60 * 60 * 24 * 112 => new lang_string('nummonths', 'moodle', 4),
+            60 * 60 * 24 * 140 => new lang_string('nummonths', 'moodle', 5),
+            60 * 60 * 24 * 168 => new lang_string('nummonths', 'moodle', 6),
+            'all' => new lang_string('all')
+        ]
+    ));
+    $temp->add(new admin_setting_configselect('statsmaxruntime', new lang_string('statsmaxruntime', 'admin'),
+        new lang_string('configstatsmaxruntime3', 'admin'), 0,
+        [
+            0 => new lang_string('untilcomplete'),
+            60 * 30 => '10 ' . new lang_string('minutes'),
+            60 * 30 => '30 ' . new lang_string('minutes'),
+            60 * 60 => '1 ' . new lang_string('hour'),
+            60 * 60 * 2 => '2 ' . new lang_string('hours'),
+            60 * 60 * 3 => '3 ' . new lang_string('hours'),
+            60 * 60 * 4 => '4 ' . new lang_string('hours'),
+            60 * 60 * 5 => '5 ' . new lang_string('hours'),
+            60 * 60 * 6 => '6 ' . new lang_string('hours'),
+            60 * 60 * 7 => '7 ' . new lang_string('hours'),
+            60 * 60 * 8 => '8 ' . new lang_string('hours'),
+        ]
+    ));
+    $temp->add(new admin_setting_configtext('statsruntimedays', new lang_string('statsruntimedays', 'admin'),
+        new lang_string('configstatsruntimedays', 'admin'), 31, PARAM_INT));
+    $temp->add(new admin_setting_configtext('statsuserthreshold', new lang_string('statsuserthreshold', 'admin'),
+        new lang_string('configstatsuserthreshold', 'admin'), 0, PARAM_INT));
+    $ADMIN->add('server', $temp);
+
+    // HTTP.
+    $temp = new admin_settingpage('http', new lang_string('http', 'admin'));
+    $temp->add(new admin_setting_configcheckbox('slasharguments', new lang_string('slasharguments', 'admin'),
+        new lang_string('configslasharguments', 'admin'), 1));
+    $temp->add(new admin_setting_heading('reverseproxy', new lang_string('reverseproxy', 'admin'), '', ''));
+    $options = [
+        0 => 'HTTP_CLIENT_IP, HTTP_X_FORWARDED_FOR, REMOTE_ADDR',
+        GETREMOTEADDR_SKIP_HTTP_CLIENT_IP => 'HTTP_X_FORWARDED_FOR, REMOTE_ADDR',
+        GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR => 'HTTP_CLIENT, REMOTE_ADDR',
+        GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR | GETREMOTEADDR_SKIP_HTTP_CLIENT_IP => 'REMOTE_ADDR'
+    ];
+    $temp->add(new admin_setting_configselect('getremoteaddrconf', new lang_string('getremoteaddrconf', 'admin'),
+        new lang_string('configgetremoteaddrconf', 'admin'),
+        GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR | GETREMOTEADDR_SKIP_HTTP_CLIENT_IP, $options));
+    $temp->add(new admin_setting_configtext('reverseproxyignore', new lang_string('reverseproxyignore', 'admin'),
+        new lang_string('configreverseproxyignore', 'admin'), ''));
+
+    $temp->add(new admin_setting_heading('webproxy', new lang_string('webproxy', 'admin'),
+        new lang_string('webproxyinfo', 'admin')));
+    $temp->add(new admin_setting_configtext('proxyhost', new lang_string('proxyhost', 'admin'),
+        new lang_string('configproxyhost', 'admin'), '', PARAM_HOST));
+    $temp->add(new admin_setting_configtext('proxyport', new lang_string('proxyport', 'admin'),
+        new lang_string('configproxyport', 'admin'), 0, PARAM_INT));
+    $options = ['HTTP' => 'HTTP'];
+    if (defined('CURLPROXY_SOCKS5')) {
+        $options['SOCKS5'] = 'SOCKS5';
+    }
+    $temp->add(new admin_setting_configselect('proxytype', new lang_string('proxytype', 'admin'),
+        new lang_string('configproxytype', 'admin'), 'HTTP', $options));
+    $temp->add(new admin_setting_configtext('proxyuser', new lang_string('proxyuser', 'admin'),
+        new lang_string('configproxyuser', 'admin'), ''));
+    $temp->add(new admin_setting_configpasswordunmask('proxypassword', new lang_string('proxypassword', 'admin'),
+        new lang_string('configproxypassword', 'admin'), ''));
+    $temp->add(new admin_setting_configtext('proxybypass', new lang_string('proxybypass', 'admin'),
+        new lang_string('configproxybypass', 'admin'), 'localhost, 127.0.0.1'));
+    $ADMIN->add('server', $temp);
+
+    $temp = new admin_settingpage('maintenancemode', new lang_string('sitemaintenancemode', 'admin'));
+    $options = [0 => new lang_string('disable'), 1 => new lang_string('enable')];
+    $temp->add(new admin_setting_configselect('maintenance_enabled', new lang_string('sitemaintenancemode', 'admin'),
+        new lang_string('helpsitemaintenance', 'admin'), 0, $options));
+    $temp->add(new admin_setting_confightmleditor('maintenance_message', new lang_string('optionalmaintenancemessage', 'admin'),
+        '', ''));
+    $ADMIN->add('server', $temp);
+
+    // Cleanup.
+    $temp = new admin_settingpage('cleanup', new lang_string('cleanup', 'admin'));
+    $temp->add(new admin_setting_configselect('deleteunconfirmed', new lang_string('deleteunconfirmed', 'admin'),
+        new lang_string('configdeleteunconfirmed', 'admin'), 168,
+        [
+            0 => new lang_string('never'),
+            168 => new lang_string('numdays', '', 7),
+            144 => new lang_string('numdays', '', 6),
+            120 => new lang_string('numdays', '', 5),
+            96 => new lang_string('numdays', '', 4),
+            72 => new lang_string('numdays', '', 3),
+            48 => new lang_string('numdays', '', 2),
+            24 => new lang_string('numdays', '', 1),
+            12 => new lang_string('numhours', '', 12),
+            6 => new lang_string('numhours', '', 6),
+            1 => new lang_string('numhours', '', 1),
+        ]
+    ));
+
+    $temp->add(new admin_setting_configselect('deleteincompleteusers', new lang_string('deleteincompleteusers', 'admin'),
+        new lang_string('configdeleteincompleteusers', 'admin'), 0,
+        [
+            0 => new lang_string('never'),
+            168 => new lang_string('numdays', '', 7),
+            144 => new lang_string('numdays', '', 6),
+            120 => new lang_string('numdays', '', 5),
+            96 => new lang_string('numdays', '', 4),
+            72 => new lang_string('numdays', '', 3),
+            48 => new lang_string('numdays', '', 2),
+            24 => new lang_string('numdays', '', 1),
+        ]
+    ));
+
+    $temp->add(new admin_setting_configcheckbox('disablegradehistory', new lang_string('disablegradehistory', 'grades'),
+        new lang_string('disablegradehistory_help', 'grades'), 0));
+
+    $temp->add(new admin_setting_configselect('gradehistorylifetime', new lang_string('gradehistorylifetime', 'grades'),
+        new lang_string('gradehistorylifetime_help', 'grades'), 0,
+        [
+            0 => new lang_string('neverdeletehistory', 'grades'),
+            1000 => new lang_string('numdays', '', 1000),
+            365 => new lang_string('numdays', '', 365),
+            180 => new lang_string('numdays', '', 180),
+            150 => new lang_string('numdays', '', 150),
+            120 => new lang_string('numdays', '', 120),
+            90 => new lang_string('numdays', '', 90),
+            60 => new lang_string('numdays', '', 60),
+            30 => new lang_string('numdays', '', 30),
+        ]
+    ));
+
+    $temp->add(new admin_setting_configselect('tempdatafoldercleanup', new lang_string('tempdatafoldercleanup', 'admin'),
+        new lang_string('configtempdatafoldercleanup', 'admin'), 168,
+        [
             1 => new lang_string('numhours', '', 1),
             3 => new lang_string('numhours', '', 3),
             6 => new lang_string('numhours', '', 6),
@@ -166,227 +229,283 @@ $temp->add(new admin_setting_configselect('tempdatafoldercleanup', new lang_stri
             24 => new lang_string('numhours', '', 24),
             48 => new lang_string('numdays', '', 2),
             168 => new lang_string('numdays', '', 7),
-)));
+        ]
+    ));
 
-$ADMIN->add('server', $temp);
+    $ADMIN->add('server', $temp);
 
     $temp->add(new admin_setting_configduration('filescleanupperiod',
         new lang_string('filescleanupperiod', 'admin'),
         new lang_string('filescleanupperiod_help', 'admin'),
         86400));
 
-$ADMIN->add('server', new admin_externalpage('environment', new lang_string('environment','admin'), "$CFG->wwwroot/$CFG->admin/environment.php"));
-$ADMIN->add('server', new admin_externalpage('phpinfo', new lang_string('phpinfo'), "$CFG->wwwroot/$CFG->admin/phpinfo.php"));
-$ADMIN->add('server', new admin_externalpage('testoutgoingmailconf', new lang_string('testoutgoingmailconf', 'admin'),
-            new moodle_url("$CFG->wwwroot/$CFG->admin/testoutgoingmailconf.php"), 'moodle/site:config', true));
+    // Environment.
+    $ADMIN->add('server', new admin_externalpage('environment', new lang_string('environment', 'admin'),
+        "{$CFG->wwwroot}/{$CFG->admin}/environment.php"));
+
+    // PHP info.
+    $ADMIN->add('server', new admin_externalpage('phpinfo', new lang_string('phpinfo'),
+        "{$CFG->wwwroot}/{$CFG->admin}/phpinfo.php"));
+
+    // Test outgoing mail configuration (hidden, accessed via direct link from the settings page).
+    $ADMIN->add('server', new admin_externalpage('testoutgoingmailconf', new lang_string('testoutgoingmailconf', 'admin'),
+        new moodle_url('/admin/testoutgoingmailconf.php'), 'moodle/site:config', true));
+
+    // Performance.
+    $temp = new admin_settingpage('performance', new lang_string('performance', 'admin'));
+
+    // Memory limit options for large administration tasks.
+    $memoryoptions = [
+        '64M' => '64M',
+        '128M' => '128M',
+        '256M' => '256M',
+        '512M' => '512M',
+        '1024M' => '1024M',
+        '2048M' => '2048M',
+    ];
+
+    // Allow larger memory usage for 64-bit sites only.
+    if (PHP_INT_SIZE === 8) {
+        $memoryoptions['3072M'] = '3072M';
+        $memoryoptions['4096M'] = '4096M';
+    }
+
+    $temp->add(new admin_setting_configselect('extramemorylimit', new lang_string('extramemorylimit', 'admin'),
+        new lang_string('configextramemorylimit', 'admin'), '512M', $memoryoptions));
+
+    $temp->add(new admin_setting_configtext('maxtimelimit', new lang_string('maxtimelimit', 'admin'),
+        new lang_string('maxtimelimit_desc', 'admin'), 0, PARAM_INT));
 
+    $temp->add(new admin_setting_configtext('curlcache', new lang_string('curlcache', 'admin'),
+        new lang_string('configcurlcache', 'admin'), 120, PARAM_INT));
 
-// "performance" settingpage
-$temp = new admin_settingpage('performance', new lang_string('performance', 'admin'));
+    $temp->add(new admin_setting_configtext('curltimeoutkbitrate', new lang_string('curltimeoutkbitrate', 'admin'),
+        new lang_string('curltimeoutkbitrate_help', 'admin'), 56, PARAM_INT));
 
-// Memory limit options for large administration tasks.
-$memoryoptions = array(
-    '64M' => '64M',
-    '128M' => '128M',
-    '256M' => '256M',
-    '512M' => '512M',
-    '1024M' => '1024M',
-    '2048M' => '2048M');
+    $ADMIN->add('server', $temp);
 
-// Allow larger memory usage for 64-bit sites only.
-if (PHP_INT_SIZE === 8) {
-    $memoryoptions['3072M'] = '3072M';
-    $memoryoptions['4096M'] = '4096M';
-}
+    // Tasks.
+    $ADMIN->add('server', new admin_category('taskconfig', new lang_string('taskadmintitle', 'admin')));
 
-$temp->add(new admin_setting_configselect('extramemorylimit', new lang_string('extramemorylimit', 'admin'),
-                                          new lang_string('configextramemorylimit', 'admin'), '512M',
-                                          $memoryoptions));
-$temp->add(new admin_setting_configtext('maxtimelimit', new lang_string('maxtimelimit', 'admin'),
-        new lang_string('maxtimelimit_desc', 'admin'), 0, PARAM_INT));
+    // Task processing.
+    $temp = new admin_settingpage('taskprocessing', new lang_string('taskprocessing', 'admin'));
 
-$temp->add(new admin_setting_configtext('curlcache', new lang_string('curlcache', 'admin'),
-                                        new lang_string('configcurlcache', 'admin'), 120, PARAM_INT));
-
-$temp->add(new admin_setting_configtext('curltimeoutkbitrate', new lang_string('curltimeoutkbitrate', 'admin'),
-                                        new lang_string('curltimeoutkbitrate_help', 'admin'), 56, PARAM_INT));
-
-$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'));
-$temp->add(
-    new admin_setting_configtext(
-        'task_scheduled_concurrency_limit',
-        new lang_string('task_scheduled_concurrency_limit', 'admin'),
-        new lang_string('task_scheduled_concurrency_limit_desc', 'admin'),
-        3,
-        PARAM_INT
-    )
-);
-
-$temp->add(
-    new admin_setting_configduration(
-        'task_scheduled_max_runtime',
-        new lang_string('task_scheduled_max_runtime', 'admin'),
-        new lang_string('task_scheduled_max_runtime_desc', 'admin'),
-        30 * MINSECS
-    )
-);
-
-$temp->add(
-    new admin_setting_configtext(
-        'task_adhoc_concurrency_limit',
-        new lang_string('task_adhoc_concurrency_limit', 'admin'),
-        new lang_string('task_adhoc_concurrency_limit_desc', 'admin'),
-        3,
-        PARAM_INT
-    )
-);
-
-$temp->add(
-    new admin_setting_configduration(
-        'task_adhoc_max_runtime',
-        new lang_string('task_adhoc_max_runtime', 'admin'),
-        new lang_string('task_adhoc_max_runtime_desc', 'admin'),
-        30 * MINSECS
-    )
-);
-$ADMIN->add('taskconfig', $temp);
-
-$temp = new admin_settingpage('tasklogging', new lang_string('tasklogging','admin'));
-$temp->add(
-    new admin_setting_configselect(
-        'task_logmode',
-        new lang_string('task_logmode', 'admin'),
-        new lang_string('task_logmode_desc', 'admin'),
-        \core\task\logmanager::MODE_ALL,
-        [
-            \core\task\logmanager::MODE_ALL => new lang_string('task_logmode_all', 'admin'),
-            \core\task\logmanager::MODE_FAILONLY => new lang_string('task_logmode_failonly', 'admin'),
-            \core\task\logmanager::MODE_NONE => new lang_string('task_logmode_none', 'admin'),
-        ]
-    )
-);
-$temp->add(
-    new admin_setting_configcheckbox(
-        'task_logtostdout',
-        new lang_string('task_logtostdout', 'admin'),
-        new lang_string('task_logtostdout_desc', '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',
+            new lang_string('task_scheduled_concurrency_limit', 'admin'),
+            new lang_string('task_scheduled_concurrency_limit_desc', 'admin'),
+            3,
+            PARAM_INT
+        )
+    );
 
-if (\core\task\logmanager::uses_standard_settings()) {
     $temp->add(
         new admin_setting_configduration(
-            'task_logretention',
-            new \lang_string('task_logretention', 'admin'),
-            new \lang_string('task_logretention_desc', 'admin'),
-            28 * DAYSECS
+            'task_scheduled_max_runtime',
+            new lang_string('task_scheduled_max_runtime', 'admin'),
+            new lang_string('task_scheduled_max_runtime_desc', 'admin'),
+            30 * MINSECS
         )
     );
 
     $temp->add(
         new admin_setting_configtext(
-            'task_logretainruns',
-            new \lang_string('task_logretainruns', 'admin'),
-            new \lang_string('task_logretainruns_desc', 'admin'),
-            20,
+            'task_adhoc_concurrency_limit',
+            new lang_string('task_adhoc_concurrency_limit', 'admin'),
+            new lang_string('task_adhoc_concurrency_limit_desc', 'admin'),
+            3,
             PARAM_INT
         )
     );
-}
-$ADMIN->add('taskconfig', $temp);
 
-if (\core\task\logmanager::uses_standard_settings()) {
-    $ADMIN->add('taskconfig', new admin_externalpage(
-        'tasklogs',
-        new lang_string('tasklogs','admin'),
-        "{$CFG->wwwroot}/{$CFG->admin}/tasklogs.php"
-    ));
-}
+    $temp->add(
+        new admin_setting_configduration(
+            'task_adhoc_max_runtime',
+            new lang_string('task_adhoc_max_runtime', 'admin'),
+            new lang_string('task_adhoc_max_runtime_desc', 'admin'),
+            30 * MINSECS
+        )
+    );
+    $ADMIN->add('taskconfig', $temp);
 
-// E-mail settings.
-$ADMIN->add('server', new admin_category('email', new lang_string('categoryemail', 'admin')));
-
-$temp = new admin_settingpage('outgoingmailconfig', new lang_string('outgoingmailconfig', 'admin'));
-
-$temp->add(new admin_setting_heading('smtpheading', new lang_string('smtp', 'admin'),
-            new lang_string('smtpdetail', 'admin')));
-$temp->add(new admin_setting_configtext('smtphosts', new lang_string('smtphosts', 'admin'),
-            new lang_string('configsmtphosts', 'admin'), '', PARAM_RAW));
-$options = array('' => new lang_string('none', 'admin'), 'ssl' => 'SSL', 'tls' => 'TLS');
-$temp->add(new admin_setting_configselect('smtpsecure', new lang_string('smtpsecure', 'admin'),
-            new lang_string('configsmtpsecure', 'admin'), '', $options));
-$authtypeoptions = array('LOGIN' => 'LOGIN', 'PLAIN' => 'PLAIN', 'NTLM' => 'NTLM', 'CRAM-MD5' => 'CRAM-MD5');
-$temp->add(new admin_setting_configselect('smtpauthtype', new lang_string('smtpauthtype', 'admin'),
-            new lang_string('configsmtpauthtype', 'admin'), 'LOGIN', $authtypeoptions));
-$temp->add(new admin_setting_configtext('smtpuser', new lang_string('smtpuser', 'admin'),
-            new lang_string('configsmtpuser', 'admin'), '', PARAM_NOTAGS));
-$temp->add(new admin_setting_configpasswordunmask('smtppass', new lang_string('smtppass', 'admin'),
-            new lang_string('configsmtpuser', 'admin'), ''));
-$temp->add(new admin_setting_configtext('smtpmaxbulk', new lang_string('smtpmaxbulk', 'admin'),
-           new lang_string('configsmtpmaxbulk', 'admin'), 1, PARAM_INT));
-$temp->add(new admin_setting_heading('noreplydomainheading', new lang_string('noreplydomain', 'admin'),
+    // Task log configuration.
+    $temp = new admin_settingpage('tasklogging', new lang_string('tasklogging', 'admin'));
+    $temp->add(
+        new admin_setting_configselect(
+            'task_logmode',
+            new lang_string('task_logmode', 'admin'),
+            new lang_string('task_logmode_desc', 'admin'),
+            \core\task\logmanager::MODE_ALL,
+            [
+                \core\task\logmanager::MODE_ALL => new lang_string('task_logmode_all', 'admin'),
+                \core\task\logmanager::MODE_FAILONLY => new lang_string('task_logmode_failonly', 'admin'),
+                \core\task\logmanager::MODE_NONE => new lang_string('task_logmode_none', 'admin'),
+            ]
+        )
+    );
+    $temp->add(
+        new admin_setting_configcheckbox(
+            'task_logtostdout',
+            new lang_string('task_logtostdout', 'admin'),
+            new lang_string('task_logtostdout_desc', 'admin'),
+            1
+        )
+    );
+
+    if (\core\task\logmanager::uses_standard_settings()) {
+        $temp->add(
+            new admin_setting_configduration(
+                'task_logretention',
+                new \lang_string('task_logretention', 'admin'),
+                new \lang_string('task_logretention_desc', 'admin'),
+                28 * DAYSECS
+            )
+        );
+
+        $temp->add(
+            new admin_setting_configtext(
+                'task_logretainruns',
+                new \lang_string('task_logretainruns', 'admin'),
+                new \lang_string('task_logretainruns_desc', 'admin'),
+                20,
+                PARAM_INT
+            )
+        );
+    }
+    $ADMIN->add('taskconfig', $temp);
+
+    // Task logs.
+    if (\core\task\logmanager::uses_standard_settings()) {
+        $ADMIN->add('taskconfig', new admin_externalpage(
+            'tasklogs',
+            new lang_string('tasklogs', 'admin'),
+            "{$CFG->wwwroot}/{$CFG->admin}/tasklogs.php"
+        ));
+    }
+
+    // Email.
+    $ADMIN->add('server', new admin_category('email', new lang_string('categoryemail', 'admin')));
+
+    // Outgoing mail configuration.
+    $temp = new admin_settingpage('outgoingmailconfig', new lang_string('outgoingmailconfig', 'admin'));
+
+    $temp->add(new admin_setting_heading('smtpheading', new lang_string('smtp', 'admin'),
+        new lang_string('smtpdetail', 'admin')));
+
+    $temp->add(new admin_setting_configtext('smtphosts', new lang_string('smtphosts', 'admin'),
+        new lang_string('configsmtphosts', 'admin'), '', PARAM_RAW));
+
+    $options = [
+        '' => new lang_string('none', 'admin'),
+        'ssl' => 'SSL',
+        'tls' => 'TLS',
+    ];
+
+    $temp->add(new admin_setting_configselect('smtpsecure', new lang_string('smtpsecure', 'admin'),
+        new lang_string('configsmtpsecure', 'admin'), '', $options));
+
+    $authtypeoptions = [
+        'LOGIN' => 'LOGIN',
+        'PLAIN' => 'PLAIN',
+        'NTLM' => 'NTLM',
+        'CRAM-MD5' => 'CRAM-MD5',
+    ];
+
+    $temp->add(new admin_setting_configselect('smtpauthtype', new lang_string('smtpauthtype', 'admin'),
+        new lang_string('configsmtpauthtype', 'admin'), 'LOGIN', $authtypeoptions));
+
+    $temp->add(new admin_setting_configtext('smtpuser', new lang_string('smtpuser', 'admin'),
+        new lang_string('configsmtpuser', 'admin'), '', PARAM_NOTAGS));
+
+    $temp->add(new admin_setting_configpasswordunmask('smtppass', new lang_string('smtppass', 'admin'),
+        new lang_string('configsmtpuser', 'admin'), ''));
+
+    $temp->add(new admin_setting_configtext('smtpmaxbulk', new lang_string('smtpmaxbulk', 'admin'),
+        new lang_string('configsmtpmaxbulk', 'admin'), 1, PARAM_INT));
+
+    $temp->add(new admin_setting_heading('noreplydomainheading', new lang_string('noreplydomain', 'admin'),
         new lang_string('noreplydomaindetail', 'admin')));
-$temp->add(new admin_setting_configtext('noreplyaddress', new lang_string('noreplyaddress', 'admin'),
-          new lang_string('confignoreplyaddress', 'admin'), 'noreply@' . get_host_from_url($CFG->wwwroot), PARAM_EMAIL));
-$temp->add(new admin_setting_configtextarea('allowedemaildomains',
+
+    $temp->add(new admin_setting_configtext('noreplyaddress', new lang_string('noreplyaddress', 'admin'),
+        new lang_string('confignoreplyaddress', 'admin'), 'noreply@' . get_host_from_url($CFG->wwwroot), PARAM_EMAIL));
+
+    $temp->add(new admin_setting_configtextarea('allowedemaildomains',
         new lang_string('allowedemaildomains', 'admin'),
         new lang_string('configallowedemaildomains', 'admin'),
         ''));
-$url = new moodle_url('/admin/testoutgoingmailconf.php');
-$link = html_writer::link($url, get_string('testoutgoingmailconf', 'admin'));
-$temp->add(new admin_setting_heading('testoutgoinmailc', new lang_string('testoutgoingmailconf', 'admin'),
+
+    $url = new moodle_url('/admin/testoutgoingmailconf.php');
+    $link = html_writer::link($url, get_string('testoutgoingmailconf', 'admin'));
+    $temp->add(new admin_setting_heading('testoutgoinmailc', new lang_string('testoutgoingmailconf', 'admin'),
         new lang_string('testoutgoingmaildetail', 'admin', $link)));
-$temp->add(new admin_setting_heading('emaildoesnotfit', new lang_string('doesnotfit', 'admin'),
+
+    $temp->add(new admin_setting_heading('emaildoesnotfit', new lang_string('doesnotfit', 'admin'),
         new lang_string('doesnotfitdetail', 'admin')));
-$charsets = get_list_of_charsets();
-unset($charsets['UTF-8']); // Not needed here.
-$options = array();
-$options['0'] = 'UTF-8';
-$options = array_merge($options, $charsets);
-$temp->add(new admin_setting_configselect('sitemailcharset', new lang_string('sitemailcharset', 'admin'),
-          new lang_string('configsitemailcharset','admin'), '0', $options));
-$temp->add(new admin_setting_configcheckbox('allowusermailcharset', new lang_string('allowusermailcharset', 'admin'),
-          new lang_string('configallowusermailcharset', 'admin'), 0));
-$temp->add(new admin_setting_configcheckbox('allowattachments', new lang_string('allowattachments', 'admin'),
-          new lang_string('configallowattachments', 'admin'), 1));
-$options = array('LF' => 'LF', 'CRLF' => 'CRLF');
-$temp->add(new admin_setting_configselect('mailnewline', new lang_string('mailnewline', 'admin'),
-          new lang_string('configmailnewline', 'admin'), 'LF', $options));
-
-$choices = array(new lang_string('never', 'admin'),
-                 new lang_string('always', 'admin'),
-                 new lang_string('onlynoreply', 'admin'));
-$temp->add(new admin_setting_configselect('emailfromvia', new lang_string('emailfromvia', 'admin'),
-          new lang_string('configemailfromvia', 'admin'), 1, $choices));
-
-$temp->add(new admin_setting_configtext('emailsubjectprefix', new lang_string('emailsubjectprefix', 'admin'),
+
+    $charsets = get_list_of_charsets();
+    unset($charsets['UTF-8']);
+    $options = [
+        '0' => 'UTF-8',
+    ];
+    $options = array_merge($options, $charsets);
+    $temp->add(new admin_setting_configselect('sitemailcharset', new lang_string('sitemailcharset', 'admin'),
+        new lang_string('configsitemailcharset', 'admin'), '0', $options));
+
+    $temp->add(new admin_setting_configcheckbox('allowusermailcharset', new lang_string('allowusermailcharset', 'admin'),
+        new lang_string('configallowusermailcharset', 'admin'), 0));
+
+    $temp->add(new admin_setting_configcheckbox('allowattachments', new lang_string('allowattachments', 'admin'),
+        new lang_string('configallowattachments', 'admin'), 1));
+
+    $options = [
+        'LF' => 'LF',
+        'CRLF' => 'CRLF',
+    ];
+    $temp->add(new admin_setting_configselect('mailnewline', new lang_string('mailnewline', 'admin'),
+        new lang_string('configmailnewline', 'admin'), 'LF', $options));
+
+    $choices = [
+        new lang_string('never', 'admin'),
+        new lang_string('always', 'admin'),
+        new lang_string('onlynoreply', 'admin'),
+    ];
+    $temp->add(new admin_setting_configselect('emailfromvia', new lang_string('emailfromvia', 'admin'),
+        new lang_string('configemailfromvia', 'admin'), 1, $choices));
+
+    $temp->add(new admin_setting_configtext('emailsubjectprefix', new lang_string('emailsubjectprefix', 'admin'),
         new lang_string('configemailsubjectprefix', 'admin'), '', PARAM_RAW));
-$temp->add(new admin_setting_configtextarea('emailheaders', new lang_string('emailheaders', 'admin'),
+
+    $temp->add(new admin_setting_configtextarea('emailheaders', new lang_string('emailheaders', 'admin'),
         new lang_string('configemailheaders', 'admin'), '', PARAM_RAW, '50', '3'));
 
-$ADMIN->add('email', $temp);
-
-// "update notifications" settingpage
-if (empty($CFG->disableupdatenotifications)) {
-    $temp = new admin_settingpage('updatenotifications', new lang_string('updatenotifications', 'core_admin'));
-    $temp->add(new admin_setting_configcheckbox('updateautocheck', new lang_string('updateautocheck', 'core_admin'),
-                                                new lang_string('updateautocheck_desc', 'core_admin'), 1));
-    $temp->add(new admin_setting_configselect('updateminmaturity', new lang_string('updateminmaturity', 'core_admin'),
-                                              new lang_string('updateminmaturity_desc', 'core_admin'), MATURITY_STABLE,
-                                              array(
-                                                  MATURITY_ALPHA  => new lang_string('maturity'.MATURITY_ALPHA, 'core_admin'),
-                                                  MATURITY_BETA   => new lang_string('maturity'.MATURITY_BETA, 'core_admin'),
-                                                  MATURITY_RC     => new lang_string('maturity'.MATURITY_RC, 'core_admin'),
-                                                  MATURITY_STABLE => new lang_string('maturity'.MATURITY_STABLE, 'core_admin'),
-                                              )));
-    $temp->add(new admin_setting_configcheckbox('updatenotifybuilds', new lang_string('updatenotifybuilds', 'core_admin'),
-                                                new lang_string('updatenotifybuilds_desc', 'core_admin'), 0));
-    $ADMIN->add('server', $temp);
+    $ADMIN->add('email', $temp);
+
+    // Update notifications.
+    if (empty($CFG->disableupdatenotifications)) {
+        $temp = new admin_settingpage('updatenotifications', new lang_string('updatenotifications', 'core_admin'));
+        $temp->add(new admin_setting_configcheckbox('updateautocheck', new lang_string('updateautocheck', 'core_admin'),
+            new lang_string('updateautocheck_desc', 'core_admin'), 1));
+        $temp->add(new admin_setting_configselect('updateminmaturity', new lang_string('updateminmaturity', 'core_admin'),
+            new lang_string('updateminmaturity_desc', 'core_admin'), MATURITY_STABLE,
+            [
+                MATURITY_ALPHA => new lang_string('maturity'.MATURITY_ALPHA, 'core_admin'),
+                MATURITY_BETA => new lang_string('maturity'.MATURITY_BETA, 'core_admin'),
+                MATURITY_RC => new lang_string('maturity'.MATURITY_RC, 'core_admin'),
+                MATURITY_STABLE => new lang_string('maturity'.MATURITY_STABLE, 'core_admin'),
+            ]
+        ));
+        $temp->add(new admin_setting_configcheckbox('updatenotifybuilds', new lang_string('updatenotifybuilds', 'core_admin'),
+            new lang_string('updatenotifybuilds_desc', 'core_admin'), 0));
+        $ADMIN->add('server', $temp);
+    }
 }
-
-} // end of speedup
diff --git a/admin/tool/dataprivacy/lang/en/deprecated.txt b/admin/tool/dataprivacy/lang/en/deprecated.txt
deleted file mode 100644 (file)
index c400ae8..0000000
+++ /dev/null
@@ -1 +0,0 @@
-statuspreprocessing,tool_dataprivacy
index 63d33fe..46d7af2 100644 (file)
@@ -135,7 +135,7 @@ $string['effectiveretentionperioduser'] = '{$a} (since the last time the user ac
 $string['emailsalutation'] = 'Dear {$a},';
 $string['errorcannotrequestdeleteforself'] = 'You don\'t have permission to create deletion request for yourself.';
 $string['errorcannotrequestdeleteforother'] = 'You don\'t have permission to create deletion request for this user.';
-$string['errorinvalidrequestcomments'] = 'Please ensure your comment contains plain text only.';
+$string['errorinvalidrequestcomments'] = 'The comments field may contain plain text only.';
 $string['errorinvalidrequestcreationmethod'] = 'Invalid request creation method!';
 $string['errorinvalidrequeststatus'] = 'Invalid request status!';
 $string['errorinvalidrequesttype'] = 'Invalid request type!';
@@ -344,6 +344,3 @@ $string['role_help'] = 'The role which the override should apply to.';
 $string['duplicaterole'] = 'Role already specified';
 $string['purposeoverview'] = 'A purpose describes the intended use and retention policy for stored data. The basis for storing and retaining that data is also described in the purpose.';
 $string['roleoverrideoverview'] = 'The default retention policy can be overridden for specific user roles, allowing you to specify a longer, or a shorter, retention policy. A user is only expired when all of their roles have expired.';
-
-// Deprecated since Moodle 3.6.
-$string['statuspreprocessing'] = 'Pre-processing';
index abaf3e2..cf9126a 100644 (file)
@@ -11,5 +11,5 @@ information provided here is intended especially for developers.
 
 === 3.6 ===
 
-* The legacy log store is in its first stage of deprecation and is due for removal in Moodle 4.0. Please use one of
+* The legacy log store is in its first stage of deprecation and is due for removal in Moodle 3.10. Please use one of
   the other log stores such as "standard" and "database".
index f765213..bb57d22 100644 (file)
@@ -12,8 +12,8 @@ Feature: See the competencies for an activity on the course competencies page.
       | Test-Comp1 | ID-FW1 |
       | Test-Comp2 | ID-FW1 |
     Given the following "courses" exist:
-      | shortname | fullname   |
-      | C1        | Course 1 |
+      | shortname | fullname   | enablecompletion |
+      | C1        | Course 1   | 1                |
     And the following "users" exist:
       | username | firstname | lastname | email |
       | student1 | Student | 1 | student1@example.com |
@@ -21,9 +21,9 @@ Feature: See the competencies for an activity on the course competencies page.
       | user | course | role |
       | student1 | C1 | student |
     And the following "activities" exist:
-      | activity | name       | intro      | course | idnumber |
-      | page     | PageName1  | PageDesc1  | C1     | PAGE1    |
-      | page     | PageName2  | PageDesc2  | C1     | PAGE2    |
+      | activity | name       | intro      | course | idnumber | completion | completionview |
+      | page     | PageName1  | PageDesc1  | C1     | PAGE1    | 1          | 1              |
+      | page     | PageName2  | PageDesc2  | C1     | PAGE2    | 1          | 1              |
     And I log in as "admin"
     And I am on site homepage
     And I follow "Course 1"
@@ -61,3 +61,15 @@ Feature: See the competencies for an activity on the course competencies page.
     And I should not see "Test-Comp1"
     And I should not see "Test-Comp2"
     And I should see "No competencies have been linked to this activity or resource."
+
+  @javascript
+  Scenario: None course competencies page.
+    When I log in as "student1"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I follow "PageName1"
+    Then I should see "Test page content"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I follow "PageName1"
+    Then I should see "Test page content"
index 5fbfd20..b228638 100644 (file)
@@ -52,7 +52,7 @@ class external extends external_api {
     public static function verify_webfinger_parameters() {
         return new external_function_parameters(
             array(
-                'profileurl' => new external_value(PARAM_RAW, 'The profile url that the user has given us', VALUE_REQUIRED),
+                'profileurl' => new external_value(PARAM_NOTAGS, 'The profile url that the user has given us', VALUE_REQUIRED),
                 'course' => new external_value(PARAM_INT, 'The course we are adding to', VALUE_REQUIRED),
                 'section' => new external_value(PARAM_INT, 'The section within the course we are adding to', VALUE_REQUIRED),
             )
index f1a922a..49027fd 100644 (file)
@@ -46,7 +46,7 @@ class profile_manager {
             $user = \core_user::get_user($userid, 'moodlenetprofile');
             try {
                 $userprofile = $user->moodlenetprofile ? $user->moodlenetprofile : '';
-                return (isset($user)) ? new moodlenet_user_profile($userprofile, $userid) : null;
+                return (isset($user)) ? new moodlenet_user_profile(s($userprofile), $userid) : null;
             } catch (\moodle_exception $e) {
                 // If an exception is thrown, means there isn't a valid profile set. No need to log exception.
                 return null;
@@ -59,7 +59,7 @@ class profile_manager {
             if ($field->get_category_name() == self::get_category_name()
                     && $field->inputname == 'profile_field_mnetprofile') {
                 try {
-                    return new moodlenet_user_profile($field->display_data(), $userid);
+                    return new moodlenet_user_profile(s($field->display_data()), $userid);
                 } catch (\moodle_exception $e) {
                     // If an exception is thrown, means there isn't a valid profile set. No need to log exception.
                     return null;
index 0442276..c3f696a 100644 (file)
@@ -105,5 +105,23 @@ function xmldb_tool_moodlenet_upgrade(int $oldversion) {
     // Automatically generated Moodle v3.9.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2021052501) {
+
+        // Find out if there are users with MoodleNet profiles set.
+        $sql = "SELECT u.*
+                  FROM {user} u
+                 WHERE u.moodlenetprofile IS NOT NULL";
+
+        $records = $DB->get_records_sql($sql);
+
+        foreach ($records as $record) {
+            // Force clean user value just incase there is something malicious.
+            $record->moodlenetprofile = clean_text($record->moodlenetprofile, PARAM_NOTAGS);
+            $DB->update_record('user', $record);
+        }
+
+        upgrade_plugin_savepoint(true, 2021052501, 'tool', 'moodlenet');
+    }
+
     return true;
 }
index 7a3d19f..539a58d 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component  = 'tool_moodlenet';
-$plugin->version    = 2021052500;
+$plugin->version    = 2021052501;
 $plugin->requires   = 2021052500;
 $plugin->maturity   = MATURITY_ALPHA;
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 8854abb..9f1a799 100644 (file)
@@ -83,7 +83,7 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
         $mform->setExpanded('defaultheader', true);
 
         $displaylist = core_course_category::make_categories_list('moodle/course:create');
-        $mform->addElement('select', 'defaults[category]', get_string('coursecategory'), $displaylist);
+        $mform->addElement('autocomplete', 'defaults[category]', get_string('coursecategory'), $displaylist);
         $mform->addHelpButton('defaults[category]', 'coursecategory');
 
         $choices = array();
index 4a9f5cf..6741568 100644 (file)
@@ -138,13 +138,28 @@ class manager {
      */
     const CONFIG_SHIPPED_VERSION = 'shipped_version';
 
+    /**
+     * Helper method to initialize admin page, setting appropriate extra URL parameters
+     *
+     * @param string $action
+     */
+    protected function setup_admin_externalpage(string $action): void {
+        admin_externalpage_setup('tool_usertours/tours', '', array_filter([
+            'action' => $action,
+            'id' => optional_param('id', 0, PARAM_INT),
+            'tourid' => optional_param('tourid', 0, PARAM_INT),
+            'direction' => optional_param('direction', 0, PARAM_INT),
+        ]));
+    }
+
     /**
      * This is the entry point for this controller class.
      *
      * @param   string  $action     The action to perform.
      */
     public function execute($action) {
-        admin_externalpage_setup('tool_usertours/tours');
+        $this->setup_admin_externalpage($action);
+
         // Add the main content.
         switch($action) {
             case self::ACTION_NEWTOUR:
index 13da0f4..5c0f74f 100644 (file)
@@ -454,9 +454,7 @@ class auth_plugin_db extends auth_plugin_base {
                 $user->confirmed  = 1;
                 $user->auth       = $this->authtype;
                 $user->mnethostid = $CFG->mnet_localhost_id;
-                if (empty($user->lang)) {
-                    $user->lang = $CFG->lang;
-                }
+
                 if ($collision = $DB->get_record_select('user', "username = :username AND mnethostid = :mnethostid AND auth <> :auth", array('username'=>$user->username, 'mnethostid'=>$CFG->mnet_localhost_id, 'auth'=>$this->authtype), 'id,username,auth')) {
                     $trace->output(get_string('auth_dbinsertuserduplicate', 'auth_db', array('username'=>$user->username, 'auth'=>$collision->auth)), 1);
                     continue;
index f03372f..aae5522 100644 (file)
@@ -949,9 +949,7 @@ class auth_plugin_ldap extends auth_plugin_base {
                 //
                 // The cast to int is a workaround for MDL-53959.
                 $user->suspended = (int)$this->is_user_suspended($user);
-                if (empty($user->lang)) {
-                    $user->lang = $CFG->lang;
-                }
+
                 if (empty($user->calendartype)) {
                     $user->calendartype = $CFG->calendartype;
                 }
index 2744dcd..27cf5d7 100644 (file)
@@ -278,13 +278,7 @@ class auth_plugin_mnet extends auth_plugin_base {
         if (isset($remoteuser->lang)) {
             $remoteuser->lang = clean_param(str_replace('_utf8', '', $remoteuser->lang), PARAM_LANG);
         }
-        if (empty($remoteuser->lang)) {
-            if (!empty($CFG->lang)) {
-                $remoteuser->lang = $CFG->lang;
-            } else {
-                $remoteuser->lang = 'en';
-            }
-        }
+
         $firsttime = false;
 
         // get the local record for the remote user
index 5ebb83a..b8af8a3 100644 (file)
@@ -61,10 +61,18 @@ Feature: Authentication
     # The following tests are all provided to ensure that the accessibility tests themselves are tested.
     # In normal tests only one of the following is required.
     Then the page should meet accessibility standards
-    And the page should meet "wcag131, wcag412" accessibility standards
-    And the page should meet accessibility standards with "wcag131, wcag412" extra tests
+    And the page should meet "wcag131, wcag141, wcag412" accessibility standards
+    And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests
 
     And I follow "Log in"
     And the page should meet accessibility standards
-    And the page should meet "wcag131, wcag412" accessibility standards
-    And the page should meet accessibility standards with "wcag131, wcag412" extra tests
+    And the page should meet "wcag131, wcag141, wcag412" accessibility standards
+    And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests
+
+  @javascript @accessibility
+  Scenario: The login page must have sufficient colour contrast
+    Given the following config values are set as admin:
+      | custommenuitems | -This is a custom item\|/customurl/ |
+    When I am on site homepage
+    Then the page should meet "wcag143" accessibility standards
+    And the page should meet accessibility standards with "wcag143" extra tests
index c1372cc..8c84801 100644 (file)
@@ -1021,6 +1021,8 @@ class backup_gradebook_structure_step extends backup_structure_step {
             'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime',
             'needsupdate', 'timecreated', 'timemodified'));
 
+        $this->add_plugin_structure('local', $grade_item, true);
+
         $grade_grades = new backup_nested_element('grade_grades');
         $grade_grade = new backup_nested_element('grade_grade', array('id'), array(
             'userid', 'rawgrade', 'rawgrademax', 'rawgrademin',
index baf3b04..7e9e325 100644 (file)
@@ -146,7 +146,11 @@ class restore_gradebook_structure_step extends restore_structure_step {
 
         $paths[] = new restore_path_element('attributes', '/gradebook/attributes');
         $paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category');
-        $paths[] = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
+
+        $gradeitem = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
+        $paths[] = $gradeitem;
+        $this->add_plugin_structure('local', $gradeitem);
+
         if ($userinfo) {
             $paths[] = new restore_path_element('grade_grade', '/gradebook/grade_items/grade_item/grade_grades/grade_grade');
         }
index 88a9c88..3d55558 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /backup/*,
 information provided here is intended especially for developers.
 
+=== 4.0 ===
+ * Local plugins can now hook into a backup and restore process of grade items by
+   using define_grade_item_plugin_structure method (See MDL-69418).
+
 === 3.1 ===
 
 * New close() method added to loggers so they can close any open resource. Previously
index a5ca451..e8f1382 100644 (file)
@@ -1165,7 +1165,7 @@ abstract class restore_dbops {
 
             // if user lang doesn't exist here, use site default
             if (!array_key_exists($user->lang, $languages)) {
-                $user->lang = $CFG->lang;
+                $user->lang = get_newuser_language();
             }
 
             // if user theme isn't available on target site or they are disabled, reset theme
index 3e2d269..496cf02 100644 (file)
@@ -111,8 +111,8 @@ class copy  {
      *  Take the validated form data and extract the required information for copy operations.
      *
      * @param \stdClass $formdata Data from the validated course copy form.
-     * @throws \moodle_exception
      * @return \stdClass $copydata Data required for course copy operations.
+     * @throws \moodle_exception If one of the required copy fields is missing
      */
     private final function get_copy_data(\stdClass $formdata): \stdClass {
         $copydata = new \stdClass();
@@ -121,7 +121,7 @@ class copy  {
             if (isset($formdata->{$field})) {
                 $copydata->{$field} = $formdata->{$field};
             } else {
-                throw new \moodle_exception('copy_class_field_not_found');
+                throw new \moodle_exception('copyfieldnotfound', 'backup', '', null, $field);
             }
         }
 
index ff7a55d..6856101 100644 (file)
@@ -106,7 +106,7 @@ class copy_form extends \moodleform {
             // Always keep current category.
             $displaylist[$course->category] = \core_course_category::get($course->category, MUST_EXIST, true)->get_formatted_name();
         }
-        $mform->addElement('select', 'category', get_string('coursecategory'), $displaylist);
+        $mform->addElement('autocomplete', 'category', get_string('coursecategory'), $displaylist);
         $mform->addHelpButton('category', 'coursecategory');
 
         // Course visibility.
@@ -162,15 +162,17 @@ class copy_form extends \moodleform {
         }
 
         // Keep source course user data.
+        $mform->addElement('select', 'userdata', get_string('userdata', 'backup'),
+            [0 => get_string('no'), 1 => get_string('yes')]);
+        $mform->setDefault('userdata', 0);
+        $mform->addHelpButton('userdata', 'userdata', 'backup');
+
         $requiredcapabilities = array(
             'moodle/restore:createuser', 'moodle/backup:userinfo', 'moodle/restore:userinfo'
         );
-        if (has_all_capabilities($requiredcapabilities, $coursecontext)) {
-            $dataarray = array();
-            $dataarray[] = $mform->createElement('advcheckbox', 'userdata',
-                get_string('enable'), '', array('group' => 1), array(0, 1));
-            $mform->addGroup($dataarray, 'dataarray', get_string('userdata', 'backup'), ' ', false);
-            $mform->addHelpButton('dataarray', 'userdata', 'backup');
+        if (!has_all_capabilities($requiredcapabilities, $coursecontext)) {
+            $mform->hardFreeze('userdata');
+            $mform->setConstants('userdata', 0);
         }
 
         // Keep manual enrolments.
index ff1e613..e7f65ca 100644 (file)
@@ -49,7 +49,7 @@ class award_criteria_manual extends award_criteria {
         $rec = $DB->get_record('role', array('id' => $rid));
 
         if ($rec) {
-            return role_get_name($rec, $PAGE->context, ROLENAME_ALIAS);
+            return role_get_name($rec, $PAGE->context, ROLENAME_BOTH);
         } else {
             return null;
         }
index 9763156..491b555 100644 (file)
@@ -86,24 +86,6 @@ $string['hiddencourses'] = 'Removed from view';
 $string['show'] = 'Restore to view';
 $string['privacy:request:preference:set'] = 'The value of the setting \'{$a->name}\' was \'{$a->value}\'';
 
-// Deprecated since Moodle 3.6.
-$string['defaulttab'] = 'Default tab';
-$string['defaulttab_desc'] = 'The tab that will be displayed when a user first views their course overview. When returning to their course overview, the user\'s active tab is remembered.';
-$string['morecourses'] = 'More courses';
-$string['nocoursesinprogress'] = 'No in progress courses';
-$string['nocoursesfuture'] = 'No future courses';
-$string['nocoursespast'] = 'No past courses';
-$string['noevents'] = 'No upcoming activities due';
-$string['next30days'] = 'Next 30 days';
-$string['next7days'] = 'Next 7 days';
-$string['recentlyoverdue'] = 'Recently overdue';
-$string['sortbycourses'] = 'Sort by courses';
-$string['sortbydates'] = 'Sort by dates';
-$string['timeline'] = 'Timeline';
-$string['viewcoursename'] = 'View course {$a}';
-$string['privacy:metadata:overviewlasttab'] = 'This stores the last tab selected by the user on the overview block.';
-$string['viewcourse'] = 'View course';
-
 // Deprecated since Moodle 3.7.
 $string['complete'] = 'complete';
 $string['nocourses'] = 'No courses';
index fb6f86c..c70ee8b 100644 (file)
@@ -1,17 +1,2 @@
-defaulttab,block_myoverview
-defaulttab_desc,block_myoverview
-morecourses,block_myoverview
-nocoursesinprogress,block_myoverview
-nocoursesfuture,block_myoverview
-nocoursespast,block_myoverview
-noevents,block_myoverview
-next30days,block_myoverview
-next7days,block_myoverview
-recentlyoverdue,block_myoverview
-sortbycourses,block_myoverview
-sortbydates,block_myoverview
-timeline,block_myoverview
-viewcoursename,block_myoverview
-privacy:metadata:overviewlasttab,block_myoverview
 nocourses,block_myoverview
-complete,block_myoverview
\ No newline at end of file
+complete,block_myoverview
index 7563077..d92a97e 100644 (file)
@@ -1,7 +1,7 @@
 This files describes API changes in /cache/stores/* - cache store plugins.
 Information provided here is intended especially for developers.
 
-=== 4.0 ===
+=== 3.10 ===
 * The function supports_recursion() from the lock_factory interface has been deprecated including the related implementations.
 * The function extend_lock() from the lock_factory interface has been deprecated without replacement including the related
   implementations.
index 8475e29..6488acb 100644 (file)
@@ -94,7 +94,7 @@ trait eventtype {
                 $categoryoptions[$id] = $category;
             }
 
-            $mform->addElement('select', 'categoryid', get_string('category'), $categoryoptions);
+            $mform->addElement('autocomplete', 'categoryid', get_string('category'), $categoryoptions);
             $mform->hideIf('categoryid', 'eventtype', 'noteq', 'category');
         }
 
index 5f1ffaf..9fc33df 100644 (file)
@@ -199,10 +199,10 @@ Feature: Perform basic calendar functionality
     And I am viewing site calendar
     And I click on "New event" "button"
     And I set the field "Type of event" to "Course"
-    When I open the autocomplete suggestions list
-    Then I should see "Course 1" in the ".form-autocomplete-suggestions" "css_element"
-    And I should not see "Course 2" in the ".form-autocomplete-suggestions" "css_element"
-    And I should not see "Course 3" in the ".form-autocomplete-suggestions" "css_element"
+    When I expand the "Course" autocomplete
+    Then "Course 1" "autocomplete_suggestions" should exist
+    And "Course 2" "autocomplete_suggestions" should not exist
+    And "Course 3" "autocomplete_suggestions" should not exist
     And I click on "Close" "button"
     And I am on site homepage
     And I navigate to "Appearance > Calendar" in site administration
@@ -211,10 +211,10 @@ Feature: Perform basic calendar functionality
     And I am viewing site calendar
     And I click on "New event" "button"
     And I set the field "Type of event" to "Course"
-    When I open the autocomplete suggestions list
-    Then I should see "Course 1" in the ".form-autocomplete-suggestions" "css_element"
-    And I should see "Course 2" in the ".form-autocomplete-suggestions" "css_element"
-    And I should see "Course 3" in the ".form-autocomplete-suggestions" "css_element"
+    When I expand the "Course" autocomplete
+    Then "Course 1" "autocomplete_suggestions" should exist
+    And "Course 2" "autocomplete_suggestions" should exist
+    And "Course 3" "autocomplete_suggestions" should exist
 
   @javascript
   Scenario: Students can only see user event type by default.
@@ -238,3 +238,12 @@ Feature: Perform basic calendar functionality
     And I follow "This month"
     When I click on "New event" "button"
     Then I should see "User" in the "div#fitem_id_staticeventtype" "css_element"
+
+  @javascript @accessibility
+  Scenario: The calendar page must be accessible
+    Given I log in as "student1"
+    And I am on "Course 1" course homepage
+    When I follow "This month"
+    Then the page should meet accessibility standards
+    And the page should meet "wcag131, wcag143, wcag412" accessibility standards
+    And the page should meet accessibility standards with "wcag131, wcag143, wcag412" extra tests
index 5e9f641..35fda4d 100644 (file)
@@ -1,7 +1,7 @@
 This files describes API changes in /calendar/* ,
 information provided here is intended especially for developers.
 
-=== 4.0 ===
+=== 3.10 ===
 * The core_calendar\local\event\value_objects\times_interface class now has new method get_usermidnight_time() which
   returns the user midnight time for a given event.
 
index 7dbe18b..77599fd 100644 (file)
@@ -43,7 +43,7 @@ class cohort_edit_form extends moodleform {
         $mform->setType('name', PARAM_TEXT);
 
         $options = $this->get_category_options($cohort->contextid);
-        $mform->addElement('select', 'contextid', get_string('context', 'role'), $options);
+        $mform->addElement('autocomplete', 'contextid', get_string('context', 'role'), $options);
 
         $mform->addElement('text', 'idnumber', get_string('idnumber', 'cohort'), 'maxlength="254" size="50"');
         $mform->setType('idnumber', PARAM_RAW); // Idnumbers are plain text, must not be changed.
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 1a5955d..b93d41f 100644 (file)
@@ -21,6 +21,7 @@ Feature: Add cohorts of users
       | Description | Test cohort description |
     And I press "Save changes"
 
+  @javascript
   Scenario: Add a cohort
     When I follow "Cohorts"
     Then I should see "Test cohort name"
@@ -28,6 +29,7 @@ Feature: Add cohorts of users
     And I should see "Test cohort description"
     And I should see "Created manually"
 
+  @javascript
   Scenario: Add users to a cohort selecting them from the system users list
     When I add "First User (first@example.com)" user to "333" cohort members
     And I add "Second User (second@example.com)" user to "333" cohort members
@@ -37,6 +39,7 @@ Feature: Add cohorts of users
     And the "Current users" select box should contain "Second User (second@example.com)"
     And the "Current users" select box should not contain "Forth User (forth@example.com)"
 
+  @javascript
   Scenario: Add users to a cohort using a bulk user action
     When I follow "Accounts"
     And I follow "Bulk user actions"
index 41a8b41..1eb3d8e 100644 (file)
@@ -337,7 +337,7 @@ class core_comment_external extends external_api {
             $args->area      = $commentrecord->commentarea;
             $manager = new comment($args);
 
-            if ($commentrecord->userid != $USER->id && !$manager->can_delete($commentrecord->id)) {
+            if (!$manager->can_delete($commentrecord)) {
                 throw new comment_exception('nopermissiontodelentry');
             }
 
index c1fe37c..50caa94 100644 (file)
@@ -91,8 +91,8 @@ switch ($action) {
         }
         break;
     case 'delete':
-        $comment_record = $DB->get_record('comments', array('id'=>$commentid));
-        if ($manager->can_delete($commentid) || $comment_record->userid == $USER->id) {
+        $comment = $DB->get_record('comments', ['id' => $commentid]);
+        if ($manager->can_delete($comment)) {
             if ($manager->delete($commentid)) {
                 $result = array(
                     'client_id' => $client_id,
index 58f8151..65d81e3 100644 (file)
@@ -589,8 +589,7 @@ class comment {
             $c->avatar = $OUTPUT->user_picture($u, array('size'=>18));
             $c->userid = $u->id;
 
-            $candelete = $this->can_delete($c->id);
-            if (($USER->id == $u->id) || !empty($candelete)) {
+            if ($this->can_delete($c)) {
                 $c->delete = true;
             }
             $comments[] = $c;
@@ -800,16 +799,22 @@ class comment {
     /**
      * Delete a comment
      *
-     * @param  int $commentid
+     * @param  int|stdClass $comment The id of a comment, or a comment record.
      * @return bool
      */
-    public function delete($commentid) {
-        global $DB, $USER;
-        $candelete = has_capability('moodle/comment:delete', $this->context);
-        if (!$comment = $DB->get_record('comments', array('id'=>$commentid))) {
+    public function delete($comment) {
+        global $DB;
+        if (is_object($comment)) {
+            $commentid = $comment->id;
+        } else {
+            $commentid = $comment;
+            $comment = $DB->get_record('comments', ['id' => $commentid]);
+        }
+
+        if (!$comment) {
             throw new comment_exception('dbupdatefailed');
         }
-        if (!($USER->id == $comment->userid || !empty($candelete))) {
+        if (!$this->can_delete($comment)) {
             throw new comment_exception('nopermissiontocomment');
         }
         $DB->delete_records('comments', array('id'=>$commentid));
@@ -976,13 +981,35 @@ class comment {
     }
 
     /**
-     * Returns true if the user can delete this comment
-     * @param int $commentid
+     * Returns true if the user can delete this comment.
+     *
+     * The user can delete comments if it is one they posted and they can still make posts,
+     * or they have the capability to delete comments.
+     *
+     * A database call is avoided if a comment record is passed.
+     *
+     * @param int|stdClass $comment The id of a comment, or a comment record.
      * @return bool
      */
-    public function can_delete($commentid) {
+    public function can_delete($comment) {
+        global $USER, $DB;
+        if (is_object($comment)) {
+            $commentid = $comment->id;
+        } else {
+            $commentid = $comment;
+        }
+
         $this->validate(array('commentid'=>$commentid));
-        return has_capability('moodle/comment:delete', $this->context);
+
+        if (!is_object($comment)) {
+            // Get the comment record from the database.
+            $comment = $DB->get_record('comments', array('id' => $commentid), 'id, userid', MUST_EXIST);
+        }
+
+        $hascapability = has_capability('moodle/comment:delete', $this->context);
+        $owncomment = $USER->id == $comment->userid;
+
+        return ($hascapability || ($owncomment && $this->can_post()));
     }
 
     /**
diff --git a/comment/tests/context_freeze_test.php b/comment/tests/context_freeze_test.php
new file mode 100644 (file)
index 0000000..36f1d60
--- /dev/null
@@ -0,0 +1,166 @@
+<?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/>.
+
+/**
+ * Tests for comments when the context is frozen.
+ *
+ * @package    core_comment
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests for comments when the context is frozen.
+ *
+ * @package    core_comment
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class comment_context_freeze_testcase extends advanced_testcase {
+    /**
+     * Creates a comment by a student.
+     *
+     * Returns:
+     * - The comment object
+     * - The sudent that wrote the comment
+     * - The arguments used to create the comment
+     *
+     * @param stdClass $course Moodle course from the datagenerator
+     * @return array
+     */
+    protected function create_student_comment_and_freeze_course($course): array {
+        set_config('contextlocking', 1);
+
+        $context = context_course::instance($course->id);
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        $args = new stdClass;
+        $args->context = $context;
+        $args->course = $course;
+        $args->area = 'page_comments';
+        $args->itemid = 0;
+        $args->component = 'block_comments';
+        $args->linktext = get_string('showcomments');
+        $args->notoggle = true;
+        $args->autostart = true;
+        $args->displaycancel = false;
+
+        // Create a comment by the student.
+        $this->setUser($student);
+        $comment = new comment($args);
+        $newcomment = $comment->add('New comment');
+
+        // Freeze the context.
+        $this->setAdminUser();
+        $context->set_locked(true);
+
+        return [$newcomment, $student, $args];
+    }
+
+    /**
+     * Test that a student cannot delete their own comments in frozen contexts via the external service.
+     */
+    public function test_delete_student_external() {
+        global $CFG;
+        require_once($CFG->dirroot . '/comment/lib.php');
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        list($newcomment, $student, $args) = $this->create_student_comment_and_freeze_course($course);
+
+        // Check that a student cannot delete their own comment.
+        $this->setUser($student);
+        $studentcomment = new comment($args);
+        $this->assertFalse($studentcomment->can_delete($newcomment->id));
+        $this->assertFalse($studentcomment->can_post());
+        $this->expectException(comment_exception::class);
+        $this->expectExceptionMessage(get_string('nopermissiontodelentry', 'error'));
+        core_comment_external::delete_comments([$newcomment->id]);
+    }
+
+    /**
+     * Test that a student cannot delete their own comments in frozen contexts.
+     */
+    public function test_delete_student() {
+        global $CFG;
+        require_once($CFG->dirroot . '/comment/lib.php');
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        list($newcomment, $student, $args) = $this->create_student_comment_and_freeze_course($course);
+
+        // Check that a student cannot delete their own comment.
+        $this->setUser($student);
+        $studentcomment = new comment($args);
+        $this->assertFalse($studentcomment->can_delete($newcomment->id));
+        $this->assertFalse($studentcomment->can_post());
+        $this->expectException(comment_exception::class);
+        $this->expectExceptionMessage(get_string('nopermissiontocomment', 'error'));
+        $studentcomment->delete($newcomment->id);
+    }
+
+    /**
+     * Test that an admin cannot delete comments in frozen contexts via the external service.
+     */
+    public function test_delete_admin_external() {
+        global $CFG;
+        require_once($CFG->dirroot . '/comment/lib.php');
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        list($newcomment, $student, $args) = $this->create_student_comment_and_freeze_course($course);
+
+        // Check that the admin user cannot delete the comment.
+        $admincomment = new comment($args);
+        $this->assertFalse($admincomment->can_delete($newcomment->id));
+        $this->assertFalse($admincomment->can_post());
+        $this->expectException(comment_exception::class);
+        $this->expectExceptionMessage(get_string('nopermissiontodelentry', 'error'));
+        core_comment_external::delete_comments([$newcomment->id]);
+    }
+
+    /**
+     * Test that an admin cannot delete comments in frozen contexts.
+     */
+    public function test_delete_admin() {
+        global $CFG;
+        require_once($CFG->dirroot . '/comment/lib.php');
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        list($newcomment, $student, $args) = $this->create_student_comment_and_freeze_course($course);
+
+        // Check that the admin user cannot delete the comment.
+        $admincomment = new comment($args);
+        $this->assertFalse($admincomment->can_delete($newcomment->id));
+        $this->assertFalse($admincomment->can_post());
+        $this->expectException(comment_exception::class);
+        $this->expectExceptionMessage(get_string('nopermissiontocomment', 'error'));
+        $admincomment->delete($newcomment->id);
+    }
+}
index ba22662..f5df09e 100644 (file)
@@ -4660,6 +4660,9 @@ class api {
                 $recommend = false;
                 $strdesc = 'evidence_coursemodulecompleted';
 
+                if ($outcome == course_module_competency::OUTCOME_NONE) {
+                    continue;
+                }
                 if ($outcome == course_module_competency::OUTCOME_EVIDENCE) {
                     $action = evidence::ACTION_LOG;
 
@@ -4720,6 +4723,9 @@ class api {
             $recommend = false;
             $strdesc = 'evidence_coursecompleted';
 
+            if ($outcome == course_module_competency::OUTCOME_NONE) {
+                continue;
+            }
             if ($outcome == course_competency::OUTCOME_EVIDENCE) {
                 $action = evidence::ACTION_LOG;
 
index 37e3385..227294b 100644 (file)
@@ -28,6 +28,7 @@ use core_text;
 use stored_file;
 use stdClass;
 use coding_exception;
+use context;
 use moodle_url;
 use core\event\contentbank_content_updated;
 
@@ -85,6 +86,17 @@ abstract class content {
         return $this->content->contenttype;
     }
 
+    /**
+     * Return the contenttype instance of this content.
+     *
+     * @return contenttype The content type instance
+     */
+    public function get_content_type_instance(): contenttype {
+        $context = context::instance_by_id($this->content->contextid);
+        $contenttypeclass = "\\{$this->content->contenttype}\\contenttype";
+        return new $contenttypeclass($context);
+    }
+
     /**
      * Returns $this->content->timemodified.
      *
index 307b949..1e5c934 100644 (file)
@@ -334,4 +334,18 @@ class contentbank {
 
         return $contenttypes;
     }
+
+    /**
+     * Return a content class form a content id.
+     *
+     * @throws coding_exception if the ID is not valid or some class does no exists
+     * @param int $id the content id
+     * @return content the content class instance
+     */
+    public function get_content_from_id(int $id): content {
+        global $DB;
+        $record = $DB->get_record('contentbank_content', ['id' => $id], '*', MUST_EXIST);
+        $contentclass = "\\$record->contenttype\\content";
+        return new $contentclass($record);
+    }
 }
index 699cee0..8f9fec3 100644 (file)
@@ -40,12 +40,18 @@ use moodle_url;
  */
 abstract class contenttype {
 
-    /** Plugin implements uploading feature */
+    /** @var string Constant representing whether the plugin implements uploading feature */
     const CAN_UPLOAD = 'upload';
 
-    /** Plugin implements edition feature */
+    /** @var string Constant representing whether the plugin implements edition feature */
     const CAN_EDIT = 'edit';
 
+    /**
+     * @var string Constant representing whether the plugin implements download feature
+     * @since  Moodle 3.10
+     */
+    const CAN_DOWNLOAD = 'download';
+
     /** @var \context This contenttype's context. **/
     protected $context = null;
 
@@ -115,6 +121,21 @@ abstract class contenttype {
         return $content;
     }
 
+    /**
+     * Replace a content using an uploaded file.
+     *
+     * @throws file_exception If file operations fail
+     * @throws dml_exception if the content creation fails
+     * @param stored_file $file the uploaded file
+     * @param content $content the original content record
+     * @return content Object with the updated content bank information.
+     */
+    public function replace_content(stored_file $file, content $content): content {
+        $content->import_file($file);
+        $content->update_content();
+        return $content;
+    }
+
     /**
      * Delete this content from the content_bank.
      * This method can be overwritten by the plugins if they need to delete specific information.
@@ -220,6 +241,31 @@ abstract class contenttype {
         return '';
     }
 
+    /**
+     * Returns the URL to download the content.
+     *
+     * @since  Moodle 3.10
+     * @param  content $content The content to be downloaded.
+     * @return string           URL with the content to download.
+     */
+    public function get_download_url(content $content): string {
+        $downloadurl = '';
+        $file = $content->get_file();
+        if (!empty($file)) {
+            $url = \moodle_url::make_pluginfile_url(
+                $file->get_contextid(),
+                $file->get_component(),
+                $file->get_filearea(),
+                $file->get_itemid(),
+                $file->get_filepath(),
+                $file->get_filename()
+            );
+            $downloadurl = $url->out(false);
+        }
+
+        return $downloadurl;
+    }
+
     /**
      * Returns the HTML code to render the icon for content bank contents.
      *
@@ -392,6 +438,38 @@ abstract class contenttype {
         return true;
     }
 
+    /**
+     * Returns whether or not the user has permission to download the content.
+     *
+     * @since  Moodle 3.10
+     * @param  content $content The content to be downloaded.
+     * @return bool    True if the user can download the content. False otherwise.
+     */
+    final public function can_download(content $content): bool {
+        if (!$this->is_feature_supported(self::CAN_DOWNLOAD)) {
+            return false;
+        }
+
+        if (!$this->can_access()) {
+            return false;
+        }
+
+        $hascapability = has_capability('moodle/contentbank:downloadcontent', $this->context);
+        return $hascapability && $this->is_download_allowed($content);
+    }
+
+    /**
+     * Returns plugin allows downloading.
+     *
+     * @since  Moodle 3.10
+     * @param  content $content The content to be downloaed.
+     * @return bool    True if plugin allows downloading. False otherwise.
+     */
+    protected function is_download_allowed(content $content): bool {
+        // Plugins can overwrite this function to add any check they need.
+        return true;
+    }
+
     /**
      * Returns the plugin supports the feature.
      *
index 1c1f2ea..e16a30e 100644 (file)
@@ -110,7 +110,7 @@ class contenttype extends \core_contentbank\contenttype {
      * @return array
      */
     protected function get_implemented_features(): array {
-        return [self::CAN_UPLOAD, self::CAN_EDIT];
+        return [self::CAN_UPLOAD, self::CAN_EDIT, self::CAN_DOWNLOAD];
     }
 
     /**
diff --git a/contentbank/contenttype/h5p/tests/behat/admin_replace_content.feature b/contentbank/contenttype/h5p/tests/behat/admin_replace_content.feature
new file mode 100644 (file)
index 0000000..bc5202c
--- /dev/null
@@ -0,0 +1,30 @@
+@core @core_contentbank @contenttype_h5p @_file_upload @_switch_iframe @javascript
+Feature: Replace H5P file from an existing content
+  In order to replace an H5P content from the content bank
+  As an admin
+  I need to be able to replace the content with a new .h5p file
+
+  Background:
+    Given the following "contentbank content" exist:
+      | contextlevel | reference | contenttype     | user  | contentname       | filepath                              |
+      | System       |           | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
+    And I log in as "admin"
+    And I press "Customise this page"
+    And I add the "Navigation" block if not present
+    And I expand "Site pages" node
+    And I click on "Content bank" "link"
+
+  Scenario: Admins can replace the original .h5p file with a new one
+    Given I click on "filltheblanks.h5p" "link"
+    And I switch to "h5p-player" class iframe
+    And I switch to "h5p-iframe" class iframe
+    And I should see "Of which countries"
+    And I switch to the main frame
+    When I open the action menu in "region-main-settings-menu" "region"
+    And I choose "Replace with file" in the open action menu
+    And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Upload content" filemanager
+    And I click on "Save changes" "button"
+    Then I switch to "h5p-player" class iframe
+    And I switch to "h5p-iframe" class iframe
+    And I should see "Lorum ipsum"
+    And I switch to the main frame
diff --git a/contentbank/contenttype/h5p/tests/behat/teacher_replace_content.feature b/contentbank/contenttype/h5p/tests/behat/teacher_replace_content.feature
new file mode 100644 (file)
index 0000000..5a53f5c
--- /dev/null
@@ -0,0 +1,67 @@
+@core @core_contentbank @contenttype_h5p @_file_upload @_switch_iframe @javascript
+Feature: Replace H5P file from an existing content requires special capabilities
+  In order replace an H5P content from the content bank
+  As a teacher
+  I need to be able to replace the content only if certain capabilities are allowed
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+    And the following "categories" exist:
+      | name  | category | idnumber |
+      | Cat 1 | 0        | CAT1     |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | CAT1     |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+    And the following "contentbank content" exist:
+      | contextlevel | reference | contenttype     | user     | contentname       | filepath                              |
+      | Course       | C1        | contenttype_h5p | admin    | admincontent      | /h5p/tests/fixtures/ipsums.h5p        |
+      | Course       | C1        | contenttype_h5p | teacher1 | teachercontent    | /h5p/tests/fixtures/filltheblanks.h5p |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Navigation" block if not present
+    And I expand "Site pages" node
+    And I click on "Content bank" "link"
+    # Force the content deploy
+    And I click on "admincontent" "link"
+    And I click on "Content bank" "link"
+
+  Scenario: Teacher can replace its own H5P files
+    Given I click on "teachercontent" "link"
+    When I open the action menu in "region-main-settings-menu" "region"
+    And I choose "Replace with file" in the open action menu
+    And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Upload content" filemanager
+    And I click on "Save changes" "button"
+    Then I switch to "h5p-player" class iframe
+    And I switch to "h5p-iframe" class iframe
+    And I should see "Lorum ipsum"
+    And I switch to the main frame
+
+  Scenario: Teacher cannot replace another user's H5P files
+    When I click on "admincontent" "link"
+    Then "region-main-settings-menu" "region" should not exist
+
+  Scenario: Teacher cannot replace a content without having upload capability
+    Given the following "permission overrides" exist:
+      | capability                | permission | role           | contextlevel | reference |
+      | moodle/contentbank:upload | Prevent    | editingteacher | Course       | C1        |
+    When I click on "teachercontent" "link"
+    Then "region-main-settings-menu" "region" should not exist
+
+  Scenario: Teacher cannot replace a content without having the H5P upload capability
+    Given the following "permission overrides" exist:
+      | capability             | permission | role           | contextlevel | reference |
+      | contenttype/h5p:upload | Prevent    | editingteacher | Course       | C1        |
+    When I click on "teachercontent" "link"
+    Then "region-main-settings-menu" "region" should not exist
+
+  Scenario: Teacher cannot replace a content without having the manage own content capability
+    Given the following "permission overrides" exist:
+      | capability                          | permission | role           | contextlevel | reference |
+      | moodle/contentbank:manageowncontent | Prevent    | editingteacher | Course       | C1        |
+    When I click on "teachercontent" "link"
+    Then "region-main-settings-menu" "region" should not exist
index c708516..c806134 100644 (file)
@@ -147,4 +147,40 @@ class contenttype_h5p_contenttype_plugin_testcase extends advanced_testcase {
         $this->assertNotEquals($defaulticon, $findicon);
         $this->assertContains('find', $findicon, '', true);
     }
+
+    /**
+     * Tests get_download_url result.
+     *
+     * @covers ::get_download_url
+     */
+    public function test_get_download_url() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $systemcontext = context_system::instance();
+        $this->setAdminUser();
+        $contenttype = new contenttype_h5p\contenttype($systemcontext);
+
+        // Add an H5P fill the blanks file to the content bank.
+        $filename = 'filltheblanks.h5p';
+        $filepath = $CFG->dirroot . '/h5p/tests/fixtures/' . $filename;
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+        $contents = $generator->generate_contentbank_data('contenttype_h5p', 1, 0, $systemcontext, true, $filepath);
+        $filltheblanks = array_shift($contents);
+
+        // Check before deploying the URL is returned OK.
+        $url1 = $contenttype->get_download_url($filltheblanks);
+        $this->assertNotEmpty($url1);
+        $this->assertContains($filename, $url1);
+
+        // Deploy the contents though the player to create the H5P DB entries and know specific content type.
+        $h5pplayer = new \core_h5p\player($filltheblanks->get_file_url(), new \stdClass(), true);
+        $h5pplayer->add_assets_to_page();
+        $h5pplayer->output();
+
+        // Once the H5P has been deployed, the URL is still the same.
+        $url2 = $contenttype->get_download_url($filltheblanks);
+        $this->assertNotEmpty($url2);
+        $this->assertEquals($url1, $url2);
+    }
 }
index d94f61f..6a001f0 100644 (file)
@@ -44,6 +44,11 @@ class contentbank_files_form extends moodleform {
         $mform->addElement('hidden', 'contextid', $this->_customdata['contextid']);
         $mform->setType('contextid', PARAM_INT);
 
+        if (!empty($this->_customdata['id'])) {
+            $mform->addElement('hidden', 'id', $this->_customdata['id']);
+            $mform->setType('id', PARAM_INT);
+        }
+
         $options = $this->_customdata['options'];
         $mform->addElement('filepicker', 'file', get_string('file', 'core_contentbank'), null, $options);
         $mform->addHelpButton('file', 'file', 'core_contentbank');
diff --git a/contentbank/tests/behat/download_content.feature b/contentbank/tests/behat/download_content.feature
new file mode 100644 (file)
index 0000000..9fca557
--- /dev/null
@@ -0,0 +1,61 @@
+@core @core_contentbank @contentbank_h5p @_file_upload @javascript
+Feature: Download H5P content from the content bank
+  In order export H5P content from the content bank
+  As an admin
+  I need to be able to download any H5P content from the content bank
+
+  Background:
+    Given the following "users" exist:
+      | username    | firstname | lastname | email              |
+      | manager     | Max       | Manager  | man@example.com    |
+    And the following "role assigns" exist:
+      | user        | role      | contextlevel  | reference     |
+      | manager     | manager   | System        |               |
+    And the following "contentbank content" exist:
+      | contextlevel | reference | contenttype     | user    | contentname              | filepath                               |
+      | System       |           | contenttype_h5p | admin   | filltheblanksadmin.h5p   | /h5p/tests/fixtures/filltheblanks.h5p  |
+      | System       |           | contenttype_h5p | manager | filltheblanksmanager.h5p | /h5p/tests/fixtures/filltheblanks.h5p  |
+    And I log in as "admin"
+    And I am on site homepage
+    And I turn editing mode on
+    And I add the "Navigation" block if not present
+    And I configure the "Navigation" block
+    And I set the following fields to these values:
+      | Page contexts | Display throughout the entire site |
+    And I press "Save changes"
+
+  Scenario: Admins can download content from the content bank
+    Given I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Content bank" "link" in the "Navigation" "block"
+    And I follow "filltheblanksmanager.h5p"
+    And I open the action menu in "region-main-settings-menu" "region"
+    And I should see "Download"
+    When I choose "Download" in the open action menu
+    Then I should see "filltheblanksmanager.h5p"
+
+  Scenario: Users can download content created by different users
+    Given the following "permission overrides" exist:
+      | capability                            | permission | role    | contextlevel | reference |
+      | moodle/contentbank:manageanycontent   | Prohibit   | manager | System       |           |
+    And I log out
+    And I log in as "manager"
+    When I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Content bank" "link" in the "Navigation" "block"
+    And I should see "filltheblanksadmin.h5p"
+    And I follow "filltheblanksadmin.h5p"
+    And I open the action menu in "region-main-settings-menu" "region"
+    Then I should see "Download"
+    And I should not see "Rename"
+
+  Scenario: Users without the required capability cannot download content
+    Given the following "permission overrides" exist:
+      | capability                            | permission | role    | contextlevel | reference |
+      | moodle/contentbank:downloadcontent    | Prohibit   | manager | System       |           |
+    And I log out
+    And I log in as "manager"
+    When I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Content bank" "link" in the "Navigation" "block"
+    And I should see "filltheblanksmanager.h5p"
+    And I follow "filltheblanksmanager.h5p"
+    And I open the action menu in "region-main-settings-menu" "region"
+    Then I should not see "Download"
index ddfff94..4c7ec2b 100644 (file)
@@ -275,4 +275,27 @@ class core_contenttype_content_testcase extends \advanced_testcase {
         $contentfile = $content->get_file($file);
         $this->assertEquals($importedfile->get_id(), $contentfile->get_id());
     }
+
+    /**
+     * Tests for 'get_content_type_instance'
+     *
+     * @covers ::get_content_type_instance
+     */
+    public function test_get_content_type_instance(): void {
+        global $USER;
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        $context = context_system::instance();
+
+        $type = new contenttype($context);
+        $record = (object)[
+            'name' => 'content name',
+            'usercreated' => $USER->id,
+        ];
+        $content = $type->create_content($record);
+
+        $contenttype = $content->get_content_type_instance();
+
+        $this->assertInstanceOf(get_class($type), $contenttype);
+    }
 }
index cd22e80..3d6a703 100644 (file)
@@ -31,6 +31,7 @@ use advanced_testcase;
 use context_course;
 use context_coursecat;
 use context_system;
+use Exception;
 
 global $CFG;
 require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
@@ -603,4 +604,31 @@ class core_contentbank_testcase extends advanced_testcase {
         $actual = $cb->get_contenttypes_with_capability_feature('test2', null, $enabled);
         $this->assertEquals($contenttypescanfeature, array_values($actual));
     }
+
+    /**
+     * Test the behaviour of get_content_from_id()
+     *
+     * @covers  ::get_content_from_id
+     */
+    public function test_get_content_from_id() {
+
+        $this->resetAfterTest();
+        $cb = new \core_contentbank\contentbank();
+
+        // Create a category and two courses.
+        $systemcontext = context_system::instance();
+
+        // Add some content to the content bank.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+        $contents = $generator->generate_contentbank_data(null, 3, 0, $systemcontext);
+        $content = reset($contents);
+
+        // Get the content instance form id.
+        $newinstance = $cb->get_content_from_id($content->get_id());
+        $this->assertEquals($content->get_id(), $newinstance->get_id());
+
+        // Now produce and exception with an innexistent id.
+        $this->expectException(Exception::class);
+        $cb->get_content_from_id(0);
+    }
 }
index c8ae908..1995357 100644 (file)
@@ -290,6 +290,84 @@ class core_contenttype_contenttype_testcase extends \advanced_testcase {
         $this->assertEquals(1, $DB->count_records('files', ['contenthash' => $dummyfile->get_contenthash()]));
     }
 
+    /**
+     * Tests for behaviour of replace_content() using a dummy file.
+     *
+     * @covers ::replace_content
+     */
+    public function test_replace_content(): void {
+        global $USER;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        $context = context_system::instance();
+
+        // Add some content to the content bank.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+        $contents = $generator->generate_contentbank_data('contenttype_testable', 3, 0, $context);
+        $content = reset($contents);
+
+        $dummy = [
+            'contextid' => context_user::instance($USER->id)->id,
+            'component' => 'user',
+            'filearea' => 'draft',
+            'itemid' => 1,
+            'filepath' => '/',
+            'filename' => 'file.h5p',
+            'userid' => $USER->id,
+        ];
+        $fs = get_file_storage();
+        $dummyfile = $fs->create_file_from_string($dummy, 'Dummy content');
+
+        $contenttype = new contenttype(context_system::instance());
+        $content = $contenttype->replace_content($dummyfile, $content);
+
+        $this->assertEquals('contenttype_testable', $content->get_content_type());
+        $this->assertInstanceOf('\\contenttype_testable\\content', $content);
+
+        $file = $content->get_file();
+        $this->assertEquals($dummyfile->get_userid(), $file->get_userid());
+        $this->assertEquals($dummyfile->get_contenthash(), $file->get_contenthash());
+        $this->assertEquals('contentbank', $file->get_component());
+        $this->assertEquals('public', $file->get_filearea());
+        $this->assertEquals('/', $file->get_filepath());
+    }
+
+    /**
+     * Tests for behaviour of replace_content() using an error file.
+     *
+     * @covers ::replace_content
+     */
+    public function test_replace_content_exception(): void {
+        global $USER;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        $context = context_system::instance();
+
+        // Add some content to the content bank.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+        $contents = $generator->generate_contentbank_data('contenttype_testable', 3, 0, $context);
+        $content = reset($contents);
+
+        $dummy = [
+            'contextid' => context_user::instance($USER->id)->id,
+            'component' => 'user',
+            'filearea' => 'draft',
+            'itemid' => 1,
+            'filepath' => '/',
+            'filename' => 'error.txt',
+            'userid' => $USER->id,
+        ];
+        $fs = get_file_storage();
+        $dummyfile = $fs->create_file_from_string($dummy, 'Dummy content');
+
+        $contenttype = new contenttype(context_system::instance());
+
+        $this->expectException(Exception::class);
+        $content = $contenttype->replace_content($dummyfile, $content);
+    }
+
     /**
      * Test the behaviour of can_delete().
      */
@@ -348,7 +426,7 @@ class core_contenttype_contenttype_testcase extends \advanced_testcase {
     /**
      * Helper function to setup 3 users (manager1, manager2 and user) and 4 contents (3 created by manager1 and 1 by user).
      */
-    protected function contenttype_setup_scenario_data(): void {
+    protected function contenttype_setup_scenario_data(string $contenttype = 'contenttype_testable'): void {
         global $DB;
         $systemcontext = context_system::instance();
 
@@ -358,14 +436,17 @@ class core_contenttype_contenttype_testcase extends \advanced_testcase {
         $this->managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
         $this->getDataGenerator()->role_assign($this->managerroleid, $this->manager1->id);
         $this->getDataGenerator()->role_assign($this->managerroleid, $this->manager2->id);
+        $editingteacherrolerid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']);
         $this->user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->role_assign($editingteacherrolerid, $this->user->id);
 
         // Add some content to the content bank.
         $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
-        $this->contents[$this->manager1->id] = $generator->generate_contentbank_data(null, 3, $this->manager1->id);
-        $this->contents[$this->user->id] = $generator->generate_contentbank_data(null, 1, $this->user->id);
+        $this->contents[$this->manager1->id] = $generator->generate_contentbank_data($contenttype, 3, $this->manager1->id);
+        $this->contents[$this->user->id] = $generator->generate_contentbank_data($contenttype, 1, $this->user->id);
 
-        $this->contenttype = new \contenttype_testable\contenttype($systemcontext);
+        $contenttypeclass = "\\$contenttype\\contenttype";
+        $this->contenttype = new $contenttypeclass($systemcontext);
     }
 
     /**
@@ -510,4 +591,70 @@ class core_contenttype_contenttype_testcase extends \advanced_testcase {
         $this->assertFalse($contenttype->can_manage($contentbyteacher));
         $this->assertFalse($contenttype->can_manage($contentbyadmin));
     }
+
+    /**
+     * Test the behaviour of can_download().
+     *
+     * @covers ::can_download
+     */
+    public function test_can_download() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->contenttype_setup_scenario_data('contenttype_h5p');
+
+        $managercontent = array_shift($this->contents[$this->manager1->id]);
+        $usercontent = array_shift($this->contents[$this->user->id]);
+
+        // Check the content has been created as expected.
+        $records = $DB->count_records('contentbank_content');
+        $this->assertEquals(4, $records);
+
+        // Check user can download content created by anybody.
+        $this->setUser($this->user);
+        $this->assertTrue($this->contenttype->can_download($usercontent));
+        $this->assertTrue($this->contenttype->can_download($managercontent));
+
+        // Check manager can download all the content too.
+        $this->setUser($this->manager1);
+        $this->assertTrue($this->contenttype->can_download($managercontent));
+        $this->assertTrue($this->contenttype->can_download($usercontent));
+
+        // Unassign capability to manager role and check she cannot download content anymore.
+        unassign_capability('moodle/contentbank:downloadcontent', $this->managerroleid);
+        $this->assertFalse($this->contenttype->can_download($managercontent));
+        $this->assertFalse($this->contenttype->can_download($usercontent));
+    }
+
+    /**
+     * Tests get_download_url result.
+     *
+     * @covers ::get_download_url
+     */
+    public function test_get_download_url() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        $systemcontext = context_system::instance();
+
+        // Add some content to the content bank.
+        $filename = 'filltheblanks.h5p';
+        $filepath = $CFG->dirroot . '/h5p/tests/fixtures/' . $filename;
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+        $contents = $generator->generate_contentbank_data('contenttype_testable', 1, 0, $systemcontext, true, $filepath);
+        $content = array_shift($contents);
+
+        // Check the URL is returned OK for a content with file.
+        $contenttype = new contenttype($systemcontext);
+        $url = $contenttype->get_download_url($content);
+        $this->assertNotEmpty($url);
+        $this->assertContains($filename, $url);
+
+        // Check the URL is empty when the content hasn't any file.
+        $record = new stdClass();
+        $content = $contenttype->create_content($record);
+        $url = $contenttype->get_download_url($content);
+        $this->assertEmpty($url);
+    }
 }
index 00cc40c..4410de4 100644 (file)
@@ -34,6 +34,17 @@ $context = context::instance_by_id($contextid, MUST_EXIST);
 
 require_capability('moodle/contentbank:upload', $context);
 
+$cb = new \core_contentbank\contentbank();
+
+$id = optional_param('id', null, PARAM_INT);
+if ($id) {
+    $content = $cb->get_content_from_id($id);
+    $contenttype = $content->get_content_type_instance();
+    if (!$contenttype->can_manage($content) || !$contenttype->can_upload()) {
+        print_error('nopermissions', 'error', $returnurl, get_string('replacecontent', 'contentbank'));
+    }
+}
+
 $title = get_string('contentbank');
 \core_contentbank\helper::get_page_ready($context, $title, true);
 if ($PAGE->course) {
@@ -55,8 +66,12 @@ if (has_capability('moodle/user:ignoreuserquota', $context)) {
     $maxareabytes = FILE_AREA_MAX_BYTES_UNLIMITED;
 }
 
-$cb = new \core_contentbank\contentbank();
-$accepted = $cb->get_supported_extensions_as_string($context);
+if ($id) {
+    $extensions = $contenttype->get_manageable_extensions();
+    $accepted = implode(',', $extensions);
+} else {
+    $accepted = $cb->get_supported_extensions_as_string($context);
+}
 
 $data = new stdClass();
 $options = array(
@@ -68,7 +83,7 @@ $options = array(
 );
 file_prepare_standard_filemanager($data, 'files', $options, $context, 'contentbank', 'public', 0);
 
-$mform = new contentbank_files_form(null, ['contextid' => $contextid, 'data' => $data, 'options' => $options]);
+$mform = new contentbank_files_form(null, ['contextid' => $contextid, 'data' => $data, 'options' => $options, 'id' => $id]);
 
 $error = '';
 
@@ -82,7 +97,11 @@ if ($mform->is_cancelled()) {
     $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $formdata->file, 'itemid, filepath, filename', false);
     if (!empty($files)) {
         $file = reset($files);
-        $content = $cb->create_content_from_file($context, $USER->id, $file);
+        if ($id) {
+            $content = $contenttype->replace_content($file, $content);
+        } else {
+            $content = $cb->create_content_from_file($context, $USER->id, $file);
+        }
         $viewurl = new \moodle_url('/contentbank/view.php', ['id' => $content->get_id(), 'contextid' => $contextid]);
         redirect($viewurl);
     } else {
index 5fd66d1..4c84b1b 100644 (file)
@@ -83,6 +83,24 @@ if ($contenttype->can_manage($content)) {
         false,
         $attributes
     ));
+
+    if ($contenttype->can_upload()) {
+        $actionmenu->add_secondary_action(new action_menu_link(
+            new moodle_url('/contentbank/upload.php', ['contextid' => $context->id, 'id' => $content->get_id()]),
+            new pix_icon('i/upload', get_string('upload')),
+            get_string('replacecontent', 'contentbank'),
+            false
+        ));
+    }
+}
+if ($contenttype->can_download($content)) {
+    // Add the download content item to the menu.
+    $actionmenu->add_secondary_action(new action_menu_link(
+        new moodle_url($contenttype->get_download_url($content)),
+        new pix_icon('t/download', get_string('download')),
+        get_string('download'),
+        false
+    ));
 }
 if ($contenttype->can_delete($content)) {
     // Add the delete content item to the menu.
index ddbd433..906837e 100644 (file)
@@ -111,7 +111,7 @@ class core_course_deletecategory_form extends moodleform {
         }
 
         if ($displaylist) {
-            $mform->addElement('select', 'newparent', get_string('movecategorycontentto'), $displaylist);
+            $mform->addElement('autocomplete', 'newparent', get_string('movecategorycontentto'), $displaylist);
             if (in_array($this->coursecat->parent, $displaylist)) {
                 $mform->setDefault('newparent', $this->coursecat->parent);
             }
index f11d28c..236fbad 100644 (file)
@@ -63,7 +63,7 @@ class core_course_editcategory_form extends moodleform {
             $strsubmit = get_string('createcategory');
         }
 
-        $mform->addElement('select', 'parent', get_string('parentcategory'), $options);
+        $mform->addElement('autocomplete', 'parent', get_string('parentcategory'), $options);
 
         $mform->addElement('text', 'name', get_string('categoryname'), array('size' => '30'));
         $mform->addRule('name', get_string('required'), 'required', null);
index 1b9cfc4..8a289cc 100644 (file)
@@ -77,7 +77,7 @@ class course_edit_form extends moodleform {
         if (empty($course->id)) {
             if (has_capability('moodle/course:create', $categorycontext)) {
                 $displaylist = core_course_category::make_categories_list('moodle/course:create');
-                $mform->addElement('select', 'category', get_string('coursecategory'), $displaylist);
+                $mform->addElement('autocomplete', 'category', get_string('coursecategory'), $displaylist);
                 $mform->addHelpButton('category', 'coursecategory');
                 $mform->setDefault('category', $category->id);
             } else {
@@ -93,7 +93,7 @@ class course_edit_form extends moodleform {
                     $displaylist[$course->category] = core_course_category::get($course->category, MUST_EXIST, true)
                         ->get_formatted_name();
                 }
-                $mform->addElement('select', 'category', get_string('coursecategory'), $displaylist);
+                $mform->addElement('autocomplete', 'category', get_string('coursecategory'), $displaylist);
                 $mform->addHelpButton('category', 'coursecategory');
             } else {
                 //keep current
index 3107584..d917970 100644 (file)
@@ -70,7 +70,7 @@ class course_request_form extends moodleform {
 
         if (empty($CFG->lockrequestcategory)) {
             $displaylist = core_course_category::make_categories_list('moodle/course:request');
-            $mform->addElement('select', 'category', get_string('coursecategory'), $displaylist);
+            $mform->addElement('autocomplete', 'category', get_string('coursecategory'), $displaylist);
             $mform->setDefault('category', $CFG->defaultrequestcategory);
             $mform->addHelpButton('category', 'coursecategory');
         }
index f03ae95..5e5ce0a 100644 (file)
@@ -88,7 +88,7 @@ if ($switchrole > 0 && has_capability('moodle/role:switchroles', $context)) {
         $roles[0] = get_string('switchrolereturn');
         $assumedrole = $USER->access['rsw'][$context->path];
     }
-    $availableroles = get_switchable_roles($context);
+    $availableroles = get_switchable_roles($context, ROLENAME_BOTH);
     if (is_array($availableroles)) {
         foreach ($availableroles as $key => $role) {
             if ($assumedrole == (int)$key) {
index eaeeeec..14ad0b2 100644 (file)
@@ -43,6 +43,7 @@ Feature: Test category management actions
     And I should see "Category 1 (edited)" in the "#category-listing" "css_element"
     And I should see "Category 1 (edited)" in the "#course-listing h3" "css_element"
 
+  @javascript
   Scenario: Test deleting a categories through the management interface.
     Given the following "categories" exist:
       | name | category | idnumber |
@@ -227,9 +228,9 @@ Feature: Test category management actions
     And I should see "Delete category: Cat 1"
     And I should see "Contents of Cat 1"
     And "What to do" "select" should exist
-    And "Move into" "select" should exist
-    And the "Move into" select box should not contain "Cat 2"
-    And the "Move into" select box should contain "Miscellaneous"
+    And I expand the "Move into" autocomplete
+    And "Cat 2" "autocomplete_suggestions" should not exist
+    And "Miscellaneous" "autocomplete_selection" should be visible
     And I set the field "What to do" to "Delete all - cannot be undone"
     And "Move into" "select" should not be visible
     And I press "Cancel"
index c50671f..9a9c6e8 100644 (file)
@@ -76,7 +76,7 @@ Feature: Users can request and approve courses
     And I am on course index
     And I follow "English category"
     And I press "Request a course"
-    And the field "Course category" matches value "English category"
+    And the "Course category" select box should contain "English category"
     And I set the following fields to these values:
       | Course full name  | My new course |
       | Course short name | Mynewcourse   |
index 0883b52..c2655fc 100644 (file)
@@ -36,7 +36,7 @@ Feature: Tagging courses
     And I am on "Course 1" course homepage
     And I navigate to "Edit settings" in current page administration
     And I expand all fieldsets
-    Then I should see "Mathematics" in the ".form-autocomplete-selection" "css_element"
+    Then "Mathematics" "autocomplete_suggestions" should exist
     And I set the following fields to these values:
       | Tags | Algebra |
     And I press "Save and display"
index bb5ab23..1b3c581 100644 (file)
@@ -32,9 +32,15 @@ Feature: Rename roles within a course
     And I navigate to course participants
     And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
     And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I should see "Tutor" in the ".form-autocomplete-suggestions" "css_element"
-    And I should see "Learner" in the ".form-autocomplete-suggestions" "css_element"
-    And I should not see "Student" in the ".form-autocomplete-suggestions" "css_element"
+    And I should see "Tutor (Non-editing teacher)" in the ".form-autocomplete-suggestions" "css_element"
+    And I should see "Learner (Student)" in the ".form-autocomplete-suggestions" "css_element"
+    And I click on "Student 1's role assignments" "link"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Student 1" "table_row"
+    And "Tutor (Non-editing teacher)" "autocomplete_suggestions" should exist
+    And I click on "Cancel" "link"
+    And I press "Enrol users"
+    And the "Assign role" select box should contain "Learner (Student)"
+    And I click on "Cancel" "button" in the "Enrol users" "dialogue"
     And I am on "Course 1" course homepage
     And I navigate to "Edit settings" in current page administration
     And I set the following fields to these values:
index d9a389d..b3aac3d 100644 (file)
@@ -26,9 +26,17 @@ Feature: Rename roles in a course
       | Your word for 'Teacher' | Lecturer |
       | Your word for 'Student' | Learner  |
     And I press "Save and display"
-    And I navigate to "Users > Enrolled users" in current page administration
-    Then I should see "Lecturer" in the "Teacher 1" "table_row"
+    And I navigate to course participants
+    Then I should see "Lecturer (Teacher)" in the "Teacher 1" "table_row"
+    And I should see "Learner (Student)" in the "Student 1" "table_row"
+    And I log out
+    And I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I navigate to course participants
+    And I should see "Lecturer" in the "Teacher 1" "table_row"
     And I should see "Learner" in the "Student 1" "table_row"
+    And I should not see "Lecturer (Teacher)" in the "Teacher 1" "table_row"
+    And I should not see "Learner (Student)" in the "Student 1" "table_row"
 
   Scenario: Ability to rename roles can be prevented
     Given I log in as "admin"
index 16e84d3..4af9f85 100644 (file)
@@ -1,7 +1,7 @@
 This files describes API changes in /course/*,
 information provided here is intended especially for developers.
 
-=== 4.0 ===
+=== 3.10 ===
 
 * The function make_categories_options() has now been deprecated. Please use \core_course_category::make_categories_list() instead.
 
index 682441e..73116b7 100644 (file)
@@ -24,8 +24,6 @@
 
 namespace customfield_select;
 
-use core_customfield\api;
-
 defined('MOODLE_INTERNAL') || die;
 
 /**
@@ -53,8 +51,7 @@ class data_controller extends \core_customfield\data_controller {
     public function get_default_value() {
         $defaultvalue = $this->get_field()->get_configdata_property('defaultvalue');
         if ('' . $defaultvalue !== '') {
-            $options = field_controller::get_options_array($this->get_field());
-            $key = array_search($defaultvalue, $options);
+            $key = array_search($defaultvalue, $this->get_field()->get_options());
             if ($key !== false) {
                 return $key;
             }
@@ -70,7 +67,7 @@ class data_controller extends \core_customfield\data_controller {
     public function instance_form_definition(\MoodleQuickForm $mform) {
         $field = $this->get_field();
         $config = $field->get('configdata');
-        $options = field_controller::get_options_array($field);
+        $options = $field->get_options();
         $formattedoptions = array();
         $context = $this->get_field()->get_handler()->get_configuration_context();
         foreach ($options as $key => $option) {
@@ -120,7 +117,7 @@ class data_controller extends \core_customfield\data_controller {
             return null;
         }
 
-        $options = field_controller::get_options_array($this->get_field());
+        $options = $this->get_field()->get_options();
         if (array_key_exists($value, $options)) {
             return format_string($options[$value], true,
                 ['context' => $this->get_field()->get_handler()->get_configuration_context()]);
index c47f05c..6f198f7 100644 (file)
@@ -60,10 +60,24 @@ class field_controller extends \core_customfield\field_controller {
      *
      * @param \core_customfield\field_controller $field
      * @return array
+     *
+     * @deprecated since Moodle 3.10 - MDL-68569 please use $field->get_options
      */
     public static function get_options_array(\core_customfield\field_controller $field) : array {
-        if ($field->get_configdata_property('options')) {
-            $options = preg_split("/\s*\n\s*/", trim($field->get_configdata_property('options')));
+        debugging('get_options_array() is deprecated, please use $field->get_options() instead', DEBUG_DEVELOPER);
+
+        return $field->get_options();
+    }
+
+    /**
+     * Return configured field options
+     *
+     * @return array
+     */
+    public function get_options(): array {
+        $optionconfig = $this->get_configdata_property('options');
+        if ($optionconfig) {
+            $options = preg_split("/\s*\n\s*/", trim($optionconfig));
         } else {
             $options = array();
         }
@@ -108,7 +122,7 @@ class field_controller extends \core_customfield\field_controller {
      * @return array
      */
     public function course_grouping_format_values($values): array {
-        $options = self::get_options_array($this);
+        $options = $this->get_options();
         $ret = [];
         foreach ($values as $value) {
             if (isset($options[$value])) {
@@ -127,6 +141,6 @@ class field_controller extends \core_customfield\field_controller {
      * @return int
      */
     public function parse_value(string $value) {
-        return (int) array_search($value, self::get_options_array($this));
+        return (int) array_search($value, $this->get_options());
     }
 }
\ No newline at end of file
index aaca643..1f5214b 100644 (file)
@@ -68,7 +68,7 @@ class enrol_cohort_plugin extends enrol_plugin {
             }
             $cohortname = format_string($cohort->name, true, array('context'=>context::instance_by_id($cohort->contextid)));
             if ($role = $DB->get_record('role', array('id'=>$instance->roleid))) {
-                $role = role_get_name($role, context_course::instance($instance->courseid, IGNORE_MISSING));
+                $role = role_get_name($role, context_course::instance($instance->courseid, IGNORE_MISSING), ROLENAME_BOTH);
                 return get_string('pluginname', 'enrol_'.$enrol) . ' (' . $cohortname . ' - ' . $role .')';
             } else {
                 return get_string('pluginname', 'enrol_'.$enrol) . ' (' . $cohortname . ')';
@@ -364,13 +364,14 @@ class enrol_cohort_plugin extends enrol_plugin {
     protected function get_role_options($instance, $coursecontext) {
         global $DB;
 
-        $roles = get_assignable_roles($coursecontext);
+        $roles = get_assignable_roles($coursecontext, ROLENAME_BOTH);
         $roles[0] = get_string('none');
         $roles = array_reverse($roles, true); // Descending default sortorder.
+
+        // If the instance is already configured, but the configured role is no longer assignable in the course then add it back.
         if ($instance->id and !isset($roles[$instance->roleid])) {
             if ($role = $DB->get_record('role', array('id' => $instance->roleid))) {
-                $roles = role_fix_names($roles, $coursecontext, ROLENAME_ALIAS, true);
-                $roles[$instance->roleid] = role_get_name($role, $coursecontext);
+                $roles[$instance->roleid] = role_get_name($role, $coursecontext, ROLENAME_BOTH);
             } else {
                 $roles[$instance->roleid] = get_string('error');
             }
index 171aa26..257a5c2 100644 (file)
@@ -116,7 +116,7 @@ class enrol_manual_enrol_users_form extends moodleform {
             }
         }
 
-        $roles = get_assignable_roles($context);
+        $roles = get_assignable_roles($context, ROLENAME_BOTH);
         $mform->addElement('select', 'roletoassign', get_string('assignrole', 'enrol_manual'), $roles);
         $mform->setDefault('roletoassign', $instance->roleid);
 
index d9ce6f7..d6d3079 100644 (file)
@@ -31,83 +31,24 @@ defined('MOODLE_INTERNAL') || die();
  * Activity name filtering
  */
 class filter_activitynames extends moodle_text_filter {
-    // Trivial-cache - keyed on $cachedcourseid and $cacheduserid.
-    static $activitylist = null;
-    static $cachedcourseid;
-    static $cacheduserid;
 
     function filter($text, array $options = array()) {
-        global $USER; // Since 2.7 we can finally start using globals in filters.
-
         $coursectx = $this->context->get_course_context(false);
         if (!$coursectx) {
             return $text;
         }
         $courseid = $coursectx->instanceid;
 
-        // Initialise/invalidate our trivial cache if dealing with a different course.
-        if (!isset(self::$cachedcourseid) || self::$cachedcourseid !== (int)$courseid) {
-            self::$activitylist = null;
-        }
-        self::$cachedcourseid = (int)$courseid;
-        // And the same for user id.
-        if (!isset(self::$cacheduserid) || self::$cacheduserid !== (int)$USER->id) {
-            self::$activitylist = null;
-        }
-        self::$cacheduserid = (int)$USER->id;
-
-        /// It may be cached
-
-        if (is_null(self::$activitylist)) {
-            self::$activitylist = array();
-
-            $modinfo = get_fast_modinfo($courseid);
-            if (!empty($modinfo->cms)) {
-                self::$activitylist = array(); // We will store all the created filters here.
-
-                // Create array of visible activities sorted by the name length (we are only interested in properties name and url).
-                $sortedactivities = array();
-                foreach ($modinfo->cms as $cm) {
-                    // Use normal access control and visibility, but exclude labels and hidden activities.
-                    if ($cm->visible and $cm->has_view() and $cm->uservisible) {
-                        $sortedactivities[] = (object)array(
-                            'name' => $cm->name,
-                            'url' => $cm->url,
-                            'id' => $cm->id,
-                            'namelen' => -strlen($cm->name), // Negative value for reverse sorting.
-                        );
-                    }
-                }
-                // Sort activities by the length of the activity name in reverse order.
-                core_collator::asort_objects_by_property($sortedactivities, 'namelen', core_collator::SORT_NUMERIC);
-
-                foreach ($sortedactivities as $cm) {
-                    $title = s(trim(strip_tags($cm->name)));
-                    $currentname = trim($cm->name);
-                    $entitisedname  = s($currentname);
-                    // Avoid empty or unlinkable activity names.
-                    if (!empty($title)) {
-                        $href_tag_begin = html_writer::start_tag('a',
-                                array('class' => 'autolink', 'title' => $title,
-                                    'href' => $cm->url));
-                        self::$activitylist[$cm->id] = new filterobject($currentname, $href_tag_begin, '</a>', false, true);
-                        if ($currentname != $entitisedname) {
-                            // If name has some entity (&amp; &quot; &lt; &gt;) add that filter too. MDL-17545.
-                            self::$activitylist[$cm->id.'-e'] = new filterobject($entitisedname, $href_tag_begin, '</a>', false, true);
-                        }
-                    }
-                }
-            }
-        }
+        $activitylist = $this->get_cached_activity_list($courseid);
 
         $filterslist = array();
-        if (self::$activitylist) {
+        if (!empty($activitylist)) {
             $cmid = $this->context->instanceid;
-            if ($this->context->contextlevel == CONTEXT_MODULE && isset(self::$activitylist[$cmid])) {
+            if ($this->context->contextlevel == CONTEXT_MODULE && isset($activitylist[$cmid])) {
                 // remove filterobjects for the current module
-                $filterslist = array_values(array_diff_key(self::$activitylist, array($cmid => 1, $cmid.'-e' => 1)));
+                $filterslist = array_values(array_diff_key($activitylist, array($cmid => 1, $cmid.'-e' => 1)));
             } else {
-                $filterslist = array_values(self::$activitylist);
+                $filterslist = array_values($activitylist);
             }
         }
 
@@ -117,4 +58,76 @@ class filter_activitynames extends moodle_text_filter {
             return $text;
         }
     }
+
+    /**
+     * Get all the cached activity list for a course
+     *
+     * @param int $courseid id of the course
+     * @return filterobject[] the activities
+     */
+    protected function get_cached_activity_list($courseid) {
+        global $USER;
+        $cached = cache::make_from_params(cache_store::MODE_REQUEST, 'filter', 'activitynames');
+
+        // Return cached activity list.
+        if ($cached->get('cachecourseid') == $courseid && $cached->get('cacheuserid') == $USER->id) {
+            return $cached->get('activitylist');
+        }
+
+        // Not cached yet, get activity list and set cache.
+        $activitylist = $this->get_activity_list($courseid);
+        $cached->set('cacheuserid', $USER->id);
+        $cached->set('cachecourseid', $courseid);
+        $cached->set('activitylist', $activitylist);
+        return $activitylist;
+    }
+
+    /**
+     * Get all the activity list for a course
+     *
+     * @param int $courseid id of the course
+     * @return filterobject[] the activities
+     */
+    protected function get_activity_list($courseid) {
+        $activitylist = array();
+
+        $modinfo = get_fast_modinfo($courseid);
+        if (!empty($modinfo->cms)) {
+            $activitylist = array(); // We will store all the created filters here.
+
+            // Create array of visible activities sorted by the name length (we are only interested in properties name and url).
+            $sortedactivities = array();
+            foreach ($modinfo->cms as $cm) {
+                // Use normal access control and visibility, but exclude labels and hidden activities.
+                if ($cm->visible and $cm->has_view() and $cm->uservisible) {
+                    $sortedactivities[] = (object)array(
+                        'name' => $cm->name,
+                        'url' => $cm->url,
+                        'id' => $cm->id,
+                        'namelen' => -strlen($cm->name), // Negative value for reverse sorting.
+                    );
+                }
+            }
+            // Sort activities by the length of the activity name in reverse order.
+            core_collator::asort_objects_by_property($sortedactivities, 'namelen', core_collator::SORT_NUMERIC);
+
+            foreach ($sortedactivities as $cm) {
+                $title = s(trim(strip_tags($cm->name)));
+                $currentname = trim($cm->name);
+                $entitisedname  = s($currentname);
+                // Avoid empty or unlinkable activity names.
+                if (!empty($title)) {
+                    $hreftagbegin = html_writer::start_tag('a',
+                        array('class' => 'autolink', 'title' => $title,
+                            'href' => $cm->url));
+                    $activitylist[$cm->id] = new filterobject($currentname, $hreftagbegin, '</a>', false, true);
+                    if ($currentname != $entitisedname) {
+                        // If name has some entity (&amp; &quot; &lt; &gt;) add that filter too. MDL-17545.
+                        $activitylist[$cm->id.'-e'] = new filterobject($entitisedname, $hreftagbegin, '</a>', false, true);
+                    }
+                }
+            }
+        }
+        return $activitylist;
+    }
 }
index b21c924..98776b7 100644 (file)
@@ -102,4 +102,56 @@ class filter_activitynames_filter_testcase extends advanced_testcase {
         $this->assertEquals($page->cmid, $matches[2][0]);
         $this->assertEquals($page->name, $matches[3][0]);
     }
+
+    public function test_cache() {
+        $this->resetAfterTest(true);
+
+        // Create a test courses.
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $context1 = context_course::instance($course1->id);
+        $context2 = context_course::instance($course2->id);
+
+        // Create page 1.
+        $page1 = $this->getDataGenerator()->create_module('page',
+            ['course' => $course1->id, 'name' => 'Test 1']);
+        // Format text with page 1 in HTML.
+        $html = '<p>Please read the two pages Test 1 and Test 2.</p>';
+        $filtered1 = format_text($html, FORMAT_HTML, array('context' => $context1));
+        // Find all the activity links in the result.
+        $matches = [];
+        preg_match_all('~<a class="autolink" title="([^"]*)" href="[^"]*/mod/page/view.php\?id=([0-9]+)">([^<]*)</a>~',
+            $filtered1, $matches);
+        // There should be 1 link.
+        $this->assertCount(1, $matches[1]);
+        $this->assertEquals($page1->name, $matches[1][0]);
+
+        // Create page 2.
+        $page2 = $this->getDataGenerator()->create_module('page',
+        ['course' => $course1->id, 'name' => 'Test 2']);
+        // Filter the text again.
+        $filtered2 = format_text($html, FORMAT_HTML, array('context' => $context1));
+        // The filter result does not change due to caching.
+        $this->assertEquals($filtered1, $filtered2);
+
+        // Change context, so that cache for course 1 is cleared.
+        $filtered3 = format_text($html, FORMAT_HTML, array('context' => $context2));
+        $this->assertNotEquals($filtered1, $filtered3);
+        $matches = [];
+        preg_match_all('~<a class="autolink" title="([^"]*)" href="[^"]*/mod/page/view.php\?id=([0-9]+)">([^<]*)</a>~',
+            $filtered3, $matches);
+        // There should be no links.
+        $this->assertCount(0, $matches[1]);
+
+        // Filter the text for course 1.
+        $filtered4 = format_text($html, FORMAT_HTML, array('context' => $context1));
+        // Find all the activity links in the result.
+        $matches = [];
+        preg_match_all('~<a class="autolink" title="([^"]*)" href="[^"]*/mod/page/view.php\?id=([0-9]+)">([^<]*)</a>~',
+            $filtered4, $matches);
+        // There should be 2 links.
+        $this->assertCount(2, $matches[1]);
+        $this->assertEquals($page1->name, $matches[1][0]);
+        $this->assertEquals($page2->name, $matches[1][1]);
+    }
 }
index deb397b..699e02c 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-require_once '../../../config.php';
-require_once $CFG->dirroot.'/grade/lib.php';
-require_once $CFG->dirroot.'/grade/report/lib.php';
-require_once 'category_form.php';
+require_once('../../../config.php');
+require_once($CFG->dirroot.'/grade/lib.php');
+require_once($CFG->dirroot.'/grade/edit/tree/lib.php');
+require_once($CFG->dirroot.'/grade/report/lib.php');
+require_once('category_form.php');
 
 $courseid = required_param('courseid', PARAM_INT);
 $id       = optional_param('id', 0, PARAM_INT); // grade_category->id
@@ -133,132 +134,7 @@ if ($mform->is_cancelled()) {
     redirect($returnurl);
 
 } else if ($data = $mform->get_data(false)) {
-    // If no fullname is entered for a course category, put ? in the DB
-    if (!isset($data->fullname) || $data->fullname == '') {
-        $data->fullname = '?';
-    }
-
-    if (!isset($data->aggregateonlygraded)) {
-        $data->aggregateonlygraded = 0;
-    }
-    if (!isset($data->aggregateoutcomes)) {
-        $data->aggregateoutcomes = 0;
-    }
-    grade_category::set_properties($grade_category, $data);
-
-    /// CATEGORY
-    if (empty($grade_category->id)) {
-        $grade_category->insert();
-
-    } else {
-        $grade_category->update();
-    }
-
-    /// GRADE ITEM
-    // grade item data saved with prefix "grade_item_"
-    $itemdata = new stdClass();
-    foreach ($data as $k => $v) {
-        if (preg_match('/grade_item_(.*)/', $k, $matches)) {
-            $itemdata->{$matches[1]} = $v;
-        }
-    }
-
-    if (!isset($itemdata->aggregationcoef)) {
-        $itemdata->aggregationcoef = 0;
-    }
-
-    if (!isset($itemdata->gradepass) || $itemdata->gradepass == '') {
-        $itemdata->gradepass = 0;
-    }
-
-    if (!isset($itemdata->grademax) || $itemdata->grademax == '') {
-        $itemdata->grademax = 0;
-    }
-
-    if (!isset($itemdata->grademin) || $itemdata->grademin == '') {
-        $itemdata->grademin = 0;
-    }
-
-    $hidden      = empty($itemdata->hidden) ? 0: $itemdata->hidden;
-    $hiddenuntil = empty($itemdata->hiddenuntil) ? 0: $itemdata->hiddenuntil;
-    unset($itemdata->hidden);
-    unset($itemdata->hiddenuntil);
-
-    $locked   = empty($itemdata->locked) ? 0: $itemdata->locked;
-    $locktime = empty($itemdata->locktime) ? 0: $itemdata->locktime;
-    unset($itemdata->locked);
-    unset($itemdata->locktime);
-
-    $convert = array('grademax', 'grademin', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef', 'aggregationcoef2');
-    foreach ($convert as $param) {
-        if (property_exists($itemdata, $param)) {
-            $itemdata->$param = unformat_float($itemdata->$param);
-        }
-    }
-    if (isset($itemdata->aggregationcoef2)) {
-        $itemdata->aggregationcoef2 = $itemdata->aggregationcoef2 / 100.0;
-    }
-
-    // When creating a new category, a number of grade item fields are filled out automatically, and are required.
-    // If the user leaves these fields empty during creation of a category, we let the default values take effect
-    // Otherwise, we let the user-entered grade item values take effect
-    $grade_item = $grade_category->load_grade_item();
-    $grade_item_copy = fullclone($grade_item);
-    grade_item::set_properties($grade_item, $itemdata);
-
-    if (empty($grade_item->id)) {
-        $grade_item->id = $grade_item_copy->id;
-    }
-    if (empty($grade_item->grademax) && $grade_item->grademax != '0') {
-        $grade_item->grademax = $grade_item_copy->grademax;
-    }
-    if (empty($grade_item->grademin) && $grade_item->grademin != '0') {
-        $grade_item->grademin = $grade_item_copy->grademin;
-    }
-    if (empty($grade_item->gradepass) && $grade_item->gradepass != '0') {
-        $grade_item->gradepass = $grade_item_copy->gradepass;
-    }
-    if (empty($grade_item->aggregationcoef) && $grade_item->aggregationcoef != '0') {
-        $grade_item->aggregationcoef = $grade_item_copy->aggregationcoef;
-    }
-
-    // Handle null decimals value - must be done before update!
-    if (!property_exists($itemdata, 'decimals') or $itemdata->decimals < 0) {
-        $grade_item->decimals = null;
-    }
-
-    // Change weightoverride flag. Check if the value is set, because it is not when the checkbox is not ticked.
-    $itemdata->weightoverride = isset($itemdata->weightoverride) ? $itemdata->weightoverride : 0;
-    if ($grade_item->weightoverride != $itemdata->weightoverride && $grade_category->aggregation == GRADE_AGGREGATE_SUM) {
-        // If we are using natural weight and the weight has been un-overriden, force parent category to recalculate weights.
-        $grade_category->force_regrading();
-    }
-    $grade_item->weightoverride = $itemdata->weightoverride;
-
-    $grade_item->outcomeid = null;
-
-    if (!empty($data->grade_item_rescalegrades) && $data->grade_item_rescalegrades == 'yes') {
-        $grade_item->rescale_grades_keep_percentage($grade_item_copy->grademin, $grade_item_copy->grademax, $grade_item->grademin,
-                $grade_item->grademax, 'gradebook');
-    }
-
-    // update hiding flag
-    if ($hiddenuntil) {
-        $grade_item->set_hidden($hiddenuntil, false);
-    } else {
-        $grade_item->set_hidden($hidden, false);
-    }
-
-    $grade_item->set_locktime($locktime); // locktime first - it might be removed when unlocking
-    $grade_item->set_locked($locked, false, true);
-
-    $grade_item->update(); // We don't need to insert it, it's already created when the category is created
-
-    // set parent if needed
-    if (isset($data->parentcategory)) {
-        $grade_category->set_parent($data->parentcategory, 'gradebook');
-    }
-
+    grade_edit_tree::update_gradecategory($grade_category, $data);
     redirect($returnurl);
 }
 
index 91744d8..4216129 100644 (file)
@@ -565,6 +565,141 @@ class grade_edit_tree {
 
         return $deepest_level;
     }
+
+    /**
+     * Updates the provided gradecategory item with the provided data.
+     *
+     * @param grade_category $gradecategory The category to update.
+     * @param stdClass $data the data to update the category with.
+     * @return void
+     */
+    public static function update_gradecategory(grade_category $gradecategory, stdClass $data) {
+        // If no fullname is entered for a course category, put ? in the DB.
+        if (!isset($data->fullname) || $data->fullname == '') {
+            $data->fullname = '?';
+        }
+
+        if (!isset($data->aggregateonlygraded)) {
+            $data->aggregateonlygraded = 0;
+        }
+        if (!isset($data->aggregateoutcomes)) {
+            $data->aggregateoutcomes = 0;
+        }
+        grade_category::set_properties($gradecategory, $data);
+
+        // CATEGORY.
+        if (empty($gradecategory->id)) {
+            $gradecategory->insert();
+
+        } else {
+            $gradecategory->update();
+        }
+
+        // GRADE ITEM.
+        // Grade item data saved with prefix "grade_item_".
+        $itemdata = new stdClass();
+        foreach ($data as $k => $v) {
+            if (preg_match('/grade_item_(.*)/', $k, $matches)) {
+                $itemdata->{$matches[1]} = $v;
+            }
+        }
+
+        if (!isset($itemdata->aggregationcoef)) {
+            $itemdata->aggregationcoef = 0;
+        }
+
+        if (!isset($itemdata->gradepass) || $itemdata->gradepass == '') {
+            $itemdata->gradepass = 0;
+        }
+
+        if (!isset($itemdata->grademax) || $itemdata->grademax == '') {
+            $itemdata->grademax = 0;
+        }
+
+        if (!isset($itemdata->grademin) || $itemdata->grademin == '') {
+            $itemdata->grademin = 0;
+        }
+
+        $hidden      = empty($itemdata->hidden) ? 0 : $itemdata->hidden;
+        $hiddenuntil = empty($itemdata->hiddenuntil) ? 0 : $itemdata->hiddenuntil;
+        unset($itemdata->hidden);
+        unset($itemdata->hiddenuntil);
+
+        $locked   = empty($itemdata->locked) ? 0 : $itemdata->locked;
+        $locktime = empty($itemdata->locktime) ? 0 : $itemdata->locktime;
+        unset($itemdata->locked);
+        unset($itemdata->locktime);
+
+        $convert = array('grademax', 'grademin', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef', 'aggregationcoef2');
+        foreach ($convert as $param) {
+            if (property_exists($itemdata, $param)) {
+                $itemdata->$param = unformat_float($itemdata->$param);
+            }
+        }
+        if (isset($itemdata->aggregationcoef2)) {
+            $itemdata->aggregationcoef2 = $itemdata->aggregationcoef2 / 100.0;
+        }
+
+        // When creating a new category, a number of grade item fields are filled out automatically, and are required.
+        // If the user leaves these fields empty during creation of a category, we let the default values take effect.
+        // Otherwise, we let the user-entered grade item values take effect.
+        $gradeitem = $gradecategory->load_grade_item();
+        $gradeitemcopy = fullclone($gradeitem);
+        grade_item::set_properties($gradeitem, $itemdata);
+
+        if (empty($gradeitem->id)) {
+            $gradeitem->id = $gradeitemcopy->id;
+        }
+        if (empty($gradeitem->grademax) && $gradeitem->grademax != '0') {
+            $gradeitem->grademax = $gradeitemcopy->grademax;
+        }
+        if (empty($gradeitem->grademin) && $gradeitem->grademin != '0') {
+            $gradeitem->grademin = $gradeitemcopy->grademin;
+        }
+        if (empty($gradeitem->gradepass) && $gradeitem->gradepass != '0') {
+            $gradeitem->gradepass = $gradeitemcopy->gradepass;
+        }
+        if (empty($gradeitem->aggregationcoef) && $gradeitem->aggregationcoef != '0') {
+            $gradeitem->aggregationcoef = $gradeitemcopy->aggregationcoef;
+        }
+
+        // Handle null decimals value - must be done before update!
+        if (!property_exists($itemdata, 'decimals') or $itemdata->decimals < 0) {
+            $gradeitem->decimals = null;
+        }
+
+        // Change weightoverride flag. Check if the value is set, because it is not when the checkbox is not ticked.
+        $itemdata->weightoverride = isset($itemdata->weightoverride) ? $itemdata->weightoverride : 0;
+        if ($gradeitem->weightoverride != $itemdata->weightoverride && $gradecategory->aggregation == GRADE_AGGREGATE_SUM) {
+            // If we are using natural weight and the weight has been un-overriden, force parent category to recalculate weights.
+            $gradecategory->force_regrading();
+        }
+        $gradeitem->weightoverride = $itemdata->weightoverride;
+
+        $gradeitem->outcomeid = null;
+
+        if (!empty($data->grade_item_rescalegrades) && $data->grade_item_rescalegrades == 'yes') {
+            $gradeitem->rescale_grades_keep_percentage($gradeitemcopy->grademin, $gradeitemcopy->grademax,
+                $gradeitem->grademin, $gradeitem->grademax, 'gradebook');
+        }
+
+        // Update hiding flag.
+        if ($hiddenuntil) {
+            $gradeitem->set_hidden($hiddenuntil, false);
+        } else {
+            $gradeitem->set_hidden($hidden, false);
+        }
+
+        $gradeitem->set_locktime($locktime); // Locktime first - it might be removed when unlocking.
+        $gradeitem->set_locked($locked, false, true);
+
+        $gradeitem->update(); // We don't need to insert it, it's already created when the category is created.
+
+        // Set parent if needed.
+        if (isset($data->parentcategory)) {
+            $gradecategory->set_parent($data->parentcategory, 'gradebook');
+        }
+    }
 }
 
 /**
@@ -951,5 +1086,4 @@ class grade_edit_tree_column_select extends grade_edit_tree_column {
 
         return $togglegroup;
     }
-}
-
+}
\ No newline at end of file
index 7a8be2d..d3621a9 100644 (file)
@@ -1,7 +1,7 @@
 This files describes API changes in /grade/grading/form/* - Advanced grading methods
 information provided here is intended especially for developers.
 
-=== 4.0 ===
+=== 3.10 ===
 
 * Removed gradingform_provider.
 * Removed the following deprecated functions:
index 40a9b11..86f968b 100644 (file)
@@ -1531,9 +1531,7 @@ class grade_report_grader extends grade_report {
             // This query returns a count of ungraded grades (NULL finalgrade OR no matching record in grade_grades table)
             $sql = "SELECT gi.id, COUNT(DISTINCT u.id) AS count
                       FROM {grade_items} gi
-                      CROSS JOIN {user} u
-                      JOIN ($enrolledsql) je
-                           ON je.id = u.id
+                      CROSS JOIN ($enrolledsql) u
                       JOIN {role_assignments} ra
                            ON ra.userid = u.id
                       LEFT OUTER JOIN {grade_grades} g
@@ -1542,7 +1540,6 @@ class grade_report_grader extends grade_report {
                      WHERE gi.courseid = :courseid
                            AND ra.roleid $gradebookrolessql
                            AND ra.contextid $relatedctxsql
-                           AND u.deleted = 0
                            AND g.id IS NULL
                            $groupwheresql
                   GROUP BY gi.id";
index f3b4ad7..999fc6f 100644 (file)
@@ -164,7 +164,7 @@ class grade extends tablelike implements selectable_items, filterable_items {
     public function original_headers() {
         return array(
             '', // For filter icon.
-            get_string('firstname') . ' (' . get_string('alternatename') . ') ' . get_string('lastname'),
+            get_string('fullnameuser', 'core'),
             get_string('range', 'grades'),
             get_string('grade', 'grades'),
             get_string('feedback', 'grades'),
@@ -198,8 +198,8 @@ class grade extends tablelike implements selectable_items, filterable_items {
             $lockicon = $OUTPUT->pix_icon('t/locked', 'grade is locked') . ' ';
         }
 
-        if (!empty($item->alternatename)) {
-            $fullname = $lockicon . $item->alternatename . ' (' . $item->firstname . ') ' . $item->lastname;
+        if (has_capability('moodle/site:viewfullnames', \context_course::instance($this->courseid))) {
+            $fullname = $lockicon . fullname($item, true);
         } else {
             $fullname = $lockicon . fullname($item);
         }
index fd5e026..5921fc2 100644 (file)
@@ -161,12 +161,12 @@ class finalgrade extends grade_attribute_format implements unique_value, be_disa
         }
 
         if ($errorstr) {
-            $user = $DB->get_record('user', array('id' => $userid), 'id, firstname, alternatename, lastname');
+            $user = get_complete_user_data('id', $userid);
             $gradestr = new stdClass;
-            if (!empty($user->alternatename)) {
-                $gradestr->username = $user->alternatename . ' (' . $user->firstname . ') ' . $user->lastname;
+            if (has_capability('moodle/site:viewfullnames', \context_course::instance($gradeitem->courseid))) {
+                $gradestr->username = fullname($user, true);
             } else {
-                $gradestr->username = $user->firstname . ' ' . $user->lastname;
+                $gradestr->username = fullname($user);
             }
             $gradestr->itemname = $this->grade->grade_item->get_name();
             $errorstr = get_string($errorstr, 'grades', $gradestr);
index fd1153a..faa5ba2 100644 (file)
@@ -42,33 +42,33 @@ Feature: We can bulk insert grades for students in a course
     And I am on "Course 1" course homepage
     And I navigate to "View > Grader report" in the course gradebook
     And I follow "Single view for Test assignment one"
-    Then the field "Grade for james (Student) 1" matches value "50.00"
-    And the field "Override for james (Student) 1" matches value "0"
+    Then the field "Grade for Student 1" matches value "50.00"
+    And the field "Override for Student 1" matches value "0"
     And I set the field "Perform bulk insert" to "1"
     And I set the field "Insert value" to "1.0"
     And I press "Save"
     And I press "Continue"
-    And the field "Grade for james (Student) 1" matches value "50.00"
-    And the field "Override for james (Student) 1" matches value "0"
-    And the field "Grade for holly (Student) 2" matches value "1.00"
-    And the field "Override for holly (Student) 2" matches value "1"
-    And the field "Grade for anna (Student) 3" matches value "1.00"
-    And the field "Override for anna (Student) 3" matches value "1"
-    And the field "Grade for zac (Student) 4" matches value "1.00"
-    And the field "Override for zac (Student) 4" matches value "1"
+    And the field "Grade for Student 1" matches value "50.00"
+    And the field "Override for Student 1" matches value "0"
+    And the field "Grade for Student 2" matches value "1.00"
+    And the field "Override for Student 2" matches value "1"
+    And the field "Grade for Student 3" matches value "1.00"
+    And the field "Override for Student 3" matches value "1"
+    And the field "Grade for Student 4" matches value "1.00"
+    And the field "Override for Student 4" matches value "1"
     And I set the field "For" to "All grades"
     And I set the field "Perform bulk insert" to "1"
     And I set the field "Insert value" to "2.0"
     And I press "Save"
     And I press "Continue"
-    And the field "Grade for james (Student) 1" matches value "2.00"
-    And the field "Override for james (Student) 1" matches value "1"
-    And the field "Grade for holly (Student) 2" matches value "2.00"
-    And the field "Override for holly (Student) 2" matches value "1"
-    And the field "Grade for anna (Student) 3" matches value "2.00"
-    And the field "Override for anna (Student) 3" matches value "1"
-    And the field "Grade for zac (Student) 4" matches value "2.00"
-    And the field "Override for zac (Student) 4" matches value "1"
+    And the field "Grade for Student 1" matches value "2.00"
+    And the field "Override for Student 1" matches value "1"
+    And the field "Grade for Student 2" matches value "2.00"
+    And the field "Override for Student 2" matches value "1"
+    And the field "Grade for Student 3" matches value "2.00"
+    And the field "Override for Student 3" matches value "1"
+    And the field "Grade for Student 4" matches value "2.00"
+    And the field "Override for Student 4" matches value "1"
 
   Scenario: I can bulk insert grades and check their override flags for user view.
     Given I log in as "teacher1"
@@ -110,8 +110,8 @@ Feature: We can bulk insert grades for students in a course
     And I set the field "Perform bulk insert" to "1"
     When I set the field "Insert value" to "-1"
     And I press "Save"
-    Then I should see "The grade entered for Test assignment one for james (Student) 1 is less than the minimum allowed"
-    And I should see "The grade entered for Test assignment one for holly (Student) 2 is less than the minimum allowed"
-    And I should see "The grade entered for Test assignment one for anna (Student) 3 is less than the minimum allowed"
-    And I should see "The grade entered for Test assignment one for zac (Student) 4 is less than the minimum allowed"
+    Then I should see "The grade entered for Test assignment one for Student 1 is less than the minimum allowed"
+    And I should see "The grade entered for Test assignment one for Student 2 is less than the minimum allowed"
+    And I should see "The grade entered for Test assignment one for Student 3 is less than the minimum allowed"
+    And I should see "The grade entered for Test assignment one for Student 4 is less than the minimum allowed"
     And I should see "Grades were set for 0 items"
index e18eb5c..b04c8a7 100644 (file)
@@ -9,13 +9,13 @@ Feature: We can use Single view
       | fullname | shortname | category |
       | Course 1 | C1 | 0 |
     And the following "users" exist:
-      | username | firstname | lastname | email | idnumber | alternatename |
-      | teacher1 | Teacher | 1 | teacher1@example.com | t1 | fred |
-      | teacher2 | No edit | 1 | teacher2@example.com | t2 | nick |
-      | student1 | Student | 1 | student1@example.com | s1 | james |
-      | student2 | Student | 2 | student1@example.com | s2 | holly |
-      | student3 | Student | 3 | student1@example.com | s3 | anna |
-      | student4 | Student | 4 | student1@example.com | s4 | zac |
+      | username | firstname | lastname    | email                | idnumber | middlename | alternatename | firstnamephonetic | lastnamephonetic |
+      | teacher1 | Teacher   | 1           | teacher1@example.com | t1       |            | fred          |                   |                  |
+      | teacher2 | No edit   | 1           | teacher2@example.com | t2       |            | nick          |                   |                  |
+      | student1 | Grainne   | Beauchamp   | student1@example.com | s1       | Ann        | Jill          | Gronya            | Beecham          |
+      | student2 | Niamh     | Cholmondely | student2@example.com | s2       | Jane       | Nina          | Nee               | Chumlee          |
+      | student3 | Siobhan   | Desforges   | student3@example.com | s3       | Sarah      | Sev           | Shevon            | De-forjay        |
+      | student4 | Student   | 4           | student4@example.com | s4       |            | zac           |                   |                  |
     And the following "scales" exist:
       | name | scale |
       | Test Scale | Disappointing, Good, Very good, Excellent |
@@ -50,6 +50,9 @@ Feature: We can use Single view
       | capability                  | permission | role     | contextlevel  | reference |
       | moodle/grade:edit           | Allow      | teacher  | Course        | C1        |
       | gradereport/singleview:view | Allow      | teacher  | Course        | C1        |
+    And the following config values are set as admin:
+      | fullnamedisplay | firstnamephonetic,lastnamephonetic |
+      | alternativefullnameformat | middlename, alternatename, firstname, lastname |
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
     Given I navigate to "View > Grader report" in the course gradebook
@@ -57,7 +60,7 @@ Feature: We can use Single view
   @javascript
   Scenario: I can update grades, add feedback and exclude grades.
     Given I navigate to "View > Single view" in the course gradebook
-    And I select "Student 4" from the "Select user..." singleselect
+    And I select "Student" from the "Select user..." singleselect
     And I set the field "Override for Test assignment one" to "1"
     When I set the following fields to these values:
         | Grade for Test assignment one | 10.00 |
@@ -76,29 +79,29 @@ Feature: We can use Single view
     And the field "Grade for Test grade item" matches value "45.00"
     And the field "Grade for Course total" matches value "55.00"
     And I click on "Show grades for Test assignment three" "link"
-    And I click on "Override for james (Student) 1" "checkbox"
+    And I click on "Override for Ann, Jill, Grainne, Beauchamp" "checkbox"
     And I set the following fields to these values:
-        | Grade for james (Student) 1 | 12.05 |
-        | Feedback for james (Student) 1 | test data2 |
-    And I set the field "Exclude for holly (Student) 2" to "1"
+        | Grade for Ann, Jill, Grainne, Beauchamp | 12.05 |
+        | Feedback for Ann, Jill, Grainne, Beauchamp | test data2 |
+    And I set the field "Exclude for Jane, Nina, Niamh, Cholmondely" to "1"
     And I press "Save"
     Then I should see "Grades were set for 2 items"
     And I press "Continue"
-    And the field "Grade for james (Student) 1" matches value "12.05"
-    And the field "Exclude for holly (Student) 2" matches value "1"
+    And the field "Grade for Ann, Jill, Grainne, Beauchamp" matches value "12.05"
+    And the field "Exclude for Jane, Nina, Niamh, Cholmondely" matches value "1"
     And I select "new grade item 1" from the "Select grade item..." singleselect
-    And I set the field "Grade for james (Student) 1" to "Very good"
+    And I set the field "Grade for Ann, Jill, Grainne, Beauchamp" to "Very good"
     And I press "Save"
     Then I should see "Grades were set for 1 items"
     And I press "Continue"
     And the following should exist in the "generaltable" table:
         | First name (Alternate name) Surname | Grade |
-        | james (Student) 1 | Very good |
+        | Ann, Jill, Grainne, Beauchamp | Very good |
     And I log out
     And I log in as "teacher2"
     And I am on "Course 1" course homepage
     Given I navigate to "View > Single view" in the course gradebook
-    And I select "Student 4" from the "Select user..." singleselect
+    And I select "Student" from the "Select user..." singleselect
     And the "Exclude for Test assignment one" "checkbox" should be disabled
     And the "Override for Test assignment one" "checkbox" should be enabled
 
@@ -106,12 +109,12 @@ Feature: We can use Single view
     Given I follow "Single view for Test assignment one"
     Then I should see "Test assignment one"
     Then I navigate to "View > Grader report" in the course gradebook
-    And I follow "Single view for Student 1"
-    Then I should see "Student 1"
+    And I follow "Single view for Ann, Jill, Grainne, Beauchamp"
+    Then I should see "Gronya,Beecham"
 
   Scenario: I can bulk update grades.
-    Given I follow "Single view for Student 1"
-    Then I should see "Student 1"
+    Given I follow "Single view for Ann, Jill, Grainne, Beauchamp"
+    Then I should see "Gronya,Beecham"
     When I set the field "For" to "All grades"
     And I set the field "Insert value" to "1.0"
     And I set the field "Perform bulk insert" to "1"
@@ -119,12 +122,12 @@ Feature: We can use Single view
     Then I should see "Grades were set for 6 items"
 
   Scenario: Navigation works in the Single view.
-    Given I follow "Single view for Student 1"
-    Then I should see "Student 1"
-    And I follow "Student 2"
-    Then I should see "Student 2"
-    And I follow "Student 1"
-    Then I should see "Student 1"
+    Given I follow "Single view for Ann, Jill, Grainne, Beauchamp"
+    Then I should see "Gronya,Beecham"
+    And I follow "Nee,Chumlee"
+    Then I should see "Nee,Chumlee"
+    And I follow "Gronya,Beecham"
+    Then I should see "Gronya,Beecham"
     And I click on "Show grades for Test assignment four" "link"
     Then I should see "Test assignment four"
     And I follow "Test assignment three"
@@ -134,7 +137,7 @@ Feature: We can use Single view
 
   Scenario: Activities are clickable only when
     it has a valid activity page.
-    Given I follow "Single view for Student 1"
+    Given I follow "Single view for Ann, Jill, Grainne, Beauchamp"
     And "new grade item 1" "link" should not exist in the "//tbody//tr[position()=1]//td[position()=2]" "xpath_element"
     Then "Category total" "link" should not exist in the "//tbody//tr[position()=2]//td[position()=2]" "xpath_element"
     And "Course total" "link" should not exist in the "//tbody//tr[position()=last()]//td[position()=2]" "xpath_element"
index cc2aeeb..9d69597 100644 (file)
@@ -60,7 +60,7 @@ $preview = '';
 $error = '';
 
 /// Get applicable roles - used in menus etc later on
-$rolenames = role_fix_names(get_profile_roles($context), $context, ROLENAME_ALIAS, true);
+$rolenames = role_fix_names(get_profile_roles($context), $context, ROLENAME_BOTH, true);
 
 /// Create the form
 $editform = new autogroup_form(null, array('roles' => $rolenames));
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 9cbc121..a94d9b1 100644 (file)
@@ -440,7 +440,7 @@ class player {
         }
 
         $template = new \stdClass();
-        $template->embedurl = self::get_embed_url($url)->out();
+        $template->embedurl = self::get_embed_url($url, $this->component)->out(false);
 
         return $OUTPUT->render_from_template('core_h5p/h5pembed', $template);
     }
@@ -448,11 +448,18 @@ class player {
     /**
      * Get the encoded URL for embeding this H5P content.
      * @param  string $url The URL of the .h5p file.
+     * @param string $component optional Moodle component to send xAPI tracking
      *
      * @return \moodle_url The embed URL.
      */
-    public static function get_embed_url(string $url): \moodle_url {
-        return new \moodle_url('/h5p/embed.php', ['url' => $url]);
+    public static function get_embed_url(string $url, string $component = ''): \moodle_url {
+        $params = ['url' => $url];
+        if (!empty($component)) {
+            // If component is not empty, it will be passed too, in order to allow tracking too.
+            $params['component'] = $component;
+        }
+
+        return new \moodle_url('/h5p/embed.php', $params);
     }
 
     /**
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 59eb0cb..ee5271c 100644 (file)
@@ -1,7 +1,7 @@
 This files describes API changes in core libraries and APIs,
 information provided here is intended especially for developers.
 
-=== 4.0 ===
+=== 3.10 ===
 * Added a new cache for h5p_library_files (MDL-69207)
 
 === 3.9 ===
index 796e496..6c8b91c 100644 (file)
@@ -41,5 +41,5 @@ $string['cliunknowoption'] = 'Aukera ezezagunak:
 Mesedez, erabili --help aukera.';
 $string['cliyesnoprompt'] = 'idatzi b (bai esateko) edo e (ez esateko)';
 $string['environmentrequireinstall'] = 'derrigorrezkoa da instalatuta eta gaituta izatea';
-$string['environmentrequireversion'] = '{$a->needed} bertsioa beharrezkoa da eta zu {$a->current} ari zara egikaritzen';
+$string['environmentrequireversion'] = '{$a->needed} bertsioa beharrezkoa da eta zu {$a->current} ari zara exekutatzen';
 $string['upgradekeyset'] = 'Eguneraketa-kodea (utzi hutsik kodea erabili nahi ez baduzu)';
diff --git a/install/lang/kaa/langconfig.php b/install/lang/kaa/langconfig.php
new file mode 100644 (file)
index 0000000..4740f55
--- /dev/null
@@ -0,0 +1,33 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thislanguage'] = 'Qaraqalpaq tili';
diff --git a/install/lang/prs/moodle.php b/install/lang/prs/moodle.php
new file mode 100644 (file)
index 0000000..f7b2fbd
--- /dev/null
@@ -0,0 +1,37 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['language'] = 'زبان';
+$string['moodlelogo'] = 'لوگوی مودل (Moodle)';
+$string['next'] = 'بعدی';
+$string['previous'] = 'قبلی';
+$string['reload'] = 'بارگیری مجدد';
index 5cb42af..6cbb336 100644 (file)
@@ -78,6 +78,7 @@ $string['authpreventaccountcreation'] = 'Prevent account creation when authentic
 $string['authpreventaccountcreation_help'] = 'When a user authenticates, an account on the site is automatically created if it doesn\'t yet exist. If an external database, such as LDAP, is used for authentication, but you wish to restrict access to the site to users with an existing account only, then this option should be enabled. New accounts will need to be created manually or via the upload users feature. Note that this setting doesn\'t apply to MNet authentication.';
 $string['authsettings'] = 'Manage authentication';
 $string['autolang'] = 'Language autodetect';
+$string['autolangusercreation'] = 'Use language that is auto detected from users browser during user creation';
 $string['autologinguests'] = 'Auto-login guests';
 $string['searchareas'] = 'Search areas';
 $string['availableto'] = 'Available to';
@@ -173,6 +174,7 @@ $string['configallowview'] = 'Select which roles a user will see, be able to fil
 $string['configallusersaresitestudents'] = 'For activities on the front page of the site, should ALL users be considered as students?  If you answer "Yes", then any confirmed user account will be allowed to participate as a student in those activities.  If you answer "No", then only users who are already a participant in at least one course will be able to take part in those front page activities. Only admins and specially assigned teachers can act as teachers for these front page activities.';
 $string['configauthenticationplugins'] = 'Please choose the authentication plugins you wish to use and arrange them in order of failthrough.';
 $string['configautolang'] = 'Detect default language from browser setting, if disabled site default is used.';
+$string['configautolangusercreation'] = 'Use language from users browser during user creation';
 $string['configautologinguests'] = 'Should visitors be logged in as guests automatically when entering courses with guest access?';
 $string['configbloglevel'] = 'This setting allows you to restrict the level to which user blogs can be viewed on this site.  Note that they specify the maximum context of the VIEWER not the poster or the types of blog posts.  Blogs can also be disabled completely if you don\'t want them at all.';
 $string['configcalendarcustomexport'] = 'Enable custom date range export of calendar';
@@ -228,7 +230,7 @@ $string['configeditorfontlist'] = 'Select the fonts that should appear in the ed
 $string['configemailchangeconfirmation'] = 'Require an email confirmation step when users change their email address in their profile.';
 $string['configemailfromvia'] = 'Add via information in the "From" section of outgoing email. This informs the recipient from where this email came from and also helps combat recipients accidentally replying to no-reply email addresses.';
 $string['configemailsubjectprefix'] = 'Text to be prefixed to the subject line of all outgoing mail.';
-$string['configemailheaders'] = 'Raw email headers to be added verbatum to all outgoing email.';
+$string['configemailheaders'] = 'Raw email headers to be added verbatim to all outgoing email.';
 $string['configenablecalendarexport'] = 'Enable exporting or subscribing to calendars.';
 $string['configenablecomments'] = 'Enable comments';
 $string['configenablecourserequests'] = 'If enabled, users with the capability to request new courses (moodle/course:request) will have the option to request a course. This capability is not allowed for any of the default roles. It may be applied in the system or category context.';
@@ -424,6 +426,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';
@@ -467,7 +471,7 @@ $string['debugsqltrace'] = 'Show origin of SQL calls';
 $string['debugsqltrace1'] = 'Show only a single calling line';
 $string['debugsqltrace2'] = 'Show 2 lines of stack trace';
 $string['debugsqltrace100'] = 'Show full stack trace';
-$string['debugsqltrace_desc'] = 'If enabled adds either partial or full PHP stacktrace into the SQL as a comment';
+$string['debugsqltrace_desc'] = 'If enabled, a partial or full PHP stack trace is added into the SQL as a comment.';
 $string['debugstringids'] = 'Show