Merge branch 'MDL-38350-m310' of https://github.com/sammarshallou/moodle into MOODLE_...
authorSara Arjona <sara@moodle.com>
Mon, 31 Aug 2020 15:03:11 +0000 (17:03 +0200)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 31 Aug 2020 23:28:40 +0000 (07:28 +0800)
297 files changed:
.travis.yml
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/environment.xml
admin/settings/courses.php
admin/settings/plugins.php
admin/settings/server.php
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/templates/evaluation_options.mustache
admin/tool/analytics/templates/export_options.mustache
admin/tool/behat/cli/init.php
admin/tool/customlang/templates/translator.mustache
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/templates/component_status.mustache
admin/tool/dataprivacy/templates/data_registry.mustache
admin/tool/dataprivacy/templates/data_registry_compliance.mustache
admin/tool/dataprivacy/templates/defaults_display.mustache
admin/tool/dataprivacy/templates/summary.mustache
admin/tool/langimport/templates/langimport.mustache
admin/tool/log/upgrade.txt
admin/tool/lp/templates/manage_competencies_page.mustache
admin/tool/lp/tests/behat/course_competencies.feature
admin/tool/mobile/classes/output/subscription.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/settings.php
admin/tool/moodlenet/amd/build/instance_form.min.js
admin/tool/moodlenet/amd/build/instance_form.min.js.map
admin/tool/moodlenet/amd/src/instance_form.js
admin/tool/moodlenet/templates/import_confirmation.mustache
admin/tool/moodlenet/templates/import_options_select.mustache
admin/tool/phpunit/cli/init.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/usertours/classes/manager.php
analytics/templates/insight_info_message_prediction.mustache
auth/db/auth.php
auth/db/lang/en/auth_db.php
auth/ldap/tests/plugin_test.php
auth/tests/behat/login.feature
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js
availability/condition/completion/yui/src/form/js/form.js
availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js
availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js
availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js
availability/condition/date/yui/src/form/js/form.js
availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-debug.js
availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-min.js
availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form.js
availability/condition/grade/yui/src/form/js/form.js
availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-debug.js
availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-min.js
availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form.js
availability/condition/group/yui/src/form/js/form.js
availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-debug.js
availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-min.js
availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form.js
availability/condition/grouping/yui/src/form/js/form.js
availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-debug.js
availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-min.js
availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form.js
availability/condition/profile/yui/src/form/js/form.js
availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js
availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js
availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js
availability/yui/src/form/js/form.js
backup/backup.class.php
backup/moodle2/restore_stepslib.php
backup/util/dbops/restore_dbops.class.php
badges/classes/form/badge.php
badges/classes/form/external_backpack.php
badges/classes/output/external_backpacks_page.php
badges/renderer.php
badges/templates/external_backpacks_page.mustache
badges/tests/behat/backpack.feature
blocks/myoverview/templates/view-list.mustache
blocks/myoverview/templates/view-summary.mustache
blocks/recentlyaccessedcourses/amd/build/main.min.js
blocks/recentlyaccessedcourses/amd/build/main.min.js.map
blocks/recentlyaccessedcourses/amd/src/main.js
blocks/rss_client/templates/item.mustache
blocks/timeline/templates/view.mustache
cache/upgrade.txt
calendar/classes/external/week_day_exporter.php
calendar/templates/event_item.mustache
calendar/templates/month_detailed.mustache
calendar/templates/month_mini.mustache
calendar/upgrade.txt
cohort/index.php
competency/classes/api.php
contentbank/amd/build/actions.min.js
contentbank/amd/build/actions.min.js.map
contentbank/amd/build/selectors.min.js
contentbank/amd/build/selectors.min.js.map
contentbank/amd/build/sort.min.js
contentbank/amd/build/sort.min.js.map
contentbank/amd/src/actions.js
contentbank/amd/src/selectors.js
contentbank/amd/src/sort.js
contentbank/classes/content.php
contentbank/classes/contentbank.php
contentbank/classes/contenttype.php
contentbank/classes/external/rename_content.php
contentbank/classes/output/bankcontent.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/templates/bankcontent.mustache
contentbank/tests/behat/download_content.feature [new file with mode: 0644]
contentbank/tests/behat/sort_content.feature
contentbank/tests/content_test.php
contentbank/tests/contentbank_test.php
contentbank/tests/contenttype_test.php
contentbank/tests/external/rename_content_test.php
contentbank/upload.php
contentbank/view.php
course/classes/editcategory_form.php
course/externallib.php
course/format/topics/backup/moodle2/restore_format_topics_plugin.class.php
course/format/weeks/backup/moodle2/restore_format_weeks_plugin.class.php
course/lib.php
course/templates/activityinstance.mustache
course/templates/bulkactivitycompletion.mustache
course/templates/coursecard.mustache
course/templates/defaultactivitycompletion.mustache
course/upgrade.txt
customfield/field/select/classes/data_controller.php
customfield/field/select/classes/field_controller.php
grade/edit/outcome/course_form.html
grade/grading/form/upgrade.txt
grade/report/user/externallib.php
grade/report/user/lib.php
grade/report/user/tests/externallib_test.php
group/import.php
group/import_form.php
group/templates/index.mustache
h5p/classes/api.php
h5p/classes/editor_framework.php
h5p/classes/framework.php
h5p/tests/generator_test.php
h5p/upgrade.txt
lang/en/admin.php
lang/en/antivirus.php
lang/en/backup.php
lang/en/badges.php
lang/en/calendar.php
lang/en/contentbank.php
lang/en/course.php
lang/en/error.php
lang/en/group.php
lang/en/mimetypes.php
lang/en/moodle.php
lang/en/role.php
lib/antivirus/clamav/classes/scanner.php
lib/badgeslib.php
lib/classes/antivirus/manager.php
lib/classes/antivirus/quarantine.php [new file with mode: 0644]
lib/classes/antivirus/scanner.php
lib/classes/date.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/filetypes.php
lib/classes/plugin_manager.php
lib/classes/task/antivirus_cleanup_task.php [new file with mode: 0644]
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/cronlib.php
lib/db/access.php
lib/db/install.xml
lib/db/messages.php
lib/db/tasks.php
lib/db/upgrade.php
lib/editor/atto/plugins/image/styles.css
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js
lib/editor/atto/plugins/table/yui/src/button/js/button.js
lib/environmentlib.php
lib/filelib.php
lib/form/filemanager.js
lib/form/tests/filetypes_util_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/outputlib.php
lib/outputrenderers.php
lib/pagelib.php
lib/templates/auth_digital_minor_page.mustache
lib/templates/filemanager_uploadform.mustache
lib/templates/infected_file_email.mustache [new file with mode: 0644]
lib/testing/lib.php
lib/tests/antivirus_test.php
lib/tests/moodle_page_test.php
lib/tests/scheduled_task_test.php
lib/tests/setuplib_test.php
lib/tests/task_running_test.php [new file with mode: 0644]
lib/tests/weblib_test.php
lib/upgrade.txt
message/templates/message_index.mustache
message/templates/message_preferences_component.mustache
message/templates/message_preferences_notification_processor.mustache
message/templates/notification_preferences_component.mustache
message/templates/notification_preferences_component_notification.mustache
mod/assign/feedback/file/importziplib.php
mod/assign/feedback/file/tests/importziplib_test.php [new file with mode: 0644]
mod/assign/locallib.php
mod/book/edit_form.php
mod/book/tool/print/classes/output/renderer.php
mod/book/tool/print/templates/print_book.mustache
mod/book/tool/print/templates/print_book_chapter.mustache
mod/chat/chat_ajax.php
mod/chat/gui_ajax/index.php
mod/chat/gui_ajax/module.js
mod/chat/gui_ajax/theme/bubble/chat.css
mod/chat/gui_ajax/theme/compact/chat.css
mod/chat/gui_basic/index.php
mod/choice/lang/en/choice.php
mod/forum/classes/local/renderers/discussion.php
mod/forum/report/summary/index.php
mod/forum/report/summary/templates/filter_groups.mustache
mod/forum/templates/discussion_list.mustache
mod/forum/templates/forum_discussion.mustache
mod/forum/templates/local/grades/view_grade.mustache
mod/forum/tests/behat/portfolio_export.feature [new file with mode: 0644]
mod/forum/tests/mail_test.php
mod/glossary/formats/entrylist/entrylist_format.php
mod/glossary/lib.php
mod/lesson/lang/en/lesson.php
mod/lesson/locallib.php
mod/lesson/renderer.php
mod/lti/service/memberships/classes/local/service/memberships.php
mod/lti/view.php
mod/quiz/templates/modal_add_random_question.mustache
mod/quiz/tests/behat/manually_mark_question.feature
mod/workshop/form/rubric/styles.css
question/behaviour/upgrade.txt
question/type/ddimageortext/rendererbase.php
question/type/ddimageortext/styles.css
question/type/ddmarker/renderer.php
question/type/ddmarker/styles.css
report/competency/templates/report.mustache
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
report/participation/index.php
repository/equella/lib.php
search/upgrade.txt
theme/boost/scss/moodle.scss
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/backup-restore.scss
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/bs2-compat.scss [deleted file]
theme/boost/scss/moodle/bs4alphacompat.scss [deleted file]
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/contentbank.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/moodle/tool_usertours.scss
theme/boost/scss/moodle/variables.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/index.php
theme/upgrade.txt
user/edit.php
user/editadvanced.php
user/externallib.php
version.php

index d751251..da6a827 100644 (file)
@@ -174,7 +174,9 @@ before_script:
         # Enable test external resources
         sed -i \
           -e "/require_once/i \\define('TEST_EXTERNAL_FILES_HTTP_URL', 'http://127.0.0.1:8080');" \
+          -e "/require_once/i \\define('TEST_EXTERNAL_FILES_HTTPS_URL', 'http://127.0.0.1:8080');" \
           config.php ;
+
         # Redis cache store tests
         sed -i \
           -e "/require_once/i \\define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1');" \
@@ -212,7 +214,7 @@ before_script:
         # We need the official upstream for comparison
         git remote add upstream https://github.com/moodle/moodle.git;
 
-        git fetch upstream master;
+        git fetch upstream MOODLE_310_STABLE;
         export GIT_PREVIOUS_COMMIT="`git merge-base FETCH_HEAD $TRAVIS_COMMIT`";
         export GIT_COMMIT="$TRAVIS_COMMIT";
         export UPSTREAM_FETCH_HEAD=`git rev-parse FETCH_HEAD`
@@ -254,8 +256,6 @@ script:
       if [ "$TASK" = 'PHPUNIT' ];
       then
         vendor/bin/phpunit --fail-on-risky --disallow-test-output --verbose;
-        EXTTESTS_HITS=$(docker logs exttests 2>&1 | grep -Fv -e 'AH00558' -e '[pid 1]' | wc -l)
-        echo -e "\nTest local resources number of hits: ${EXTTESTS_HITS}.\n"
       fi
 
     - >
@@ -292,3 +292,11 @@ script:
           exit 1 ;
         fi
       fi
+
+after_script:
+    - >
+      if [ "$TASK" = 'PHPUNIT' ];
+      then
+        EXTTESTS_HITS=$(docker logs exttests 2>&1 | grep -Fv -e 'AH00558' -e '[pid 1]' | wc -l)
+        echo -e "\nTest local resources number of hits: ${EXTTESTS_HITS}.\n"
+      fi
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 6f6fb81..3c6365f 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="3.10" requires="3.5">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mariadb" version="10.2.29" />
+      <VENDOR name="mysql" version="5.7" />
+      <VENDOR name="postgres" version="9.6" />
+      <VENDOR name="mssql" version="11.0" />
+      <VENDOR name="oracle" version="11.2" />
+    </DATABASE>
+    <PHP version="7.2.0" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="mbstringrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="opensslrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="gdrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlreader" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="intlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+      <PHP_EXTENSION name="fileinfo" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="96M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+    <CUSTOM_CHECKS>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbstorageengine" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="quizattemptsupgradedmessage" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="slashargumentswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unsupporteddbtablerowformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unoconvwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfileformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfilepertable" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddblargeprefix" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="ishttpswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="incompleteunicodesupport" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="sixtyfourbitswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index b6ba368..e7d9bf9 100644 (file)
@@ -184,6 +184,7 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     // Add a category for the Activity Chooser.
     $ADMIN->add('courses', new admin_category('activitychooser', new lang_string('activitychoosercategory', 'course')));
     $temp = new admin_settingpage('activitychoosersettings', new lang_string('activitychoosersettings', 'course'));
+    // Tab mode for the activity chooser.
     $temp->add(
         new admin_setting_configselect(
             'activitychoosertabmode',
@@ -197,6 +198,31 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
             ]
         )
     );
+
+    // Build a list of plugins that use the footer callback.
+    $pluginswithfunction = get_plugins_with_function('custom_chooser_footer', 'lib.php');
+    $pluginsoptions = [];
+    $pluginsoptions[COURSE_CHOOSER_FOOTER_NONE] = get_string('activitychooserhidefooter', 'course');
+    if ($pluginswithfunction) {
+        foreach ($pluginswithfunction as $plugintype => $plugins) {
+            foreach ($plugins as $pluginname => $pluginfunction) {
+                $plugin = $plugintype.'_'.$pluginname;
+                $pluginsoptions[$plugin] = get_string('pluginname', $plugin);
+            }
+        }
+    }
+
+    // Select what plugin to show in the footer.
+    $temp->add(
+        new admin_setting_configselect(
+            'activitychooseractivefooter',
+            new lang_string('activitychooseractivefooter', 'course'),
+            new lang_string('activitychooseractivefooter_desc', 'course'),
+            COURSE_CHOOSER_FOOTER_NONE,
+            $pluginsoptions
+        )
+    );
+
     $ADMIN->add('activitychooser', $temp);
     $ADMIN->add('activitychooser',
         new admin_externalpage('activitychooserrecommended', new lang_string('activitychooserrecommendations', 'course'),
index cc23ff3..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');
@@ -555,7 +591,7 @@ if ($hassiteconfig) {
         global $CFG;
 
         // Check nobody's setting the indexing and query-only server to the same one.
-        if ($CFG->searchenginequeryonly === $value) {
+        if (isset($CFG->searchenginequeryonly) && $CFG->searchenginequeryonly === $value) {
             return get_string('searchenginequeryonlysame', 'admin');
         } else {
             return '';
@@ -625,7 +661,7 @@ if ($hassiteconfig) {
         global $CFG;
 
         // Check nobody's setting the indexing and query-only server to the same one.
-        if ($CFG->searchengine === $value) {
+        if (isset($CFG->searchengine) && $CFG->searchengine === $value) {
             return get_string('searchenginequeryonlysame', 'admin');
         } else {
             return '';
index e86bbc9..e9fac46 100644 (file)
@@ -216,6 +216,16 @@ $ADMIN->add('server', $temp);
 
 $ADMIN->add('server', new admin_category('taskconfig', new lang_string('taskadmintitle', 'admin')));
 $temp = new admin_settingpage('taskprocessing', new lang_string('taskprocessing','admin'));
+
+$setting = new admin_setting_configcheckbox(
+    'cron_enabled',
+    new lang_string('cron_enabled', 'admin'),
+    new lang_string('cron_enabled_desc', 'admin'),
+    1
+);
+$setting->set_updatedcallback('theme_reset_static_caches');
+$temp->add($setting);
+
 $temp->add(
     new admin_setting_configtext(
         'task_scheduled_concurrency_limit',
index ade6739..be1cb66 100644 (file)
@@ -64,7 +64,7 @@ class models_list implements \renderable, \templatable {
         $data = new \stdClass();
 
         $newmodelmenu = new \action_menu();
-        $newmodelmenu->set_menu_trigger(get_string('newmodel', 'tool_analytics'), 'btn btn-default');
+        $newmodelmenu->set_menu_trigger(get_string('newmodel', 'tool_analytics'), 'btn btn-secondary');
         $newmodelmenu->set_alignment(\action_menu::TL, \action_menu::BL);
 
         $newmodelmenu->add(new \action_menu_link(
index 9d77c13..7f4ce8e 100644 (file)
 {{/trainedexternally}}
 
 {{! Hidden by default if #trainedexternally as the default option is trainedmodel in this case.}}
-<div id="id-evaluation-timesplitting-container" class="m-t-1 {{#trainedexternally}}hidden{{/trainedexternally}}">
+<div id="id-evaluation-timesplitting-container" class="mt-3 {{#trainedexternally}}hidden{{/trainedexternally}}">
     {{#str}} selecttimesplittingforevaluation, tool_analytics {{/str}}
     <div>
-        <select id="id-evaluation-timesplitting" name="timesplitting" class="custom-select m-t-1">
+        <select id="id-evaluation-timesplitting" name="timesplitting" class="custom-select mt-3">
             {{#timesplittingmethods}}
                 <option value="{{id}}">{{text}}</option>
             {{/timesplittingmethods}}
index 34ebe94..c05550d 100644 (file)
@@ -39,7 +39,7 @@
     <input class="custom-control-input" type="radio" name="exportoption" id="id-mode-exportmodel" value="exportmodel" checked>
     <label class="custom-control-label" for="id-mode-exportmodel">{{#str}} exportmodel, tool_analytics {{/str}}</label>
 </div>
-<div class="custom-control custom-checkbox m-l-2" id="id-includeweights-container">
+<div class="custom-control custom-checkbox ml-5" id="id-includeweights-container">
   <input class="custom-control-input" type="checkbox" id="id-includeweights" value="1" checked>
   <label class="custom-control-label" for="id-includeweights">{{#str}} exportincludeweights, tool_analytics {{/str}}</label>
 </div>
index 91a5d86..ce09a1c 100644 (file)
@@ -50,6 +50,9 @@ list($options, $unrecognized) = cli_get_params(
         'optimize-runs' => '',
         'add-core-features-to-theme' => false,
         'axe'      => false,
+        'disable-composer' => false,
+        'composer-upgrade' => true,
+        'composer-self-update' => true,
     ),
     array(
         'j' => 'parallel',
@@ -65,19 +68,36 @@ $help = "
 Behat utilities to initialise behat tests
 
 Usage:
-  php init.php [--parallel=value [--maxruns=value] [--fromrun=value --torun=value]] [--help]
+  php init.php      [--parallel=value [--maxruns=value] [--fromrun=value --torun=value]]
+                    [--axe] [-o | --optimize-runs] [-a | --add-core-features-to-theme]
+                    [--no-composer-self-update] [--no-composer-upgrade]
+                    [--disable-composer]
+                    [--help]
 
 Options:
--j, --parallel   Number of parallel behat run to initialise
--m, --maxruns    Max parallel processes to be executed at one time.
---fromrun        Execute run starting from (Used for parallel runs on different vms)
---torun          Execute run till (Used for parallel runs on different vms)
---axe            Include axe accessibility tests
+-j, --parallel      Number of parallel behat run to initialise
+-m, --maxruns       Max parallel processes to be executed at one time
+--fromrun           Execute run starting from (Used for parallel runs on different vms)
+--torun             Execute run till (Used for parallel runs on different vms)
+--axe               Include axe accessibility tests
 
--o, --optimize-runs Split features with specified tags in all parallel runs.
--a, --add-core-features-to-theme Add all core features to specified theme's
+-o, --optimize-runs
+                    Split features with specified tags in all parallel runs.
 
--h, --help     Print out this help
+-a, --add-core-features-to-theme
+                    Add all core features to specified theme's
+
+--no-composer-self-update
+                    Prevent upgrade of the composer utility using its self-update command
+
+--no-composer-upgrade
+                    Prevent update development dependencies using composer
+
+--disable-composer
+                    A shortcut to disable composer self-update and dependency update
+                    Note: Installation of composer and/or dependencies will still happen as required
+
+-h, --help          Print out this help
 
 Example from Moodle root directory:
 \$ php admin/tool/behat/cli/init.php --parallel=2
@@ -120,8 +140,15 @@ if ($options['parallel'] && $options['parallel'] > 1) {
 $cwd = getcwd();
 $output = null;
 
-// If behat dependencies not downloaded then do it first, else symfony/process can't be used.
-testing_update_composer_dependencies();
+if ($options['disable-composer']) {
+    // Disable self-update and upgrade easily.
+    // Note: Installation will still occur regardless of this setting.
+    $options['composer-self-update'] = false;
+    $options['composer-upgrade'] = false;
+}
+
+// Install and update composer and dependencies as required.
+testing_update_composer_dependencies($options['composer-self-update'], $options['composer-upgrade']);
 
 // Check whether the behat test environment needs to be updated.
 chdir(__DIR__);
index 62cbdac..400464c 100644 (file)
@@ -59,7 +59,7 @@
     <input type="hidden" name="sesskey" value="{{{ sesskey }}}">
     <input type="hidden" name="p" value="{{ currentpage }}">
 
-    <fieldset class="m-a-1 m-3">
+    <fieldset class="m-3">
         <button type="submit" name="savecontinue" class="btn btn-secondary">
             {{#str}}savecontinue, tool_customlang{{/str}}
         </button>
@@ -70,7 +70,7 @@
 
     <div class="list-group">
         <div class="container-fluid d-none d-md-block list-group-item border-bottom-0">
-            <div class="row-fluid">
+            <div class="row">
                 <div class="col-sm-4 col-md-2">
                     <strong>{{#str}}headingcomponent, tool_customlang{{/str}}</strong>
                 </div>
@@ -81,7 +81,7 @@
                     <strong>{{#str}}headingstandard, tool_customlang{{/str}}</strong>
                 </div>
                 <div class="col-sm-12 col-md-6">
-                    <span class="p-l-1 pl-3">
+                    <span class="pl-3">
                         <strong>{{#str}}headinglocal, tool_customlang{{/str}}</strong>
                     </span>
                 </div>
@@ -96,7 +96,7 @@
                 {{#outdated}}list-group-item-warning{{/outdated}}
                 {{#modified}}list-group-item-info{{/modified}}"
             >
-            <div class="row-fluid ">
+            <div class="row">
                 <div class="col-sm-4 col-md-2">
                     <div class="d-md-none">
                         <strong>{{#str}}headingcomponent, tool_customlang{{/str}}</strong>
     {{/strings}}
     </div>
 
-    <fieldset class="m-a-1 m-3">
+    <fieldset class="m-3">
         <button type="submit" name="savecontinue" class="btn btn-secondary">
             {{#str}}savecontinue, tool_customlang{{/str}}
         </button>
index 63d33fe..54d9c5b 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!';
index 8d2bf1b..7130dbf 100644 (file)
     <hr />
     <div class="row">
         {{#compliant}}
-            <a class="component-expand p-l-2" data-component="{{raw_component}}" href='#'>
-            <h4 class=" d-inline p-l-2 " id="{{raw_component}}">{{#pix}}t/collapsed, moodle, {{#str}}expandplugin, tool_dataprivacy{{/str}}{{/pix}}{{component}}</h4>
+            <a class="component-expand pl-5" data-component="{{raw_component}}" href='#'>
+            <h4 class=" d-inline pl-5 " id="{{raw_component}}">{{#pix}}t/collapsed, moodle, {{#str}}expandplugin, tool_dataprivacy{{/str}}{{/pix}}{{component}}</h4>
             </a>
         {{/compliant}}
         {{^compliant}}
-            <h4 class="d-inline p-l-3 " id="{{raw_component}}">{{component}}</h4>
+            <h4 class="d-inline pl-6 " id="{{raw_component}}">{{component}}</h4>
             <span>{{#pix}}i/risk_xss, moodle, {{#str}}requiresattention, tool_dataprivacy{{/str}}{{/pix}}</span>
         {{/compliant}}
         {{#external}}
@@ -73,7 +73,7 @@
         <div class="hide" data-section="{{raw_component}}" aria-expanded="false" role="contentinfo">
             {{#metadata}}
                 <hr />
-                <div class="p-l-3">
+                <div class="pl-6">
                     <dl class="row">
                         <dt class="col-3">
                             {{#link}}
@@ -98,7 +98,7 @@
             {{/metadata}}
             {{#nullprovider}}
                 <hr />
-                <div class="p-l-3">
+                <div class="pl-6">
                     <div class="row">
                         <div class="col-12">
                             {{nullprovider}}
             {{/nullprovider}}
         </div>
     {{/compliant}}
-</div>
\ No newline at end of file
+</div>
index d77c7cf..501c5df 100644 (file)
@@ -54,7 +54,7 @@
 
     <div class="container-fluid mt-2">
         <div class="row">
-            <div class="col-md-4 p-l-0 nav-pills context-tree">
+            <div class="col-md-4 pl-0 nav-pills context-tree">
                 {{#tree}}
                     {{> tool_dataprivacy/context_tree_node}}
                 {{/tree}}
index c2231f3..ec41fc2 100644 (file)
@@ -59,7 +59,7 @@
             <h3 id="{{plugin_type_raw}}">{{#pix}}t/collapsed, moodle, {{#str}}expandplugintype, tool_dataprivacy{{/str}}{{/pix}}{{plugin_type}}</h3>
             </a>
         </div>
-        <div class="hide p-b-1" data-plugintarget="{{plugin_type_raw}}" aria-expanded="false" role="contentinfo">
+        <div class="hide pb-3" data-plugintarget="{{plugin_type_raw}}" aria-expanded="false" role="contentinfo">
             {{#plugins}}
                 {{> tool_dataprivacy/component_status}}
             {{/plugins}}
index dc1eb72..0053a31 100644 (file)
@@ -41,9 +41,9 @@
         "contextlevel": 70
     }
 }}
-<div class="row-fluid rtl-compatible mt-1 mb-1">
+<div class="row rtl-compatible mt-1 mb-1">
     <div class="col-md-9">
-        <div class="row-fluid rtl-compatible mt-1 mb-1">
+        <div class="row rtl-compatible mt-1 mb-1">
             <div class="col-md-3">
                 <strong>{{#str}}category, tool_dataprivacy{{/str}}</strong>
             </div>
@@ -51,7 +51,7 @@
                 {{category}}
             </div>
         </div>
-        <div class="row-fluid rtl-compatible mt-1 mb-1">
+        <div class="row rtl-compatible mt-1 mb-1">
             <div class="col-md-3">
                 <strong>{{#str}}purpose, tool_dataprivacy{{/str}}</strong>
             </div>
index 22c0221..a6be02e 100644 (file)
@@ -74,7 +74,7 @@
     {{#contexts}}
         <div class="card mb-3">
             <div class="card-header"><h3>{{contextname}}</h3></div>
-            <div class="card-body p-l-2 p-r-2">
+            <div class="card-body pl-5 pr-5">
 
                 {{#category.name}}
                 <h4>{{#str}}category, tool_dataprivacy{{/str}}</h4>
             </div>
         </div>
     {{/contexts}}
-</div>
\ No newline at end of file
+</div>
index 4b6b645..71a7515 100644 (file)
@@ -65,7 +65,7 @@
     }
 }}
 <div class="container-fluid langimport">
-    <div class="row row-fluid rtl-compatible">
+    <div class="row rtl-compatible">
         <div class="col-md-{{#caninstall}}6{{/caninstall}}{{^caninstall}}12{{/caninstall}} span{{#caninstall}}6{{/caninstall}}{{^caninstall}}12{{/caninstall}} mb-1">
             <form id="uninstallform" action="{{uninstallurl}}" method="post">
                 <fieldset>
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 ccc67ba..1c2cd11 100644 (file)
@@ -46,7 +46,7 @@
 </h2>
 <div>{{{framework.description}}}</div>
     <h3>{{#str}}competencies, core_competency{{/str}}</h3>
-    <div class="row-fluid row">
+    <div class="row">
         <div class="col-lg-6">
             <p>
                 <form data-region="filtercompetencies" data-frameworkid="{{framework.id}}" class="form-inline">
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 d8deaca..572a91a 100644 (file)
@@ -184,6 +184,13 @@ class subscription implements \renderable, \templatable {
                                     'type' => 'danger', 'message' => get_string('subscriptionfeaturenotapplied', 'tool_mobile')];
                             }
                             break;
+                        // Check QR automatic login.
+                        case 'qrautomaticlogin':
+                            if ($ms->qrcodetype == \tool_mobile\api::QR_CODE_LOGIN) {
+                                $feature['message'] = [
+                                    'type' => 'danger', 'message' => get_string('subscriptionfeaturenotapplied', 'tool_mobile')];
+                            }
+                            break;
                     }
                 }
             }
index bc7fc89..952e99b 100644 (file)
@@ -114,7 +114,7 @@ $string['qrcodeformobileapploginabout'] = 'Scan the QR code with your mobile app
 $string['qrcodeformobileappurlabout'] = 'Scan the QR code with your mobile app to fill in the site URL in your app.';
 $string['qrsiteadminsnotallowed'] = 'For security reasons login via QR code is not allowed for site administrators or if you are logged in as another user.';
 $string['qrcodetype'] = 'QR code access';
-$string['qrcodetype_desc'] = 'A QR code can be provided for mobile app users to scan and either have the site URL filled in or be automatically logged in without having to enter their credentials.';
+$string['qrcodetype_desc'] = 'A QR code can be provided for mobile app users to scan. This can be used to fill in the site URL, or where the site is secured using HTTPS, to automatically log the user in without having to enter their username and password.';
 $string['qrcodetypeurl'] = 'QR code with site URL';
 $string['qrcodetypelogin'] = 'QR code with automatic login';
 $string['readingthisemailgettheapp'] = 'Reading this in an email? <a href="{$a}">Download the mobile app and receive notifications on your mobile device</a>.';
index c2cee8c..19a8798 100644 (file)
@@ -94,11 +94,17 @@ if ($hassiteconfig) {
     $options = [
         tool_mobile\api::QR_CODE_DISABLED => new lang_string('qrcodedisabled', 'tool_mobile'),
         tool_mobile\api::QR_CODE_URL => new lang_string('qrcodetypeurl', 'tool_mobile'),
-        tool_mobile\api::QR_CODE_LOGIN => new lang_string('qrcodetypelogin', 'tool_mobile'),
     ];
+    $qrcodetypedefault = tool_mobile\api::QR_CODE_URL;
+
+    if (is_https()) {   // Allow QR login for https sites.
+        $options[tool_mobile\api::QR_CODE_LOGIN] = new lang_string('qrcodetypelogin', 'tool_mobile');
+        $qrcodetypedefault = tool_mobile\api::QR_CODE_LOGIN;
+    }
+
     $temp->add(new admin_setting_configselect('tool_mobile/qrcodetype',
                 new lang_string('qrcodetype', 'tool_mobile'),
-                new lang_string('qrcodetype_desc', 'tool_mobile'), tool_mobile\api::QR_CODE_LOGIN, $options));
+                new lang_string('qrcodetype_desc', 'tool_mobile'), $qrcodetypedefault, $options));
 
     $temp->add(new admin_setting_configtext('tool_mobile/forcedurlscheme',
                 new lang_string('forcedurlscheme_key', 'tool_mobile'),
index a6f0561..70d692b 100644 (file)
Binary files a/admin/tool/moodlenet/amd/build/instance_form.min.js and b/admin/tool/moodlenet/amd/build/instance_form.min.js differ
index 3e2853c..61f257e 100644 (file)
Binary files a/admin/tool/moodlenet/amd/build/instance_form.min.js.map and b/admin/tool/moodlenet/amd/build/instance_form.min.js.map differ
index a1443a0..cc0580e 100644 (file)
@@ -67,7 +67,7 @@ define(['tool_moodlenet/validator',
                             input.classList.remove('is-invalid'); // Just in case the class has been applied already.
                             input.classList.add('is-valid');
                             validationArea.innerText = result.message;
-                            validationArea.classList.remove('text-error');
+                            validationArea.classList.remove('text-danger');
                             validationArea.classList.add('text-success');
                             // Give the user some time to see their input is valid.
                             setTimeout(function() {
@@ -76,7 +76,7 @@ define(['tool_moodlenet/validator',
                         } else {
                             input.classList.add('is-invalid');
                             validationArea.innerText = result.message;
-                            validationArea.classList.add('text-error');
+                            validationArea.classList.add('text-danger');
                         }
                         return;
                 }).catch();
index 337b22a..f38cc0e 100644 (file)
@@ -53,7 +53,7 @@
 
             <input type="hidden" name="resourceurl" value="{{resourceurl}}">
             <input type="hidden" name="sesskey" value="{{sesskey}}">
-            <div class="box py-3 modal-header p-x-1">
+            <div class="box py-3 modal-header px-3">
                 <h4>{{#str}}confirm, core{{/str}}</h4>
             </div>
             <div class="box py-3 modal-body">
index 9dc42eb..f22566e 100644 (file)
@@ -58,7 +58,7 @@
             <input type="hidden" name="section" value="{{section}}">
             <input type="hidden" name="resourceurl" value="{{resourceurl}}">
             <input type="hidden" name="sesskey" value="{{sesskey}}">
-            <div class="box py-3 modal-header p-x-1">
+            <div class="box py-3 modal-header px-3">
                 <h4>{{#str}}importformatselectheader, tool_moodlenet{{/str}}</h4>
             </div>
             <div class="box py-3 modal-body">
index 0785277..9c17377 100644 (file)
@@ -38,8 +38,57 @@ require_once(__DIR__.'/../../../../lib/clilib.php');
 require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
 require_once(__DIR__.'/../../../../lib/testing/lib.php');
 
+list($options, $unrecognized) = cli_get_params(
+    [
+        'help'                 => false,
+        'disable-composer'     => false,
+        'composer-upgrade'     => true,
+        'composer-self-update' => true,
+    ],
+    [
+        'h' => 'help',
+    ]
+);
+
+$help = "
+Utilities to initialise the PHPUnit test site.
+
+Usage:
+  php init.php [--no-composer-self-update] [--no-composer-upgrade]
+               [--help]
+
+--no-composer-self-update
+                    Prevent upgrade of the composer utility using its self-update command
+
+--no-composer-upgrade
+                    Prevent update development dependencies using composer
+
+--disable-composer
+                    A shortcut to disable composer self-update and dependency update
+                    Note: Installation of composer and/or dependencies will still happen as required
+
+-h, --help          Print out this help
+
+Example from Moodle root directory:
+\$ php admin/tool/phpunit/cli/init.php
+";
+
+if (!empty($options['help'])) {
+    echo $help;
+    exit(0);
+}
+
 echo "Initialising Moodle PHPUnit test environment...\n";
-testing_update_composer_dependencies();
+
+if ($options['disable-composer']) {
+    // Disable self-update and upgrade easily.
+    // Note: Installation will still occur regardless of this setting.
+    $options['composer-self-update'] = false;
+    $options['composer-upgrade'] = false;
+}
+
+// Install and update composer and dependencies as required.
+testing_update_composer_dependencies($options['composer-self-update'], $options['composer-upgrade']);
 
 $output = null;
 exec('php --version', $output, $code);
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 636233a..40800e0 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2020061500; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2020061501; // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2020060900; // Requires this Moodle version
 $plugin->component = 'tool_task'; // Full name of the plugin (used for diagnostics)
 
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 6e66e8a..ed1d0e6 100644 (file)
@@ -43,7 +43,7 @@
 <br/>
 
 {{#actions}}
-    <a class="btn btn-outline-primary m-r-1 m-b-1 btn-insight" href="{{url}}">{{text}}</a><br/><br/>
+    <a class="btn btn-outline-primary mr-3 mb-3 btn-insight" href="{{url}}">{{text}}</a><br/><br/>
 {{/actions}}
 
 {{#usefulbuttons}}
index 5c2fe8c..13da0f4 100644 (file)
@@ -603,9 +603,12 @@ class auth_plugin_db extends auth_plugin_base {
             }
         }
         if (!empty($update)) {
-            $authdb->Execute("UPDATE {$this->config->table}
-                                 SET ".implode(',', $update)."
-                               WHERE {$this->config->fielduser}='".$this->ext_addslashes($extusername)."'");
+            $sql = "UPDATE {$this->config->table}
+                       SET ".implode(',', $update)."
+                     WHERE {$this->config->fielduser} = ?";
+            if (!$authdb->Execute($sql, array($this->ext_addslashes($extusername)))) {
+                print_error('auth_dbupdateerror', 'auth_db');
+            }
         }
         $authdb->Close();
         return true;
index d3f8a83..75c5c26 100644 (file)
@@ -74,5 +74,6 @@ $string['auth_dbcannotconnect'] = 'Cannot connect to external database.';
 $string['auth_dbcannotreadtable'] = 'Cannot read external table.';
 $string['auth_dbtableempty'] = 'External table is empty.';
 $string['auth_dbcolumnlist'] = 'External table contains the following columns:<br />{$a}';
+$string['auth_dbupdateerror'] = 'Error updating external database.';
 $string['pluginname'] = 'External database';
 $string['privacy:metadata'] = 'The External database authentication plugin does not store any personal data.';
index 17ee9dd..dced889 100644 (file)
@@ -108,16 +108,19 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         $o['ou']          = 'users';
         ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o);
 
+        $createdusers = array();
         for ($i=1; $i<=5; $i++) {
             $this->create_ldap_user($connection, $topdn, $i);
+            $createdusers[] = 'username' . $i;
         }
 
         // Set up creators group.
+        $assignedroles = array('username1', 'username2');
         $o = array();
         $o['objectClass'] = array('posixGroup');
         $o['cn']          = 'creators';
         $o['gidNumber']   = 1;
-        $o['memberUid']   = array('username1', 'username2');
+        $o['memberUid']   = $assignedroles;
         ldap_add($connection, 'cn='.$o['cn'].','.$topdn, $o);
 
         $creatorrole = $DB->get_record('role', array('shortname'=>'coursecreator'));
@@ -174,15 +177,23 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         // Check events, 5 users created with 2 users having roles.
         $this->assertCount(7, $events);
         foreach ($events as $index => $event) {
-            $usercreatedindex = array(0, 2, 4, 5, 6);
-            $roleassignedindex = array (1, 3);
-            if (in_array($index, $usercreatedindex)) {
-                $this->assertInstanceOf('\core\event\user_created', $event);
-            }
-            if (in_array($index, $roleassignedindex)) {
-                $this->assertInstanceOf('\core\event\role_assigned', $event);
+            $username = $DB->get_field('user', 'username', array('id' => $event->relateduserid)); // Get username.
+
+            if ($event->eventname === '\core\event\user_created') {
+                $this->assertContains($username, $createdusers);
+                unset($events[$index]); // Remove matching event.
+
+            } else if ($event->eventname === '\core\event\role_assigned') {
+                $this->assertContains($username, $assignedroles);
+                unset($events[$index]); // Remove matching event.
+
+            } else {
+                $this->fail('Unexpected event found: ' . $event->eventname);
             }
         }
+        // If all the user_created and role_assigned events have matched
+        // then the $events array should be now empty.
+        $this->assertCount(0, $events);
 
         $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
         $this->assertEquals(2, $DB->count_records('role_assignments'));
index 5ebb83a..8397ac4 100644 (file)
@@ -61,10 +61,10 @@ 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
index 5212364..4d5827f 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js differ
index 67acacb..3c31ebe 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js differ
index 5212364..4d5827f 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js differ
index 77c9cf1..2ccf97c 100644 (file)
@@ -23,7 +23,7 @@ M.availability_completion.form.initInner = function(cms) {
 
 M.availability_completion.form.getNode = function(json) {
     // Create HTML structure.
-    var html = '<span class="col-form-label p-r-1"> ' + M.util.get_string('title', 'availability_completion') + '</span>' +
+    var html = '<span class="col-form-label pr-3"> ' + M.util.get_string('title', 'availability_completion') + '</span>' +
                ' <span class="availability-group form-group"><label>' +
             '<span class="accesshide">' + M.util.get_string('label_cm', 'availability_completion') + ' </span>' +
             '<select class="custom-select" name="cm" title="' + M.util.get_string('label_cm', 'availability_completion') + '">' +
index 71cc687..c4eb06b 100644 (file)
Binary files a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js and b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js differ
index 42a4d65..4235144 100644 (file)
Binary files a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js and b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js differ
index 71cc687..c4eb06b 100644 (file)
Binary files a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js and b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js differ
index 30e36fe..1d1656c 100644 (file)
@@ -27,7 +27,7 @@ M.availability_date.form.initInner = function(html, defaultTime) {
 };
 
 M.availability_date.form.getNode = function(json) {
-    var html = '<span class="col-form-label p-r-1">' +
+    var html = '<span class="col-form-label pr-3">' +
                     M.util.get_string('direction_before', 'availability_date') + '</span> <span class="availability-group">' +
             '<label><span class="accesshide">' + M.util.get_string('direction_label', 'availability_date') + ' </span>' +
             '<select name="direction" class="custom-select">' +
index cc76dd5..8867dcc 100644 (file)
Binary files a/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-debug.js and b/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-debug.js differ
index 9f523aa..0e71a08 100644 (file)
Binary files a/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-min.js and b/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-min.js differ
index cc76dd5..8867dcc 100644 (file)
Binary files a/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form.js and b/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form.js differ
index 1957d9a..4927e6a 100644 (file)
@@ -35,7 +35,7 @@ M.availability_grade.form.getNode = function(json) {
     this.nodesSoFar++;
 
     // Create HTML structure.
-    var html = '<label class="form-group"><span class="p-r-1">' + M.util.get_string('title', 'availability_grade') + '</span> ' +
+    var html = '<label class="form-group"><span class="pr-3">' + M.util.get_string('title', 'availability_grade') + '</span> ' +
             '<span class="availability-group">' +
             '<select name="id" class="custom-select"><option value="0">' + M.util.get_string('choosedots', 'moodle') + '</option>';
     for (var i = 0; i < this.grades.length; i++) {
index e6272f9..0ce2ef3 100644 (file)
Binary files a/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-debug.js and b/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-debug.js differ
index bc86932..9f64223 100644 (file)
Binary files a/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-min.js and b/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-min.js differ
index e6272f9..0ce2ef3 100644 (file)
Binary files a/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form.js and b/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form.js differ
index 6c25361..d89ae08 100644 (file)
@@ -31,7 +31,7 @@ M.availability_group.form.initInner = function(groups) {
 
 M.availability_group.form.getNode = function(json) {
     // Create HTML structure.
-    var html = '<label><span class="p-r-1">' + M.util.get_string('title', 'availability_group') + '</span> ' +
+    var html = '<label><span class="pr-3">' + M.util.get_string('title', 'availability_group') + '</span> ' +
             '<span class="availability-group">' +
             '<select name="id" class="custom-select">' +
             '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>' +
index 6dd1a72..1eb8f26 100644 (file)
Binary files a/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-debug.js and b/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-debug.js differ
index 4430bd6..1980203 100644 (file)
Binary files a/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-min.js and b/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-min.js differ
index 6dd1a72..1eb8f26 100644 (file)
Binary files a/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form.js and b/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form.js differ
index 4152385..f8e596f 100644 (file)
@@ -31,7 +31,7 @@ M.availability_grouping.form.initInner = function(groupings) {
 
 M.availability_grouping.form.getNode = function(json) {
     // Create HTML structure.
-    var html = '<label><span class="p-r-1">' + M.util.get_string('title', 'availability_grouping') + '</span> ' +
+    var html = '<label><span class="pr-3">' + M.util.get_string('title', 'availability_grouping') + '</span> ' +
             '<span class="availability-group">' +
             '<select name="id" class="custom-select">' +
             '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>';
index f47ec11..c4fb5a4 100644 (file)
Binary files a/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-debug.js and b/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-debug.js differ
index 945137a..38bb84f 100644 (file)
Binary files a/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-min.js and b/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-min.js differ
index f47ec11..c4fb5a4 100644 (file)
Binary files a/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form.js and b/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form.js differ
index dcab53f..76f75e4 100644 (file)
@@ -33,7 +33,7 @@ M.availability_profile.form.initInner = function(standardFields, customFields) {
 
 M.availability_profile.form.getNode = function(json) {
     // Create HTML structure.
-    var html = '<span class="availability-group"><label><span class="p-r-1">' +
+    var html = '<span class="availability-group"><label><span class="pr-3">' +
             M.util.get_string('conditiontitle', 'availability_profile') + '</span> ' +
             '<select name="field" class="custom-select">' +
             '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>';
index a98f863..9b61ed9 100644 (file)
Binary files a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js and b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js differ
index d2c2b71..5041b64 100644 (file)
Binary files a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js and b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js differ
index a98f863..9b61ed9 100644 (file)
Binary files a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js and b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js differ
index 9083dae..93a12d9 100644 (file)
@@ -372,7 +372,7 @@ M.core_availability.List = function(json, root, parentRoot) {
             '<option value="|">' + M.util.get_string('listheader_multi_or', 'availability') + '</option></select></label> ' +
             M.util.get_string('listheader_multi_after', 'availability') + '</span></div>' +
             '<div class="availability-children"></div>' +
-            '<div class="availability-none"><span class="p-x-1">' + M.util.get_string('none', 'moodle') + '</span></div>' +
+            '<div class="availability-none"><span class="px-3">' + M.util.get_string('none', 'moodle') + '</span></div>' +
             '<div class="clearfix mt-1"></div>' +
             '<div class="availability-button"></div></div><div class="clearfix"></div></div>');
     if (!root) {
@@ -1130,7 +1130,7 @@ M.core_availability.EyeIcon.prototype.isHidden = function() {
  * @param {M.core_availability.Item|M.core_availability.List} toDelete Thing to delete
  */
 M.core_availability.DeleteIcon = function(toDelete) {
-    this.span = Y.Node.create('<a class="d-inline-block col-form-label availability-delete p-x-1" href="#" title="' +
+    this.span = Y.Node.create('<a class="d-inline-block col-form-label availability-delete px-3" href="#" title="' +
             M.util.get_string('delete', 'moodle') + '" role="button">');
     var img = Y.Node.create('<img src="' + M.util.image_url('t/delete', 'core') +
             '" alt="' + M.util.get_string('delete', 'moodle') + '" />');
index 54fca21..3a57d04 100644 (file)
@@ -160,7 +160,7 @@ abstract class backup implements checksumable {
     /**
      * Usually same than major release zero version, mainly for informative/historic purposes.
      */
-    const RELEASE = '4.0';
+    const RELEASE = '3.10';
 
     /**
      * Cipher to be used in backup and restore operations.
index 223b849..baf3b04 100644 (file)
@@ -122,7 +122,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
         }
 
         // Identify the backup we're dealing with.
-        $backuprelease = floatval($this->get_task()->get_info()->backup_release); // The major version: 2.9, 3.0, ...
+        $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
         $backupbuild = 0;
         preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
         if (!empty($matches[1])) {
@@ -132,7 +132,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
         // On older versions the freeze value has to be converted.
         // We do this from here as it is happening right before the file is read.
         // This only targets the backup files that can contain the legacy freeze.
-        if ($backupbuild > 20150618 && ($backuprelease < 3.0 || $backupbuild < 20160527)) {
+        if ($backupbuild > 20150618 && (version_compare($backuprelease, '3.0', '<') || $backupbuild < 20160527)) {
             $this->rewrite_step_backup_file_for_legacy_freeze($fullpath);
         }
 
@@ -505,8 +505,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
         $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
         preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
         $backupbuild = (int)$matches[1];
-        // The function floatval will return a float even if there is text mixed with the release number.
-        $backuprelease = floatval($this->get_task()->get_info()->backup_release);
+        $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
 
         // Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619).
         if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150619) {
@@ -521,7 +520,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
         // Courses from before 3.1 (20160518) may have a letter boundary problem and should be checked for this issue.
         // Backups from before and including 2.9 could have a build number that is greater than 20160518 and should
         // be checked for this problem.
-        if (!$gradebookcalculationsfreeze && ($backupbuild < 20160518 || $backuprelease <= 2.9)) {
+        if (!$gradebookcalculationsfreeze && ($backupbuild < 20160518 || version_compare($backuprelease, '2.9', '<='))) {
             require_once($CFG->libdir . '/db/upgradelib.php');
             upgrade_course_letter_boundary($this->get_courseid());
         }
@@ -4631,11 +4630,11 @@ class restore_create_categories_and_questions extends restore_structure_step {
 
         // Before 3.5, question categories could be created at top level.
         // From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
-        $backuprelease = floatval($this->get_task()->get_info()->backup_release);
+        $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
         preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
         $backupbuild = (int)$matches[1];
         $before35 = false;
-        if ($backuprelease < 3.5 || $backupbuild < 20180205) {
+        if (version_compare($backuprelease, '3.5', '<') || $backupbuild < 20180205) {
             $before35 = true;
         }
         if (empty($mapping->info->parent) && $before35) {
@@ -4892,11 +4891,11 @@ class restore_move_module_questions_categories extends restore_execution_step {
     protected function define_execution() {
         global $DB;
 
-        $backuprelease = floatval($this->task->get_info()->backup_release);
+        $backuprelease = $this->task->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
         preg_match('/(\d{8})/', $this->task->get_info()->moodle_release, $matches);
         $backupbuild = (int)$matches[1];
         $after35 = false;
-        if ($backuprelease >= 3.5 && $backupbuild > 20180205) {
+        if (version_compare($backuprelease, '3.5', '>=') && $backupbuild > 20180205) {
             $after35 = true;
         }
 
index eef73fc..a5ca451 100644 (file)
@@ -581,11 +581,11 @@ abstract class restore_dbops {
         $rc = restore_controller_dbops::load_controller($restoreid);
         $restoreinfo = $rc->get_info();
         $rc->destroy(); // Always need to destroy.
-        $backuprelease = floatval($restoreinfo->backup_release);
+        $backuprelease = $restoreinfo->backup_release; // The major version: 2.9, 3.0, 3.10...
         preg_match('/(\d{8})/', $restoreinfo->moodle_release, $matches);
         $backupbuild = (int)$matches[1];
         $after35 = false;
-        if ($backuprelease >= 3.5 && $backupbuild > 20180205) {
+        if (version_compare($backuprelease, '3.5', '>=') && $backupbuild > 20180205) {
             $after35 = true;
         }
 
index 523403b..6626b58 100644 (file)
@@ -70,7 +70,7 @@ class badge extends moodleform {
         $mform->addRule('description', null, 'required');
 
         $str = $action == 'new' ? get_string('badgeimage', 'badges') : get_string('newimage', 'badges');
-        $imageoptions = array('maxbytes' => 262144, 'accepted_types' => array('web_image'));
+        $imageoptions = array('maxbytes' => 262144, 'accepted_types' => array('optimised_image'));
         $mform->addElement('filepicker', 'image', $str, null, $imageoptions);
 
         if ($action == 'new') {
index f92febd..e5c495c 100644 (file)
@@ -80,16 +80,16 @@ class external_backpack extends \moodleform {
         $issuercontact = $CFG->badges_defaultissuercontact;
         $mform->addElement('static', 'issuerinfo', get_string('defaultissuercontact', 'core_badges'), $issuercontact);
 
-        if ($backpack && $backpack->apiversion != OPEN_BADGES_V2P1) {
-            $mform->addElement('passwordunmask', 'password', get_string('defaultissuerpassword', 'core_badges'));
-            $mform->setType('password', PARAM_RAW);
-            $mform->addHelpButton('password', 'defaultissuerpassword', 'badges');
-            $mform->hideIf('password', 'apiversion', 'eq', 1);
-        } else {
-            $oauth2options = badges_get_oauth2_service_options();
-            $mform->addElement('select', 'oauth2_issuerid', get_string('oauth2issuer', 'core_badges'), $oauth2options);
-            $mform->setType('oauth2_issuerid', PARAM_INT);
-        }
+        $mform->addElement('passwordunmask', 'password', get_string('defaultissuerpassword', 'core_badges'));
+        $mform->setType('password', PARAM_RAW);
+        $mform->addHelpButton('password', 'defaultissuerpassword', 'badges');
+        $mform->hideIf('password', 'apiversion', 'neq', 2);
+
+        $oauth2options = badges_get_oauth2_service_options();
+        $mform->addElement('select', 'oauth2_issuerid', get_string('oauth2issuer', 'core_badges'), $oauth2options);
+        $mform->setType('oauth2_issuerid', PARAM_INT);
+        $mform->hideIf('oauth2_issuerid', 'apiversion', 'neq', '2.1');
+
         if ($backpack) {
             $this->set_data($backpack);
         }
index 28da925..32c38db 100644 (file)
@@ -67,11 +67,6 @@ class external_backpacks_page implements \renderable {
         foreach ($this->backpacks as $backpack) {
             $exporter = new backpack_exporter($backpack);
             $backpack = $exporter->export($output);
-            if ($backpack->apiversion == OPEN_BADGES_V2 || $backpack->apiversion == OPEN_BADGES_V2P1) {
-                $backpack->canedit = true;
-            } else {
-                $backpack->canedit = false;
-            }
             $backpack->cantest = ($backpack->apiversion == OPEN_BADGES_V2);
             $backpack->iscurrent = ($backpack->id == $CFG->badges_site_backpack);
 
index f268bd7..28a182b 100644 (file)
@@ -626,7 +626,7 @@ class core_badges_renderer extends plugin_renderer_base {
                     get_string('downloadall'), 'POST', array('class' => 'activatebadge'));
         $downloadall = $this->output->box('', 'col-md-3');
         $downloadall .= $this->output->box($actionhtml, 'col-md-9');
-        $downloadall = $this->output->box($downloadall, 'row m-l-2');
+        $downloadall = $this->output->box($downloadall, 'row ml-5');
 
         // Local badges.
         $localhtml = html_writer::start_tag('div', array('id' => 'issued-badge-table', 'class' => 'generalbox'));
@@ -674,7 +674,7 @@ class core_badges_renderer extends plugin_renderer_base {
             $backpacksettings = html_writer::link(new moodle_url('/badges/mybackpack.php'), $label, $attr);
             $actionshtml = $this->output->box('', 'col-md-3');
             $actionshtml .= $this->output->box($backpacksettings, 'col-md-9');
-            $actionshtml = $this->output->box($actionshtml, 'row m-l-2');
+            $actionshtml = $this->output->box($actionshtml, 'row ml-5');
             $externalhtml .= $actionshtml;
         }
 
index 0f9df01..bfdcc5e 100644 (file)
@@ -25,7 +25,7 @@
     Example context (json):
     {
         "backpacks": [
-            {"backpackweburl": "http://localhost/", "sitebackpack": true, "canedit": false, "cantest": true}
+            {"backpackweburl": "http://localhost/", "sitebackpack": true, "cantest": true}
         ]
     }
 }}
@@ -50,9 +50,7 @@
             <td> {{{backpackweburl}}} </td>
             <td> {{#sitebackpack}}Yes{{/sitebackpack}} </td>
             <td>
-            {{#canedit}}
                 <a href="{{baseurl}}?id={{id}}&action=edit">{{#pix}}t/edit, core,{{#str}}editsettings{{/str}}{{/pix}}</a>
-            {{/canedit}}
             {{^iscurrent}}
                 <a href="{{baseurl}}?id={{id}}&action=delete" role="button" data-action="deletebackpack">
                     {{#pix}}t/delete, core,{{#str}}delete{{/str}}{{/pix}}
index 67376f0..ea02aa7 100644 (file)
@@ -107,7 +107,8 @@ Feature: Backpack badges
     And I set the field "backpackweburl" to "http://backpackweburl.cat"
     And I press "Save changes"
     Then I should see "http://backpackweburl.cat"
-    And "Delete" "button" should exist
+    And "Delete" "icon" should exist in the "http://backpackweburl.cat" "table_row"
+    And "Edit settings" "icon" should exist in the "http://backpackweburl.cat" "table_row"
 
   @javascript
   Scenario: Remove a site backpack
index f32e7e8..06a35f6 100644 (file)
@@ -41,7 +41,7 @@
     <li class="list-group-item course-listitem"
         data-region="course-content"
         data-course-id="{{{id}}}">
-        <div class="row-fluid">
+        <div class="row">
             <div class="{{#hasprogress}}col-md-6{{/hasprogress}}{{^hasprogress}}col-md-11 col-md-11{{/hasprogress}} d-flex align-items-center">
                 <div>
                     <div class="text-muted muted d-flex flex-wrap">
@@ -72,7 +72,7 @@
                     </a>
                     {{^visible}}
                         <div class="d-flex flex-wrap">
-                            <span class="tag tag-info">{{#str}} hiddenfromstudents {{/str}}</span>
+                            <span class="badge badge-info">{{#str}} hiddenfromstudents {{/str}}</span>
                         </div>
                     {{/visible}}
                 </div>
index 02dd284..aecd376 100644 (file)
                 </div>
                 {{^visible}}
                     <div class="d-flex flex-wrap">
-                        <span class="tag tag-info">{{#str}} hiddenfromstudents {{/str}}</span>
+                        <span class="badge badge-info">{{#str}} hiddenfromstudents {{/str}}</span>
                     </div>
                 {{/visible}}
                 <div class="summary">
                     <span class="sr-only">{{#str}}aria:coursesummary, block_myoverview{{/str}}</span>
                     {{{summary}}}
                 </div>
-                <div class="ml-auto mt-auto w-50 p-t-1">
+                <div class="ml-auto mt-auto w-50 pt-3">
                     {{#hasprogress}}
                         {{> block_myoverview/progress-bar}}
                     {{/hasprogress}}
index 54f17c8..9154041 100644 (file)
Binary files a/blocks/recentlyaccessedcourses/amd/build/main.min.js and b/blocks/recentlyaccessedcourses/amd/build/main.min.js differ
index 7d74c0f..c2da9f5 100644 (file)
Binary files a/blocks/recentlyaccessedcourses/amd/build/main.min.js.map and b/blocks/recentlyaccessedcourses/amd/build/main.min.js.map differ
index 9af85cb..423eeb0 100644 (file)
@@ -210,6 +210,11 @@ define(
                 start = start >= 0 ? start : 0;
             }
 
+            // At least show one card.
+            if (availableVisibleCards === 0) {
+                availableVisibleCards = 1;
+            }
+
             var coursesToShow = allCourses.slice(start, start + availableVisibleCards);
             // Create an id for the list of courses we expect to be displayed.
             var newVisibleCoursesId = coursesToShow.reduce(function(carry, course) {
index 067f8eb..cb0555a 100644 (file)
@@ -43,7 +43,7 @@
         "datepublished": "12 January 2016, 9:12 pm"
     }
 }}
-<li class="p-y-1">
+<li class="py-3">
     {{$title}}
         <div class="link">
             <a href="{{{link}}}" onclick='this.target="_blank"'>{{title}}</a>
index ce96132..a6ceef3 100644 (file)
@@ -57,7 +57,7 @@
             data-midnight="{{midnight}}"
             data-limit="2"
             data-offset="0"
-            data-days-limit="30"
+            data-days-limit="{{dayslimit}}"
             data-days-offset="0"
             data-no-events-url="{{urls.noevents}}"
             id="view_courses_{{uniqid}}"
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 98ad278..80ddbf0 100644 (file)
@@ -86,6 +86,9 @@ class week_day_exporter extends day_exporter {
                 'type' => PARAM_RAW,
                 'default' => '',
             ],
+            'daytitle' => [
+                'type' => PARAM_RAW,
+            ]
         ]);
 
         return $return;
@@ -104,6 +107,8 @@ class week_day_exporter extends day_exporter {
             $return['popovertitle'] = $popovertitle;
         }
 
+        $return['daytitle'] = $this->get_day_title();
+
         return $return;
     }
 
@@ -141,4 +146,24 @@ class week_day_exporter extends day_exporter {
 
         return $title;
     }
+
+    /**
+     * Get the title for this day.
+     *
+     * @return string
+     */
+    protected function get_day_title(): string {
+        $userdate = userdate($this->data[0], get_string('strftimedayshort'));
+
+        $numevents = count($this->related['events']);
+        if ($numevents == 1) {
+            $title = get_string('dayeventsone', 'calendar', $userdate);
+        } else if ($numevents) {
+            $title = get_string('dayeventsmany', 'calendar', ['num' => $numevents, 'day' => $userdate]);
+        } else {
+            $title = get_string('dayeventsnone', 'calendar', $userdate);
+        }
+
+        return $title;
+    }
 }
index 6f30790..3920e03 100644 (file)
@@ -51,7 +51,7 @@
     }} data-type="event"{{!
     }} data-course-id="{{course.id}}"{{!
     }} data-event-id="{{id}}"{{!
-    }} class="event m-t-1"{{!
+    }} class="event mt-3"{{!
     }} data-event-component="{{component}}"{{!
     }} data-event-eventtype="{{eventtype}}"{{!
     }} data-eventtype-{{normalisedeventtype}}="1"{{!
index a88f65e..ba01dfe 100644 (file)
@@ -46,8 +46,9 @@
         <thead>
             <tr>
                 {{# daynames }}
-                <th class="header text-xs-center" aria-label="{{fullname}}">
-                    {{shortname}}
+                <th class="header text-xs-center">
+                    <span class="sr-only">{{fullname}}</span>
+                    <span aria-hidden="true">{{shortname}}</span>
                 </th>
                 {{/ daynames }}
             </tr>
@@ -71,6 +72,7 @@
                         data-region="day"
                         data-new-event-timestamp="{{neweventtimestamp}}">
                         <div class="d-none d-md-block hidden-phone text-xs-center">
+                            <span class="sr-only">{{daytitle}}</span>
                             {{#hasevents}}
                                 <a data-action="view-day-link" href="#" class="aalink day" aria-label="{{viewdaylinktitle}}"
                                     data-year="{{date.year}}" data-month="{{date.mon}}" data-day="{{mday}}"
@@ -78,7 +80,7 @@
                                     data-timestamp="{{timestamp}}">{{mday}}</a>
                             {{/hasevents}}
                             {{^hasevents}}
-                                {{mday}}
+                                <span aria-hidden="true">{{mday}}</span>
                             {{/hasevents}}
                             {{#hasevents}}
                                 <div data-region="day-content">
                             {{/hasevents}}
                         </div>
                         <div class="d-md-none hidden-desktop hidden-tablet">
+                            <span class="sr-only">{{daytitle}}</span>
                             {{#hasevents}}
                                 <a data-action="view-day-link" href="#" class="day aalink" aria-label="{{viewdaylinktitle}}"
                                     data-year="{{date.year}}" data-month="{{date.mon}}" data-day="{{mday}}"
                                     data-timestamp="{{timestamp}}">{{mday}}</a>
                             {{/hasevents}}
                             {{^hasevents}}
-                                    {{mday}}
+                                <span aria-hidden="true">{{mday}}</span>
                             {{/hasevents}}
                         </div>
                     </td>
index ff10a74..4ff2656 100644 (file)
@@ -78,8 +78,9 @@
         <thead>
           <tr>
                 {{# daynames }}
-                <th class="header text-xs-center" scope="col" aria-label="{{fullname}}">
-                    {{shortname}}
+                <th class="header text-xs-center">
+                    <span class="sr-only">{{fullname}}</span>
+                    <span aria-hidden="true">{{shortname}}</span>
                 </th>
                 {{/ daynames }}
             </tr>
                         This is the timestamp for this month.
                         }} data-day-timestamp="{{timestamp}}"{{!
                     }}>{{!
-                        }}{{#popovertitle}}
+                        }}<span class="sr-only">{{daytitle}}</span>
+                        {{#popovertitle}}
                             {{< core_calendar/minicalendar_day_link }}
                                 {{$day}}{{mday}}{{/day}}
                                 {{$url}}{{viewdaylink}}{{/url}}
                             {{/ core_calendar/minicalendar_day_link }}
                         {{/popovertitle}}{{!
                         }}{{^popovertitle}}
-                            {{mday}}
+                            <span aria-hidden="true">{{mday}}</span>
                         {{/popovertitle}}{{!
                     }}</td>
                 {{/days}}
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 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 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 237c664..0f2b538 100644 (file)
Binary files a/contentbank/amd/build/actions.min.js and b/contentbank/amd/build/actions.min.js differ
index 79956ea..ee0807e 100644 (file)
Binary files a/contentbank/amd/build/actions.min.js.map and b/contentbank/amd/build/actions.min.js.map differ
index 35b476a..26786ec 100644 (file)
Binary files a/contentbank/amd/build/selectors.min.js and b/contentbank/amd/build/selectors.min.js differ
index 36d959f..2efa6a5 100644 (file)
Binary files a/contentbank/amd/build/selectors.min.js.map and b/contentbank/amd/build/selectors.min.js.map differ
index d8222ca..2fd3317 100644 (file)
Binary files a/contentbank/amd/build/sort.min.js and b/contentbank/amd/build/sort.min.js differ
index dae9e77..a1b8557 100644 (file)
Binary files a/contentbank/amd/build/sort.min.js.map and b/contentbank/amd/build/sort.min.js.map differ
index 1776292..176b127 100644 (file)
@@ -139,10 +139,26 @@ function($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents)
                 });
             }).then(function(modal) {
                 modal.setSaveButtonText(saveButtonText);
-                modal.getRoot().on(ModalEvents.save, function() {
+                modal.getRoot().on(ModalEvents.save, function(e) {
                     // The action is now confirmed, sending an action for it.
-                    var newname = $("#newname").val();
-                    return renameContent(contentid, newname);
+                    var newname = $("#newname").val().trim();
+                    if (newname) {
+                        renameContent(contentid, newname);
+                    } else {
+                        var errorStrings = [
+                            {
+                                key: 'error',
+                            },
+                            {
+                                key: 'emptynamenotallowed',
+                                component: 'core_contentbank',
+                            },
+                        ];
+                        Str.get_strings(errorStrings).then(function(langStrings) {
+                            Notification.alert(langStrings[0], langStrings[1]);
+                        }).catch(Notification.exception);
+                        e.preventDefault();
+                    }
                 });
 
                 // Handle hidden event.
@@ -211,11 +227,11 @@ function($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents)
         };
         var requestType = 'success';
         Ajax.call([request])[0].then(function(data) {
-            if (data) {
+            if (data.result) {
                 return 'contentrenamed';
             }
             requestType = 'error';
-            return 'contentnotrenamed';
+            return data.warnings[0].message;
 
         }).then(function(message) {
             var params = null;
index b8ca6a6..60fe955 100644 (file)
@@ -48,7 +48,8 @@ export default {
         sortname: getDataSelector('action', 'sortname'),
         sortdate: getDataSelector('action', 'sortdate'),
         sortsize: getDataSelector('action', 'sortsize'),
-        sorttype: getDataSelector('action', 'sorttype')
+        sorttype: getDataSelector('action', 'sorttype'),
+        sortauthor: getDataSelector('action', 'sortauthor'),
     },
     elements: {
         listitem: '.cb-listitem',
index 94c1a24..34c282b 100644 (file)
@@ -35,7 +35,7 @@ import Notification from 'core/notification';
  */
 export const init = () => {
     const contentBank = document.querySelector(selectors.regions.contentbank);
-    Prefetch.prefetchStrings('contentbank', ['contentname', 'lastmodified', 'size', 'type']);
+    Prefetch.prefetchStrings('contentbank', ['contentname', 'lastmodified', 'size', 'type', 'author']);
     Prefetch.prefetchStrings('moodle', ['sortbyx', 'sortbyxreverse']);
     registerListenerEvents(contentBank);
 };
@@ -93,12 +93,19 @@ const registerListenerEvents = (contentBank) => {
         updateSortOrder(fileArea, shownItems, 'data-bytes', ascending);
     });
 
-    // Sort by type
+    // Sort by type.
     const sortByType = contentBank.querySelector(selectors.actions.sorttype);
     sortByType.addEventListener('click', () => {
         const ascending = updateSortButtons(contentBank, sortByType);
         updateSortOrder(fileArea, shownItems, 'data-type', ascending);
     });
+
+    // Sort by author.
+    const sortByAuthor = contentBank.querySelector(selectors.actions.sortauthor);
+    sortByAuthor.addEventListener('click', () => {
+        const ascending = updateSortButtons(contentBank, sortByAuthor);
+        updateSortOrder(fileArea, shownItems, 'data-author', ascending);
+    });
 };
 
 
index b8a9d40..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.
      *
@@ -127,6 +139,7 @@ abstract class content {
      * @throws \coding_exception if not loaded.
      */
     public function set_name(string $name): bool {
+        $name = trim($name);
         if (empty($name)) {
             return false;
         }
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 18d36f3..5cd9d75 100644 (file)
@@ -87,15 +87,24 @@ class rename_content extends external_api {
                 $content = new $contentclass($record);
                 // Check capability.
                 if ($contenttype->can_manage($content)) {
-                    // This content can be renamed.
-                    if ($contenttype->rename_content($content, $params['name'])) {
-                        $result = true;
-                    } else {
+                    if (empty(trim($name))) {
+                        // If name is empty don't try to rename and return a more detailed message.
                         $warnings[] = [
                             'item' => $contentid,
-                            'warningcode' => 'contentnotrenamed',
-                            'message' => get_string('contentnotrenamed', 'core_contentbank')
+                            'warningcode' => 'emptynamenotallowed',
+                            'message' => get_string('emptynamenotallowed', 'core_contentbank')
                         ];
+                    } else {
+                        // This content can be renamed.
+                        if ($contenttype->rename_content($content, $params['name'])) {
+                            $result = true;
+                        } else {
+                            $warnings[] = [
+                                'item' => $contentid,
+                                'warningcode' => 'contentnotrenamed',
+                                'message' => get_string('contentnotrenamed', 'core_contentbank')
+                            ];
+                        }
                     }
                 } else {
                     // The user has no permission to manage this content.
index 5549392..6c799df 100644 (file)
@@ -86,6 +86,7 @@ class bankcontent implements renderable, templatable {
             $contenttypeclass = $content->get_content_type().'\\contenttype';
             $contenttype = new $contenttypeclass($this->context);
             $name = $content->get_name();
+            $author = \core_user::get_user($content->get_content()->usercreated);
             $contentdata[] = array(
                 'name' => $name,
                 'title' => strtolower($name),
@@ -94,7 +95,8 @@ class bankcontent implements renderable, templatable {
                 'timemodified' => $content->get_timemodified(),
                 'bytes' => $filesize,
                 'size' => display_size($filesize),
-                'type' => $mimetype
+                'type' => $mimetype,
+                'author' => fullname($author),
             );
         }
         $data->viewlist = get_user_preferences('core_contentbank_view_list');
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');
index 4801a7f..a62beff 100644 (file)
@@ -27,6 +27,7 @@
                 "size": "699.3KB",
                 "bytes": 716126,
                 "type": "Archive (H5P)",
+                "author": "Admin user",
                 "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",
                 "icon" : "http://something/theme/image.php/boost/core/1581597850/f/h5p-64"
             },
@@ -37,6 +38,7 @@
                 "size": "699.3KB",
                 "bytes": 716126,
                 "type": "Archive (PDF)",
+                "author": "Admin user",
                 "icon": "http://something/theme/image.php/boost/core/1584597850/f/pdf-64"
             }
         ],
@@ -122,10 +124,19 @@ data-region="contentbank">
                             <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
                         </button>
                     </div>
-                    <div class="cb-type cb-column d-flex last">
+                    <div class="cb-type cb-column d-flex">
                         <div class="title">{{#str}} type, contentbank {{/str}}</div>
                         <button class="btn btn-sm cb-btnsort dir-none ml-auto" data-string="type" data-action="sorttype"
-                        title="{{#str}} sortbyx, core, {{#str}} size, contentbank {{/str}} {{/str}}">
+                        title="{{#str}} sortbyx, core, {{#str}} type, contentbank {{/str}} {{/str}}">
+                            <span class="default">{{#pix}} t/sort, core, {{#str}}sort, core {{/str}} {{/pix}}</span>
+                            <span class="desc">{{#pix}} t/sort_desc, core, {{#str}}desc, core{{/str}} {{/pix}}</span>
+                            <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
+                        </button>
+                    </div>
+                    <div class="cb-author cb-column d-flex last">
+                        <div class="title">{{#str}} author, contentbank {{/str}}</div>
+                        <button class="btn btn-sm cb-btnsort dir-none ml-auto" data-string="author" data-action="sortauthor"
+                        title="{{#str}} sortbyx, core, {{#str}} author, contentbank {{/str}} {{/str}}">
                             <span class="default">{{#pix}} t/sort, core, {{#str}}sort, core {{/str}} {{/pix}}</span>
                             <span class="desc">{{#pix}} t/sort_desc, core, {{#str}}desc, core{{/str}} {{/pix}}</span>
                             <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
@@ -138,7 +149,8 @@ data-region="contentbank">
                     data-name="{{{ name }}}"
                     data-bytes="{{ bytes }}"
                     data-timemodified="{{ timemodified }}"
-                    data-type="{{{ type }}}">
+                    data-type="{{{ type }}}"
+                    data-author="{{{ author }}}">
                     <div class="cb-file cb-column position-relative">
                         <div class="cb-thumbnail" role="img" aria-label="{{{ name }}}"
                         style="background-image: url('{{{ icon }}}');">
@@ -155,9 +167,12 @@ data-region="contentbank">
                     <div class="cb-size cb-column small">
                         {{ size }}
                     </div>
-                    <div class="cb-type cb-column last small">
+                    <div class="cb-type cb-column small">
                         {{{ type }}}
                     </div>
+                    <div class="cb-type cb-column last small">
+                        {{{ author }}}
+                    </div>
                 </div>
             {{/contents}}
             </div>
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 6abcf95..242c227 100644 (file)
@@ -5,14 +5,20 @@ Feature: Sort content in the content bank
   I need to be able to sort the content bank in various ways
 
   Background:
-    Given the following "contentbank content" exist:
-        | contextlevel | reference | contenttype       | user  | contentname          |
-        | System       |           | contenttype_h5p   | admin | Dragon_santjordi.h5p |
-        | System       |           | contenttype_h5p   | admin | mathsbook.h5p        |
-        | System       |           | contenttype_h5p   | admin | historybook.h5p      |
-        | System       |           | contenttype_h5p   | admin | santjordi.h5p        |
-        | System       |           | contenttype_h5p   | admin | santjordi_rose.h5p   |
-        | System       |           | contenttype_h5p   | admin | SantJordi_book       |
+    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          |
+        | System       |           | contenttype_h5p   | admin   | Dragon_santjordi.h5p |
+        | System       |           | contenttype_h5p   | admin   | mathsbook.h5p        |
+        | System       |           | contenttype_h5p   | manager | historybook.h5p      |
+        | System       |           | contenttype_h5p   | admin   | santjordi.h5p        |
+        | System       |           | contenttype_h5p   | admin   | santjordi_rose.h5p   |
+        | System       |           | contenttype_h5p   | admin   | SantJordi_book       |
 
   Scenario: Admins can order content in the content bank
     Given I log in as "admin"
@@ -30,3 +36,18 @@ Feature: Sort content in the content bank
     And "historybook.h5p" "text" should appear before "Dragon_santjordi.h5p" "text"
     And "mathsbook.h5p" "text" should appear before "historybook.h5p" "text"
     Then "santjordi_rose.h5p" "text" should appear before "SantJordi_book" "text"
+
+  Scenario: Admins can order content depending on the author
+    Given 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 expand "Site pages" node
+    And I click on "Content bank" "link"
+    When I click on "Display content bank with file details" "button"
+    Then I click on "Sort by Author ascending" "button"
+    And "Dragon_santjordi.h5p" "text" should appear before "historybook.h5p" "text"
+    And "santjordi_rose.h5p" "text" should appear before "historybook" "text"
+    And I click on "Sort by Author descending" "button"
+    And "historybook.h5p" "text" should appear before "Dragon_santjordi.h5p" "text"
+    And "historybook.h5p" "text" should appear before "santjordi_rose" "text"
index b0bfede..4c7ec2b 100644 (file)
@@ -81,7 +81,9 @@ class core_contenttype_content_testcase extends \advanced_testcase {
             'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'],
             'Name with tags' => ['This is <b>bold</b>', 'This is bold'],
             'Long name' => [str_repeat('a', 100), str_repeat('a', 100)],
-            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)]
+            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)],
+            'Empty name' => ['', 'Old name'],
+            'Blanks only' => ['  ', 'Old name'],
         ];
     }
 
@@ -273,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 1fee5be..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);
     }
 
     /**
@@ -375,12 +456,14 @@ class core_contenttype_contenttype_testcase extends \advanced_testcase {
      */
     public function rename_content_provider() {
         return [
-            'Standard name' => ['New name', 'New name'],
-            'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017'],
-            'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'],
-            'Name with tags' => ['This is <b>bold</b>', 'This is bold'],
-            'Long name' => [str_repeat('a', 100), str_repeat('a', 100)],
-            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)]
+            'Standard name' => ['New name', 'New name', true],
+            'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017', true],
+            'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle', true],
+            'Name with tags' => ['This is <b>bold</b>', 'This is bold', true],
+            'Long name' => [str_repeat('a', 100), str_repeat('a', 100), true],
+            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255), true],
+            'Empty name' => ['', 'Test content ', false],
+            'Blanks only' => ['  ', 'Test content ', false],
         ];
     }
 
@@ -390,10 +473,11 @@ class core_contenttype_contenttype_testcase extends \advanced_testcase {
      * @dataProvider    rename_content_provider
      * @param   string  $newname    The name to set
      * @param   string   $expected   The name result
+     * @param   bool   $result   The bolean result expected when renaming
      *
      * @covers ::rename_content
      */
-    public function test_rename_content(string $newname, string $expected) {
+    public function test_rename_content(string $newname, string $expected, bool $result) {
         global $DB;
 
         $this->resetAfterTest();
@@ -414,9 +498,8 @@ class core_contenttype_contenttype_testcase extends \advanced_testcase {
 
         // Check the content is renamed as expected by a user with permission.
         $renamed = $contenttype->rename_content($content, $newname);
-        $this->assertTrue($renamed);
+        $this->assertEquals($result, $renamed);
         $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
-        $this->assertNotEquals($oldname, $record->name);
         $this->assertEquals($expected, $record->name);
     }
 
@@ -508,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 6a9ea67..d369bb2 100644 (file)
@@ -52,12 +52,14 @@ class rename_content_testcase extends \externallib_advanced_testcase {
      */
     public function rename_content_provider() {
         return [
-            'Standard name' => ['New name', 'New name'],
-            'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017'],
-            'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'],
-            'Name with tags' => ['This is <b>bold</b>', 'This is bold'],
-            'Long name' => [str_repeat('a', 100), str_repeat('a', 100)],
-            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)]
+            'Standard name' => ['New name', 'New name', true],
+            'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017', true],
+            'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle', true],
+            'Name with tags' => ['This is <b>bold</b>', 'This is bold', true],
+            'Long name' => [str_repeat('a', 100), str_repeat('a', 100), true],
+            'Too long name' => [str_repeat('a', 300), str_repeat('a', 255), true],
+            'Empty name' => ['', 'Test content ', false],
+            'Blanks only' => ['  ', 'Test content ', false],
         ];
     }
 
@@ -66,11 +68,12 @@ class rename_content_testcase extends \externallib_advanced_testcase {
      *
      * @dataProvider    rename_content_provider
      * @param   string  $newname    The name to set
-     * @param   string   $expected   The name result
+     * @param   string   $expectedname   The name result
+     * @param   bool   $expectedresult   The bolean result expected when renaming
      *
      * @covers ::execute
      */
-    public function test_rename_content_with_permission(string $newname, string $expected) {
+    public function test_rename_content_with_permission(string $newname, string $expectedname, bool $expectedresult) {
         global $DB;
         $this->resetAfterTest();
 
@@ -91,10 +94,9 @@ class rename_content_testcase extends \externallib_advanced_testcase {
         // Call the WS and check the content is renamed as expected.
         $result = rename_content::execute($content->get_id(), $newname);
         $result = external_api::clean_returnvalue(rename_content::execute_returns(), $result);
-        $this->assertTrue($result['result']);
+        $this->assertEquals($expectedresult, $result['result']);
         $record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
-        $this->assertNotEquals($oldname, $record->name);
-        $this->assertEquals($expected, $record->name);
+        $this->assertEquals($expectedname, $record->name);
 
         // Call the WS using an unexisting contentid and check an error is thrown.
         $this->expectException(\invalid_response_exception::class);
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 2e58af3..f11d28c 100644 (file)
@@ -75,6 +75,7 @@ class core_course_editcategory_form extends moodleform {
 
         $mform->addElement('editor', 'description_editor', get_string('description'), null,
             $this->get_description_editor_options());
+        $mform->setType('description_editor', PARAM_RAW);
 
         if (!empty($CFG->allowcategorythemes)) {
             $themes = array(''=>get_string('forceno'));
@@ -105,7 +106,8 @@ class core_course_editcategory_form extends moodleform {
         return array(
             'maxfiles'  => EDITOR_UNLIMITED_FILES,
             'maxbytes'  => $CFG->maxbytes,
-            'trusttext' => true,
+            'trusttext' => false,
+            'noclean'   => true,
             'context'   => $context,
             'subdirs'   => file_area_contains_subdirs($context, 'coursecat', 'description', $itemid),
         );
index 74fa987..5a896e7 100644 (file)
@@ -4381,15 +4381,10 @@ class core_course_external extends external_api {
         $coursecontext = context_course::instance($courseid);
         self::validate_context($coursecontext);
 
-        $pluginswithfunction = get_plugins_with_function('custom_chooser_footer', 'lib.php');
-        if ($pluginswithfunction) {
-            foreach ($pluginswithfunction as $plugintype => $plugins) {
-                foreach ($plugins as $pluginfunction) {
-                    $footerdata = $pluginfunction($courseid, $sectionid);
-                    break; // Only a single plugin can modify the footer.
-                }
-                break; // Only a single plugin can modify the footer.
-            }
+        $activeplugin = get_config('core', 'activitychooseractivefooter');
+
+        if ($activeplugin !== COURSE_CHOOSER_FOOTER_NONE) {
+            $footerdata = component_callback($activeplugin, 'custom_chooser_footer', [$courseid, $sectionid]);
             return [
                 'footer' => true,
                 'customfooterjs' => $footerdata->get_footer_js_file(),
index 076b99f..4fbe668 100644 (file)
@@ -48,8 +48,8 @@ class restore_format_topics_plugin extends restore_format_plugin {
      */
     protected function need_restore_numsections() {
         $backupinfo = $this->step->get_task()->get_info();
-        $backuprelease = $backupinfo->backup_release;
-        return version_compare($backuprelease, '3.3', 'lt');
+        $backuprelease = $backupinfo->backup_release; // The major version: 2.9, 3.0, 3.10...
+        return version_compare($backuprelease, '3.3', '<');
     }
 
     /**
index e0ec9c8..64e3b33 100644 (file)
@@ -48,8 +48,8 @@ class restore_format_weeks_plugin extends restore_format_plugin {
      */
     protected function is_pre_33_backup() {
         $backupinfo = $this->step->get_task()->get_info();
-        $backuprelease = $backupinfo->backup_release;
-        return version_compare($backuprelease, '3.3', 'lt');
+        $backuprelease = $backupinfo->backup_release; // The major version: 2.9, 3.0, 3.10...
+        return version_compare($backuprelease, '3.3', '<');
     }
 
     /**
index 7408941..f8bd189 100644 (file)
@@ -68,6 +68,9 @@ define('COURSE_DB_QUERY_LIMIT', 1000);
 /** Searching for all courses that have no value for the specified custom field. */
 define('COURSE_CUSTOMFIELD_EMPTY', -1);
 
+// Course activity chooser footer default display option.
+define('COURSE_CHOOSER_FOOTER_NONE', 'hidden');
+
 function make_log_url($module, $url) {
     switch ($module) {
         case 'course':
index 3b51f67..64232e2 100644 (file)
@@ -33,7 +33,7 @@
     }
 }}
 {{#activities}}
-<div class="row mb-1 row-fluid">
+<div class="row mb-1">
     <div class="activityinstance col-6">
         <div class="mod-indent-outer"></div>
         <div>
@@ -48,7 +48,7 @@
         </div>
     </div>
     <div class="activity-completionstatus col-6" id="completionstatus_{{cmid}}">
-        <div class="col-sm-1  p-l-0">
+        <div class="col-sm-1  pl-0">
             {{#completionstatus.icon}}
                 {{{completionstatus.icon}}}
             {{/completionstatus.icon}}
@@ -56,7 +56,7 @@
                 <span class="mr-3"></span>
             {{/completionstatus.icon}}
         </div>
-        <div class="col-sm-11  p-l-0">
+        <div class="col-sm-11  pl-0">
             <span class="text-muted muted">{{{completionstatus.string}}}</span>
         </div>
     </div>
index 4a28026..8043be9 100644 (file)
@@ -62,7 +62,7 @@
     <div class="topics">
         {{#sections}}
                 <div class="mb-1">
-                    <div class="row mb-1 row-fluid">
+                    <div class="row mb-1">
                         <div class="col-sm-12">
                             <input type="checkbox" data-section-master="{{sectionnumber}}" class="mr-1" aria-label="{{#str}}checkallsection, completion, {{{name}}}{{/str}}">
                             <h3 class="d-inline-block">{{{name}}}</h3>
index b24636c..5e6c7fb 100644 (file)
@@ -66,7 +66,7 @@
                 </a>
                 {{^visible}}
                     <div class="d-flex flex-wrap">
-                        <span class="tag tag-info">{{#str}} hiddenfromstudents {{/str}}</span>
+                        <span class="badge badge-info">{{#str}} hiddenfromstudents {{/str}}</span>
                     </div>
                 {{/visible}}
             </div>
index dd2b39f..f76da9f 100644 (file)
@@ -60,7 +60,7 @@
         {{#modules}}
             {{#canmanage}}
              <div class="mb-1">
-                <div class="row mb-1 row-fluid">
+                <div class="row mb-1">
                     <div class="col-6">
                         <label class="accesshide" for="modtype_{{id}}">{{#str}}select, core_completion{{/str}} {{formattedname}}</label>
                         <input id="modtype_{{id}}" type="checkbox" class="mr-1" name="modids[]" value="{{id}}" aria-label="{{#str}}checkactivity, completion, {{{formattedname}}}{{/str}}">
@@ -68,7 +68,7 @@
                         <span>{{{formattedname}}}</span>
                     </div>
                     <div class="activity-completionstatus col-6">
-                        <div class="col-sm-1 p-l-0">
+                        <div class="col-sm-1 pl-0">
                             {{#completionstatus.icon}}
                                 {{{completionstatus.icon}}}
                             {{/completionstatus.icon}}
@@ -76,7 +76,7 @@
                                 <span class="mr-3"></span>
                             {{/completionstatus.icon}}
                         </div>
-                        <div class="col-sm-11 p-l-0">
+                        <div class="col-sm-11 pl-0">
                             <span class="text-muted muted">{{{completionstatus.string}}}</span>
                         </div>
                     </div>
@@ -127,4 +127,4 @@ require([
         }
     });
 });
-{{/js}}
\ No newline at end of file
+{{/js}}
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 082cef6..312eef2 100644 (file)
         <?php
         if (has_capability('moodle/grade:manageoutcomes', $context)) {
         ?>
-        <td class="p-l-1 p-r-1">
-            <div class="m-y-1">
+        <td class="pl-3 pr-3">
+            <div class="my-3">
                 <input name="add" class="btn btn-secondary" id="add" type="submit" value="<?php echo $OUTPUT->larrow() . ' ' .
                     get_string('add'); ?>" title="<?php print_string('add'); ?>" />
             </div>
-            <div class="m-y-1">
+            <div class="my-3">
                 <input name="remove" class="btn btn-secondary" id="remove" type="submit" value="<?php echo get_string('remove') .
                     ' ' . $OUTPUT->rarrow(); ?>" title="<?php print_string('remove'); ?>" />
             </div>
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 da5f866..4988d2a 100644 (file)
@@ -165,6 +165,7 @@ class gradereport_user_external extends external_api {
                 'courseid'      => $course->id,
                 'userid'        => $user->id,
                 'userfullname'  => fullname($user),
+                'useridnumber'  => $user->idnumber,
                 'maxdepth'      => $report->maxdepth,
             );
             if ($tabledata) {
@@ -191,6 +192,7 @@ class gradereport_user_external extends external_api {
                     'courseid'      => $course->id,
                     'userid'        => $currentuser->id,
                     'userfullname'  => fullname($currentuser),
+                    'useridnumber'  => $currentuser->idnumber,
                     'maxdepth'      => $report->maxdepth,
                 );
                 if ($tabledata) {
@@ -478,6 +480,7 @@ class gradereport_user_external extends external_api {
                             'courseid' => new external_value(PARAM_INT, 'course id'),
                             'userid'   => new external_value(PARAM_INT, 'user id'),
                             'userfullname' => new external_value(PARAM_TEXT, 'user fullname'),
+                            'useridnumber' => new external_value(PARAM_TEXT, 'user idnumber'),
                             'maxdepth'   => new external_value(PARAM_INT, 'table max depth (needed for printing it)'),
                             'gradeitems' => new external_