Compare commits
368 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ec02c2a67 | |||
| 8a709503ac | |||
| 2c7f5745c6 | |||
| 9a18af46d7 | |||
| e09160b6db | |||
| 884a9b9706 | |||
| 753b2fb53e | |||
| 7806835569 | |||
| a937101b06 | |||
| 8596b6a299 | |||
| b5c8188961 | |||
| ed3ee9dd5c | |||
| 87aaffc3d1 | |||
| 4fae57fc97 | |||
| 9b9237fde1 | |||
| 6f3450ef1e | |||
| 089f5bb0ec | |||
| 0391a4afc7 | |||
| 0b26fce0b6 | |||
| 361070ef5d | |||
| 81cb18a67c | |||
| f4b92ca799 | |||
| e563d32ca2 | |||
| 2575add511 | |||
| 61b3c4b453 | |||
| d82d2bd6d6 | |||
| a7a8db38aa | |||
| a4e45e8159 | |||
| 814c80da13 | |||
| f7ffb41db4 | |||
| c9efe1b1ac | |||
| 82c845d8d3 | |||
| 30da75a460 | |||
| fdb7aa8035 | |||
| 4e14e2631c | |||
| 2ae147c9af | |||
| 6cacc0a4a3 | |||
| c66c1a04b5 | |||
| 06001d9c9d | |||
| 9f01ded64e | |||
| 7bd088e443 | |||
| f67020d1c2 | |||
| 2bc1e50886 | |||
| a2fe75a270 | |||
| 6b632fef09 | |||
| bc3e140d8b | |||
| b53f12d8c9 | |||
| f9c700eb9f | |||
| 0d3495e6b2 | |||
| 72eac30148 | |||
| e19f451723 | |||
| db0073291b | |||
| 984e3fc510 | |||
| 698142b1f5 | |||
| 8669b0ebb7 | |||
| 2204ea8afe | |||
| 142e12bee7 | |||
| 2bdebbf6e7 | |||
| f47ec5d0a9 | |||
| 52da2d6eaa | |||
| 78b0f6e204 | |||
| 7030fa5972 | |||
| 2bd53efc1b | |||
| 9ccb3a2af9 | |||
| 3354f46fd6 | |||
| 9fb1e3d300 | |||
| e4b11bf852 | |||
| 28cf3f9119 | |||
| 1a885a1756 | |||
| 753035e090 | |||
| cb98541563 | |||
| 2729301721 | |||
| 54a2d7df22 | |||
| 59f97a627f | |||
| 362c67f84d | |||
| cbe5ec5a51 | |||
| 201eff4d98 | |||
| 22e52ebb55 | |||
| 493d3c218e | |||
| 97ff875c83 | |||
| 8a5601722d | |||
| 68aa19ba73 | |||
| c8d12c3217 | |||
| f9d08701fb | |||
| 324e1fbb78 | |||
| b58a4e8da4 | |||
| e7c0ddb942 | |||
| e9d63ac29c | |||
| fce440eeed | |||
| 5fae2fc7a1 | |||
| 913eb31830 | |||
| 68fbcaf303 | |||
| 20403e0e43 | |||
| 2740e06920 | |||
| a84b51046a | |||
| a19092e71c | |||
| b41a748a98 | |||
| cd96cb300e | |||
| e3c031023f | |||
| c5b4da7257 | |||
| 7edc33a8ce | |||
| 61e9318339 | |||
| f254b2b88f | |||
| 264fe69743 | |||
| 80e2f8473e | |||
| b9b5f4b489 | |||
| dc871f251e | |||
| f972485484 | |||
| 1f9488ccb5 | |||
| 1d8512bc0e | |||
| b751583087 | |||
| 94b897a176 | |||
| 4c6b771c2f | |||
| b8122e1de3 | |||
| a9eae7edf4 | |||
| a7a9150b62 | |||
| c283ed04e0 | |||
| f65e706662 | |||
| 49e1731fbe | |||
| cd320b32b5 | |||
| 8dea153c6b | |||
| ae9fea0b47 | |||
| dc9258574d | |||
| 5b94aaed06 | |||
| a099c8d921 | |||
| 36a3c41b8b | |||
| d7208f8dd0 | |||
| df9c7f755d | |||
| a76583296b | |||
| b56affcfe6 | |||
| 4799f8b7c4 | |||
| 62758b8224 | |||
| cf6aeae427 | |||
| 8ea5b30a5d | |||
| 270f52bf55 | |||
| 1affa30299 | |||
| f61b2edde0 | |||
| d1b2674eea | |||
| 849f392f3b | |||
| 2c73122c60 | |||
| 3b6104d6dc | |||
| be2eb84be9 | |||
| 711b1a5ae9 | |||
| df136f7535 | |||
| 6c996b1570 | |||
| f01179355c | |||
| 414102ed74 | |||
| 24cd545eb8 | |||
| bcf349ed45 | |||
| ae221af121 | |||
| 4f87b28436 | |||
| b3a66b1484 | |||
| 52b5aa5a18 | |||
| de6a44d7d0 | |||
| 21941c6c2a | |||
| 54da1506a1 | |||
| 7f3a1dff28 | |||
| 818182879a | |||
| e995073997 | |||
| bc47266a65 | |||
| 799e988f80 | |||
| 9bae0ce6a7 | |||
| d6a633d7a8 | |||
| 16dff14e56 | |||
| c0d26fff0d | |||
| 3698f34fc3 | |||
| e52ec9edd4 | |||
| 63b4929729 | |||
| 3afbffe6a0 | |||
| 1dd574efd8 | |||
| a426320c54 | |||
| 3dea860e64 | |||
| 69f3e0753e | |||
| 690bad040a | |||
| 22c30c160e | |||
| b28b6066b4 | |||
| c4e993d546 | |||
| 5b7d2a1992 | |||
| 23846c5450 | |||
| b6d65c77cf | |||
| f77ec5e7fd | |||
| 9031cf789d | |||
| 010aa5ee16 | |||
| 2014c08636 | |||
| f5531458aa | |||
| a6e2d950e7 | |||
| dddc4cc410 | |||
| 2e16fca0f1 | |||
| 30d1832f45 | |||
| 24c44b38be | |||
| d848c604dd | |||
| 9a4034f3bf | |||
| 4c1d448b5b | |||
| f92e83fe36 | |||
| d500b5c62d | |||
| 65fad5f93f | |||
| 7ea8dc7112 | |||
| 276476233d | |||
| a565c5d93d | |||
| a72e1f1e0d | |||
| 2d86d87012 | |||
| 27ea958dfe | |||
| 2410df1fd2 | |||
| ccb1a3b39f | |||
| ce2058c4c2 | |||
| b7fb146cca | |||
| 2695a394fe | |||
| 05e9ae07cd | |||
| c14603ecd1 | |||
| 5d2d5bc542 | |||
| 3d92acac8a | |||
| ac115326f9 | |||
| cb94a54412 | |||
| 0c2e30809a | |||
| a6c9132e73 | |||
| 323276969f | |||
| 18a99edcb9 | |||
| 9d6fff9671 | |||
| c4dad5b5e6 | |||
| 9c4204c96b | |||
| 15da5ba48e | |||
| c7e65064b1 | |||
| 32702452e7 | |||
| 2e1151bbb3 | |||
| 2e7cc5e17c | |||
| 513ac0c086 | |||
| 9f7752d6bc | |||
| 79071cdd87 | |||
| 69962f36d7 | |||
| 76ef9c5d1a | |||
| b67b742e40 | |||
| eece2df651 | |||
| 61ef211525 | |||
| 535fcef31e | |||
| 817757c088 | |||
| 054d5c0f04 | |||
| adcd50bea0 | |||
| 3018a35249 | |||
| 1d217301ad | |||
| 4dc6c7a79d | |||
| 6d6a39e34d | |||
| 1b169c82be | |||
| 91376cdb69 | |||
| 3f5477f8a0 | |||
| 2166e3e681 | |||
| 9e3b15f31e | |||
| dfe187a59d | |||
| 45725c2408 | |||
| 8aea2ef66b | |||
| f065061f08 | |||
| 92df285f49 | |||
| c72839d6d0 | |||
| f33b8bb576 | |||
| 7ccf8f2fe4 | |||
| 0997a89e39 | |||
| f63aed03b8 | |||
| cf3215cff4 | |||
| d26512cb63 | |||
| b4ccc75a03 | |||
| 49f994fb03 | |||
| ebd1eb3726 | |||
| 88d21b999c | |||
| 946cbedf54 | |||
| 6ec958e639 | |||
| 737682c750 | |||
| 138e5b9996 | |||
| f3f7f28015 | |||
| 2bada93f17 | |||
| a5d64155ad | |||
| e511a7fdc4 | |||
| 767c610be1 | |||
| c0f4d9e170 | |||
| e4c7028c20 | |||
| 375ec1c034 | |||
| 1c3b9af49a | |||
| d4d2ef9119 | |||
| fdcc9383b1 | |||
| d1d57dc12d | |||
| 27a6bf394d | |||
| 5e4aab248f | |||
| c32142cf80 | |||
| ea90581cb1 | |||
| 505366bed8 | |||
| a6a5130a65 | |||
| a53238a07b | |||
| e085678bf2 | |||
| ad3ac443a9 | |||
| 81da134c2c | |||
| dc4a36c3b1 | |||
| 946120eeee | |||
| 82d35db47a | |||
| 83c3d04702 | |||
| 16736110d8 | |||
| 22f1a27aa3 | |||
| 30e6bf75dd | |||
| 9c4a0b8490 | |||
| da03cf8972 | |||
| 0b3e3d7c54 | |||
| 1b8ced636c | |||
| 2c73fbfc3d | |||
| 5e3c934691 | |||
| df3efafbcf | |||
| e2f2de4885 | |||
| 5279159083 | |||
| 8392586599 | |||
| 26a0581c1b | |||
| 6037a3dde0 | |||
| 471d1a4b6d | |||
| 3db93ec2a2 | |||
| ff6ac0b2fd | |||
| 57d5be04f8 | |||
| c9d3c97e74 | |||
| a772325afc | |||
| 4c1c1aa6c6 | |||
| 05e79651b6 | |||
| a8c569c368 | |||
| 12afd60ba2 | |||
| c4fb2a2141 | |||
| 97d63b6272 | |||
| 39944aee16 | |||
| 3284f1fe7e | |||
| a96ad13616 | |||
| 3c872b34c6 | |||
| 4699142c9b | |||
| 613e7f8a88 | |||
| b240173b66 | |||
| 74730a2d63 | |||
| 0273fc7731 | |||
| eecb28b386 | |||
| 88f81cf8d1 | |||
| fe177d5dec | |||
| d80644a986 | |||
| bfe2779cea | |||
| 39916b12fd | |||
| b861c1dec7 | |||
| 808a335c90 | |||
| 3b55f2847a | |||
| a2e4730a35 | |||
| 7c2b6ed44f | |||
| 3c4d30bcf4 | |||
| c34c6a75df | |||
| 0464ae85ca | |||
| 3211454a2a | |||
| 06bebd6820 | |||
| 3f0cc3d712 | |||
| f71bfc90b1 | |||
| ae29d57085 | |||
| a2cf272e00 | |||
| 4736877c43 | |||
| aa6c15c975 | |||
| d7d8a654f3 | |||
| 2794943d3c | |||
| 8c62673dc5 | |||
| f7a80e3f2b | |||
| 8f23a60ba8 | |||
| 2370d0c629 | |||
| 20c93ebb6f | |||
| 1720bef450 | |||
| c197d3f14b | |||
| 42934afbed | |||
| a97de58aa7 | |||
| 3784dcc912 | |||
| 6bc25f23fd | |||
| 42631c3e34 | |||
| 4e75827d72 | |||
| 6b06a36646 | |||
| b665e5aa33 | |||
| bab8ea6608 |
+1
-1
@@ -89,4 +89,4 @@ theme/boost/amd/src/bootstrap/toast.js
|
||||
theme/boost/amd/src/bootstrap/tooltip.js
|
||||
theme/boost/amd/src/bootstrap/util.js
|
||||
theme/boost/amd/src/tether.js
|
||||
theme/boost/scss/fontawesome/
|
||||
theme/boost/scss/fontawesome/
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
name: Core
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- master
|
||||
- MOODLE_[0-9]+_STABLE
|
||||
tags-ignore:
|
||||
- v[0-9]+.[0-9]+.[0-9]+*
|
||||
|
||||
env:
|
||||
php: 7.4
|
||||
@@ -49,9 +55,16 @@ jobs:
|
||||
include:
|
||||
- os: ubuntu-18.04
|
||||
php: 7.2
|
||||
# Need to disable both redis and memcached (both are optional) in order to keep igbinary installed.
|
||||
# (there are dependencies that become broken if we just switch the igbinary version).
|
||||
# Need to do this for php 7.2 because of a bug with current default igbinary
|
||||
# versions used by GHA being >= 3.2.2 and <= 3.2.4 (buggy ones).
|
||||
# TODO: MDL-72399 - Revert this patch once igbinary 3.2.5 or up is the default @ GHA.
|
||||
extensions: :redis, :memcached, igbinary-3.2.6
|
||||
db: mysqli
|
||||
- os: ubuntu-18.04
|
||||
php: 7.4
|
||||
extensions:
|
||||
db: pgsql
|
||||
|
||||
steps:
|
||||
@@ -82,6 +95,7 @@ jobs:
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: ${{ matrix.extensions }}
|
||||
coverage: none
|
||||
|
||||
- name: Checking out code from ${{ env.GITHUB_REF_SLUG }}
|
||||
|
||||
@@ -44,3 +44,4 @@ composer.phar
|
||||
/lib/yuilib/*/*/*-coverage.js
|
||||
atlassian-ide-plugin.xml
|
||||
/node_modules/
|
||||
moodle-plugin-ci.phar
|
||||
|
||||
+1
-1
@@ -90,4 +90,4 @@ theme/boost/amd/src/bootstrap/toast.js
|
||||
theme/boost/amd/src/bootstrap/tooltip.js
|
||||
theme/boost/amd/src/bootstrap/util.js
|
||||
theme/boost/amd/src/tether.js
|
||||
theme/boost/scss/fontawesome/
|
||||
theme/boost/scss/fontawesome/
|
||||
|
||||
+7
-1
@@ -10,7 +10,7 @@ language: php
|
||||
|
||||
os: linux
|
||||
|
||||
dist: xenial
|
||||
dist: bionic
|
||||
|
||||
services:
|
||||
- mysql
|
||||
@@ -19,6 +19,12 @@ services:
|
||||
addons:
|
||||
postgresql: "9.6"
|
||||
|
||||
branches:
|
||||
except:
|
||||
- master
|
||||
- /MOODLE_[0-9]+_STABLE/
|
||||
- /^v[0-9]+\.[0-9]+\.[0-9]+.*/
|
||||
|
||||
jobs:
|
||||
# Enable fast finish.
|
||||
# This will fail the build if a single job fails (except those in allow_failures).
|
||||
|
||||
+2
-2
@@ -424,7 +424,7 @@ module.exports = function(grunt) {
|
||||
'*/**/yui/src/*/meta/',
|
||||
'*/**/build/',
|
||||
].concat(thirdPartyPaths);
|
||||
grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
|
||||
grunt.file.write('.eslintignore', eslintIgnores.join('\n') + '\n');
|
||||
|
||||
// Generate .stylelintignore.
|
||||
const stylelintIgnores = [
|
||||
@@ -433,7 +433,7 @@ module.exports = function(grunt) {
|
||||
'theme/boost/style/moodle.css',
|
||||
'theme/classic/style/moodle.css',
|
||||
].concat(thirdPartyPaths);
|
||||
grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
|
||||
grunt.file.write('.stylelintignore', stylelintIgnores.join('\n') + '\n');
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Fix orphaned calendar events that were broken by MDL-67494.
|
||||
*
|
||||
* This script will look for all the calendar events which userids
|
||||
* where broken by a wrong upgrade step, affecting to Moodle 3.9.5
|
||||
* and up.
|
||||
*
|
||||
* It performs checks to both:
|
||||
* a) Detect if the site was affected (ran the wrong upgrade step).
|
||||
* b) Look for orphaned calendar events, categorising them as:
|
||||
* - standard: site / category / course / group / user events
|
||||
* - subscription: events created via subscriptions.
|
||||
* - action: normal action events, created to show common important dates.
|
||||
* - override: user and group override events, particular, that some activities support.
|
||||
* - custom: other events, not being any of the above, common or particular.
|
||||
* By specifying it (--fix) try to recover as many broken events (missing userid) as
|
||||
* possible. Standard, subscription, action, override events in core are fully supported but
|
||||
* override or custom events should be fixed by each plugin as far as there isn't any standard
|
||||
* API (plugin-wise) to launch a rebuild of the calendar events.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2021 onwards Simey Lameze <simey@moodle.com>
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define('CLI_SCRIPT', true);
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->libdir . "/clilib.php");
|
||||
require_once($CFG->libdir . '/db/upgradelib.php');
|
||||
|
||||
// Supported options.
|
||||
$long = ['fix' => false, 'help' => false];
|
||||
$short = ['f' => 'fix', 'h' => 'help'];
|
||||
|
||||
// CLI options.
|
||||
[$options, $unrecognized] = cli_get_params($long, $short);
|
||||
|
||||
if ($unrecognized) {
|
||||
$unrecognized = implode("\n ", $unrecognized);
|
||||
cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
|
||||
}
|
||||
|
||||
if ($options['help']) {
|
||||
$help = <<<EOT
|
||||
Fix orphaned calendar events.
|
||||
|
||||
This script detects calendar events that have had their
|
||||
userid lost. By default it will perform various checks
|
||||
and report them, showing the site status in an easy way.
|
||||
|
||||
Also, optionally (--fix), it wil try to recover as many
|
||||
lost userids as possible from different sources. Note that
|
||||
this script aims to process well-know events in core,
|
||||
leaving custom events in 3rd part plugins mostly unmodified
|
||||
because there isn't any consistent way to regenerate them.
|
||||
|
||||
For more details: https://tracker.moodle.org/browse/MDL-71156
|
||||
|
||||
Options:
|
||||
-h, --help Print out this help.
|
||||
-f, --fix Fix the orphaned calendar events in the DB.
|
||||
If not specified only check and report problems to STDERR.
|
||||
|
||||
Usage:
|
||||
- Only report: \$ sudo -u www-data /usr/bin/php admin/cli/fix_orphaned_calendar_events.php
|
||||
- Report and fix: \$ sudo -u www-data /usr/bin/php admin/cli/fix_orphaned_calendar_events.php -f
|
||||
EOT;
|
||||
|
||||
cli_writeln($help);
|
||||
die;
|
||||
}
|
||||
|
||||
// Check various usual pre-requisites.
|
||||
if (empty($CFG->version)) {
|
||||
cli_error('Database is not yet installed.');
|
||||
}
|
||||
|
||||
$admin = get_admin();
|
||||
if (!$admin) {
|
||||
cli_error('Error: No admin account was found.');
|
||||
}
|
||||
|
||||
if (moodle_needs_upgrading()) {
|
||||
cli_error('Moodle upgrade pending, script execution suspended.');
|
||||
}
|
||||
|
||||
// Do everything as admin by default.
|
||||
\core\session\manager::set_user($admin);
|
||||
|
||||
// Report current site status.
|
||||
cli_heading('Checking the site status');
|
||||
$needsfix = upgrade_calendar_site_status();
|
||||
|
||||
// Report current calendar events status.
|
||||
cli_heading('Checking the calendar events status');
|
||||
$info = upgrade_calendar_events_status();
|
||||
$hasbadevents = $info['total']->bad > 0 || $info['total']->bad != $info['other']->bad;
|
||||
$needsfix = $needsfix || $hasbadevents;
|
||||
|
||||
// If, selected, fix as many calendar events as possible.
|
||||
if ($options['fix']) {
|
||||
|
||||
// If the report has told us that the fix was not needed... ask for confirmation!
|
||||
if (!$needsfix) {
|
||||
cli_writeln("This site DOES NOT NEED to run the calendar events fix.");
|
||||
$input = cli_input('Are you completely sure that you want to run the fix? (y/N)', 'N', ['y', 'Y', 'n', 'N']);
|
||||
if (strtolower($input) != 'y') {
|
||||
exit(0);
|
||||
}
|
||||
cli_writeln("");
|
||||
}
|
||||
cli_heading('Fixing as many as possible calendar events');
|
||||
upgrade_calendar_events_fix_remaining($info);
|
||||
// Report current (after fix) calendar events status.
|
||||
cli_heading('Checking the calendar events status (after fix)');
|
||||
upgrade_calendar_events_status();
|
||||
} else if ($needsfix) {
|
||||
// Fix option was not provided but problem events have been found. Notify the user and provide info how to fix these events.
|
||||
cli_writeln("This site NEEDS to run the calendar events fix!");
|
||||
cli_writeln("To fix the calendar events, re-run this script with the --fix option.");
|
||||
}
|
||||
@@ -119,7 +119,7 @@ if (!empty($options['collation'])) {
|
||||
if ($dbcollation->value !== $collation || $dbcharset->value !== $charset) {
|
||||
// Try to convert the DB.
|
||||
echo "Converting database to '$collation' for $CFG->wwwroot:\n";
|
||||
$sql = "ALTER DATABASE $CFG->dbname DEFAULT CHARACTER SET $charset DEFAULT COLLATE = $collation";
|
||||
$sql = "ALTER DATABASE `$CFG->dbname` DEFAULT CHARACTER SET $charset DEFAULT COLLATE = $collation";
|
||||
try {
|
||||
$DB->change_database_structure($sql);
|
||||
} catch (exception $e) {
|
||||
@@ -146,7 +146,7 @@ if (!empty($options['collation'])) {
|
||||
|
||||
} else {
|
||||
try {
|
||||
$DB->change_database_structure("ALTER TABLE $table->name CONVERT TO CHARACTER SET $charset COLLATE $collation");
|
||||
$DB->change_database_structure("ALTER TABLE `$table->name` CONVERT TO CHARACTER SET $charset COLLATE $collation");
|
||||
echo "CONVERTED\n";
|
||||
$converted++;
|
||||
} catch (ddl_exception $e) {
|
||||
@@ -163,7 +163,7 @@ if (!empty($options['collation'])) {
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "SHOW FULL COLUMNS FROM $table->name WHERE collation IS NOT NULL";
|
||||
$sql = "SHOW FULL COLUMNS FROM `$table->name` WHERE collation IS NOT NULL";
|
||||
$rs2 = $DB->get_recordset_sql($sql);
|
||||
foreach ($rs2 as $column) {
|
||||
$column = (object)array_change_key_case((array)$column, CASE_LOWER);
|
||||
@@ -181,7 +181,7 @@ if (!empty($options['collation'])) {
|
||||
$notnull = ($column->null === 'NO') ? 'NOT NULL' : 'NULL';
|
||||
$default = (!is_null($column->default) and $column->default !== '') ? "DEFAULT '$column->default'" : '';
|
||||
// primary, unique and inc are not supported for texts
|
||||
$sql = "ALTER TABLE $table->name
|
||||
$sql = "ALTER TABLE `$table->name`
|
||||
MODIFY COLUMN $column->field $column->type
|
||||
CHARACTER SET $charset
|
||||
COLLATE $collation $notnull $default";
|
||||
@@ -192,11 +192,11 @@ if (!empty($options['collation'])) {
|
||||
$default = !is_null($column->default) ? "DEFAULT '$column->default'" : '';
|
||||
|
||||
if ($rowformat != '') {
|
||||
$sql = "ALTER TABLE $table->name $rowformat";
|
||||
$sql = "ALTER TABLE `$table->name` $rowformat";
|
||||
$DB->change_database_structure($sql);
|
||||
}
|
||||
|
||||
$sql = "ALTER TABLE $table->name
|
||||
$sql = "ALTER TABLE `$table->name`
|
||||
MODIFY COLUMN $column->field $column->type
|
||||
CHARACTER SET $charset
|
||||
COLLATE $collation $notnull $default";
|
||||
@@ -315,8 +315,8 @@ function mysql_set_row_format($tablename, $charset, $collation, $engine) {
|
||||
if ($rs->row_format == 'Compact' || $rs->row_format == 'Redundant') {
|
||||
$rowformat = $DB->get_row_format_sql($engine, $collation);
|
||||
// Try to convert to compressed format and then try updating the collation again.
|
||||
$DB->change_database_structure("ALTER TABLE $tablename $rowformat");
|
||||
$DB->change_database_structure("ALTER TABLE $tablename CONVERT TO CHARACTER SET $charset COLLATE $collation");
|
||||
$DB->change_database_structure("ALTER TABLE `$tablename` $rowformat");
|
||||
$DB->change_database_structure("ALTER TABLE `$tablename` CONVERT TO CHARACTER SET $charset COLLATE $collation");
|
||||
} else {
|
||||
// Row format may not be the problem. Can not diagnose problem. Send fail reply.
|
||||
return false;
|
||||
|
||||
@@ -158,7 +158,7 @@ if (!empty($options['info'])) {
|
||||
}
|
||||
|
||||
foreach ($fixtables as $table) {
|
||||
$DB->change_database_structure("ALTER TABLE {$prefix}$table ROW_FORMAT=Compressed");
|
||||
$DB->change_database_structure("ALTER TABLE `{$prefix}$table` ROW_FORMAT=Compressed");
|
||||
echo str_pad($prefix . $table, 32, ' ', STR_PAD_RIGHT) . " ... Compressed\n";
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ if (!empty($options['info'])) {
|
||||
echo "SET GLOBAL innodb_file_per_table=1;\n";
|
||||
echo "SET GLOBAL innodb_file_format=Barracuda;\n";
|
||||
foreach ($fixtables as $table) {
|
||||
echo "ALTER TABLE {$prefix}$table ROW_FORMAT=Compressed;\n";
|
||||
echo "ALTER TABLE `{$prefix}$table` ROW_FORMAT=Compressed;\n";
|
||||
}
|
||||
echo "\n";
|
||||
exit(0);
|
||||
|
||||
+198
-1
@@ -3163,6 +3163,11 @@
|
||||
<ON_CHECK message="sixtyfourbitswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_igbinary322_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="igbinary322buggyversion" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="3.10" requires="3.5">
|
||||
@@ -3345,6 +3350,11 @@
|
||||
<ON_CHECK message="sixtyfourbitswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_igbinary322_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="igbinary322buggyversion" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="3.11" requires="3.6">
|
||||
@@ -3447,7 +3457,7 @@
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="optional"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -3527,6 +3537,193 @@
|
||||
<ON_CHECK message="sixtyfourbitswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.0" requires="3.6">
|
||||
<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="10" />
|
||||
<VENDOR name="mssql" version="14.0" />
|
||||
<VENDOR name="oracle" version="11.2" />
|
||||
</DATABASE>
|
||||
<PHP version="7.3.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_EXTENSION name="sodium" level="optional"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
</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_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
@@ -53,11 +53,8 @@ class behat_admin extends behat_base {
|
||||
}
|
||||
|
||||
foreach ($data as $label => $value) {
|
||||
$this->execute('behat_navigation::i_select_from_flat_navigation_drawer', [get_string('administrationsite')]);
|
||||
|
||||
// Search by label.
|
||||
$this->execute('behat_forms::i_set_the_field_to', [get_string('query', 'admin'), $label]);
|
||||
$this->execute("behat_forms::press_button", get_string('search', 'admin'));
|
||||
// Navigate straight to the search results fo rthis label.
|
||||
$this->execute('behat_general::i_visit', ["/admin/search.php?query=" . urlencode($label)]);
|
||||
|
||||
// Admin settings does not use the same DOM structure than other moodle forms
|
||||
// but we also need to use lib/behat/form_field/* to deal with the different moodle form elements.
|
||||
|
||||
@@ -45,7 +45,7 @@ list($options, $unrecognised) = cli_get_params(
|
||||
array(
|
||||
'stop-on-failure' => 0,
|
||||
'verbose' => false,
|
||||
'replace' => false,
|
||||
'replace' => '',
|
||||
'help' => false,
|
||||
'tags' => '',
|
||||
'profile' => '',
|
||||
@@ -70,7 +70,7 @@ $help = "
|
||||
Behat utilities to run behat tests in parallel
|
||||
|
||||
Usage:
|
||||
php run.php [--BEHAT_OPTION=\"value\"] [--feature=\"value\"] [--replace] [--fromrun=value --torun=value] [--help]
|
||||
php run.php [--BEHAT_OPTION=\"value\"] [--feature=\"value\"] [--replace=\"{run}\"] [--fromrun=value --torun=value] [--help]
|
||||
|
||||
Options:
|
||||
--BEHAT_OPTION Any combination of behat option specified in http://behat.readthedocs.org/en/v2.5/guides/6.cli.html
|
||||
@@ -144,9 +144,10 @@ $extraopts = $unrecognised;
|
||||
if ($options['profile']) {
|
||||
$profile = $options['profile'];
|
||||
|
||||
// If profile passed is not set, then exit.
|
||||
// If profile passed is not set, then exit (note we skip if the 'replace' option is found within the 'profile' value).
|
||||
if (!isset($CFG->behat_config[$profile]) && !isset($CFG->behat_profiles[$profile]) &&
|
||||
!(isset($options['replace']) && (strpos($options['profile'], $options['replace']) >= 0 ))) {
|
||||
!($options['replace'] && (strpos($profile, (string) $options['replace']) !== false))) {
|
||||
|
||||
echo "Invalid profile passed: " . $profile . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ Feature: Verify that all form fields values can be get and set
|
||||
And the "Unenrol users" select box should not contain "Baker"
|
||||
And the "Unenrol users" select box should not contain "President, Baker"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I follow "Test this one"
|
||||
And I am on the "Test this one" "wiki activity" page
|
||||
And I press "Create page"
|
||||
# Text (textarea & editor) & Select (multi-select) - Checking "I set the following fields to these values".
|
||||
When I set the following fields to these values:
|
||||
@@ -61,8 +61,7 @@ Feature: Verify that all form fields values can be get and set
|
||||
# Checkbox - Checking "I set the field" and "The field matches value" ticked.
|
||||
And I set the field "Force format" to "1"
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I am on the "Test this one" "wiki activity" page
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I expand all fieldsets
|
||||
And the field "Force format" matches value "1"
|
||||
@@ -70,8 +69,7 @@ Feature: Verify that all form fields values can be get and set
|
||||
# Checkbox - Checking "I set the field" and "The field matches value" unticked.
|
||||
And I set the field "Force format" to ""
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I am on the "Test this one" "wiki activity" page
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I expand all fieldsets
|
||||
And the field "Force format" matches value ""
|
||||
@@ -80,8 +78,7 @@ Feature: Verify that all form fields values can be get and set
|
||||
And I set the following fields to these values:
|
||||
| Force format | 1 |
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I am on the "Test this one" "wiki activity" page
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I expand all fieldsets
|
||||
And the following fields match these values:
|
||||
@@ -92,8 +89,7 @@ Feature: Verify that all form fields values can be get and set
|
||||
And I set the following fields to these values:
|
||||
| Force format | |
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I am on the "Test this one" "wiki activity" page
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I expand all fieldsets
|
||||
And the following fields match these values:
|
||||
@@ -115,20 +111,21 @@ Feature: Verify that all form fields values can be get and set
|
||||
| Wiki name | Test this one baby |
|
||||
| Default format | HTML |
|
||||
And I press "Cancel"
|
||||
And I am on "Course 1" course homepage
|
||||
# Radio - Checking "I set the field" and "the field matches value".
|
||||
And I add a "Choice" to section "1" and I fill the form with:
|
||||
| Choice name | Test choice name |
|
||||
| Description | Test choice description |
|
||||
| Allow choice to be updated | Yes |
|
||||
| Option 1 | one |
|
||||
| Option 2 | two |
|
||||
| Option 3 | three |
|
||||
And I follow "Test choice name"
|
||||
And I set the field "one" to "1"
|
||||
And the following "activity" exists:
|
||||
| activity | choice |
|
||||
| course | C1 |
|
||||
| idnumber | choice1 |
|
||||
| intro | Test choice description |
|
||||
| name | Test choice name |
|
||||
| option | Option 1, Option 2, Option 3 |
|
||||
| section | 1 |
|
||||
| allowupdate | 1 |
|
||||
And I am on the "Test choice name" "choice activity" page
|
||||
And I set the field "Option 1" to "1"
|
||||
And I press "Save my choice"
|
||||
And the field "one" matches value "1"
|
||||
And the field "two" matches value ""
|
||||
And the field "Option 1" matches value "1"
|
||||
And the field "Option 2" matches value ""
|
||||
# Check if field xpath set/match works.
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
@@ -174,31 +171,23 @@ Feature: Verify that all form fields values can be get and set
|
||||
And I press "Cancel"
|
||||
# Advanced checkbox requires real browser to allow uncheck to work. MDL-58681. MDL-55386.
|
||||
# Advanced checkbox - Checking "I set the field" and "The field matches value" ticked.
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Test this one"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test choice name" "choice activity editing" page
|
||||
And I set the field "Display description on course page" to "1"
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test choice name" "choice activity editing" page
|
||||
And the field "Display description on course page" matches value "1"
|
||||
And the field "Display description on course page" does not match value ""
|
||||
# Advanced checkbox - Checking "I set the field" and "The field matches value" unticked.
|
||||
And I set the field "Display description on course page" to ""
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test choice name" "choice activity editing" page
|
||||
And the field "Display description on course page" matches value ""
|
||||
And the field "Display description on course page" does not match value "1"
|
||||
# Advanced checkbox - Checking "I set the following fields to these values:" and "The following fields match these values" ticked.
|
||||
And I set the following fields to these values:
|
||||
| Display description on course page | 1 |
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test choice name" "choice activity editing" page
|
||||
And the following fields match these values:
|
||||
| Display description on course page | 1 |
|
||||
And the following fields do not match these values:
|
||||
@@ -207,9 +196,7 @@ Feature: Verify that all form fields values can be get and set
|
||||
And I set the following fields to these values:
|
||||
| Display description on course page | |
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test choice name" "choice activity editing" page
|
||||
And the following fields match these values:
|
||||
| Display description on course page | |
|
||||
And the following fields do not match these values:
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
@tool @tool_behat
|
||||
Feature: Use core page resolvers for the I am on the page steps
|
||||
In order to write tests correctly
|
||||
As a developer
|
||||
I need to have steps which take me straight to a page
|
||||
|
||||
Scenario Outline: When I am on an instance
|
||||
Given the following "categories" exist:
|
||||
| name | category | idnumber |
|
||||
| Cat 1 | 0 | CAT1 |
|
||||
And the following "course" exists:
|
||||
| fullname | Economics 101 |
|
||||
| shortname | ECON101 |
|
||||
| idnumber | 2021-econ101 |
|
||||
And the following "activity" exists:
|
||||
| course | ECON101 |
|
||||
| activity | forum |
|
||||
| name | Fundamentals of Economics |
|
||||
| idnumber | fundamentalsofeconomics |
|
||||
And I log in as "admin"
|
||||
When I am on the <identifier> <type> page
|
||||
Then I should see "<shouldsee>"
|
||||
|
||||
Examples:
|
||||
| description | identifier | type | shouldsee |
|
||||
| Course Category idnumber | CAT1 | category | Add a new course |
|
||||
| Course Category name | "Cat 1" | Category | Add a new course |
|
||||
| Course Full name | "Economics 101" | course | Fundamentals of Economics |
|
||||
| Course Short name | ECON101 | COURSE | Fundamentals of Economics |
|
||||
| Course idnumber | "2021-econ101" | Course | Fundamentals of Economics |
|
||||
| Forum idnumber | fundamentalsofeconomics | Activity | Add a new discussion |
|
||||
| Generic activity editing | fundamentalsofeconomics | "Activity editing" | Updating: Forum |
|
||||
| Forum name | "Fundamentals of Economics" | "Forum activity" | Add a new discussion |
|
||||
| Forum name editing | "Fundamentals of Economics" | "Forum activity editing" | Updating: Forum |
|
||||
| Forum name permissions | "Fundamentals of Economics" | "Forum activity permissions" | Permissions in Forum: Fun |
|
||||
| Forum name roles | "Fundamentals of Economics" | "Forum activity roles" | Assign roles in Forum: Fun |
|
||||
|
||||
Scenario Outline: When I am on an instance logged in as
|
||||
Given the following "categories" exist:
|
||||
| name | category | idnumber |
|
||||
| Cat 1 | 0 | CAT1 |
|
||||
And the following "course" exists:
|
||||
| fullname | Economics 101 |
|
||||
| shortname | ECON101 |
|
||||
| idnumber | 2021-econ101 |
|
||||
And the following "activity" exists:
|
||||
| course | ECON101 |
|
||||
| activity | forum |
|
||||
| name | Fundamentals of Economics |
|
||||
| idnumber | fundamentalsofeconomics |
|
||||
When I am on the <identifier> <type> page logged in as admin
|
||||
Then I should see "<shouldsee>"
|
||||
|
||||
Examples:
|
||||
| description | identifier | type | shouldsee |
|
||||
| Course Category idnumber | CAT1 | category | Add a new course |
|
||||
| Course Category name | "Cat 1" | Category | Add a new course |
|
||||
| Course Full name | "Economics 101" | course | Fundamentals of Economics |
|
||||
| Course Short name | ECON101 | COURSE | Fundamentals of Economics |
|
||||
| Course idnumber | "2021-econ101" | Course | Fundamentals of Economics |
|
||||
| Forum idnumber | fundamentalsofeconomics | Activity | Add a new discussion |
|
||||
| Generic activity editing | fundamentalsofeconomics | "Activity editing" | Updating: Forum |
|
||||
| Forum name | "Fundamentals of Economics" | "Forum activity" | Add a new discussion |
|
||||
| Forum name editing | "Fundamentals of Economics" | "Forum activity editing" | Updating: Forum |
|
||||
| Forum name permissions | "Fundamentals of Economics" | "Forum activity permissions" | Permissions in Forum: Fun |
|
||||
| Forum name roles | "Fundamentals of Economics" | "Forum activity roles" | Assign roles in Forum: Fun |
|
||||
|
||||
Scenario Outline: When I am on a named page
|
||||
Given I log in as "admin"
|
||||
When I am on the <identifier> page
|
||||
Then I should see "<shouldsee>"
|
||||
|
||||
Examples:
|
||||
| description | identifier | shouldsee |
|
||||
| Admin page | "Admin notifications" | Check for available updates |
|
||||
| Home page | Homepage | Recently accessed courses |
|
||||
|
||||
Scenario Outline: When I am on a named page logged in as
|
||||
When I am on the <identifier> page logged in as admin
|
||||
Then I should see "<shouldsee>"
|
||||
|
||||
Examples:
|
||||
| description | identifier | shouldsee |
|
||||
| Admin page | "Admin notifications" | Check for available updates |
|
||||
| Home page | Homepage | Recently accessed courses |
|
||||
@@ -189,7 +189,12 @@ class cohort_role_assignments_table extends table_sql {
|
||||
FROM {tool_cohortroles} uca
|
||||
JOIN {user} u ON u.id = uca.userid
|
||||
JOIN {cohort} c ON c.id = uca.cohortid";
|
||||
$params = array();
|
||||
|
||||
// Check if any additional filtering is required.
|
||||
[$sqlwhere, $params] = $this->get_sql_where();
|
||||
if ($sqlwhere) {
|
||||
$sql .= " WHERE {$sqlwhere}";
|
||||
}
|
||||
|
||||
// Add order by if needed.
|
||||
if (!$count && $sqlsort = $this->get_sql_sort()) {
|
||||
|
||||
@@ -86,8 +86,8 @@ if ($removeid) {
|
||||
|
||||
$title = get_string('existingcohortroles', 'tool_cohortroles');
|
||||
echo $output->heading($title);
|
||||
$url = new moodle_url('/admin/tool/cohortroles/index.php');
|
||||
$table = new tool_cohortroles\output\cohort_role_assignments_table(uniqid(), $url);
|
||||
|
||||
$table = new tool_cohortroles\output\cohort_role_assignments_table('cohort-role-assignments', $pageurl);
|
||||
echo $table->out(50, true);
|
||||
|
||||
echo $output->spacer();
|
||||
|
||||
@@ -9,10 +9,8 @@ Feature: Manage data requests
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | John | Doe | s1@example.com |
|
||||
| student2 | Jane | Doe | s2@example.com |
|
||||
And I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
| contactdataprotectionofficer | 1 |
|
||||
And I log out
|
||||
And the following config values are set as admin:
|
||||
| contactdataprotectionofficer | 1 | tool_dataprivacy |
|
||||
|
||||
@javascript
|
||||
Scenario: Marking general enquiries as complete
|
||||
|
||||
@@ -50,6 +50,7 @@ switch ($action) {
|
||||
case 'disable':
|
||||
unset($enabled[$enrol]);
|
||||
set_config('enabled_stores', implode(',', array_keys($enabled)), 'tool_log');
|
||||
add_to_config_log('tool_logstore_visibility', '1', '0', $enrol);
|
||||
break;
|
||||
|
||||
case 'enable':
|
||||
@@ -59,6 +60,7 @@ switch ($action) {
|
||||
$enabled = array_keys($enabled);
|
||||
$enabled[] = $enrol;
|
||||
set_config('enabled_stores', implode(',', $enabled), 'tool_log');
|
||||
add_to_config_log('tool_logstore_visibility', '0', '1', $enrol);
|
||||
break;
|
||||
|
||||
case 'up':
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
@tool_log @report @report_configlog
|
||||
Feature: In a report, admin can see logstore visibility changes
|
||||
|
||||
# Change log stores visibility so the report contains known data.
|
||||
Background:
|
||||
Given I log in as "admin"
|
||||
And the following config values are set as admin:
|
||||
| enabled_stores | logstore_standard | tool_log |
|
||||
|
||||
@javascript
|
||||
Scenario: Display configuration changes report
|
||||
When I navigate to "Plugins > Logging > Manage log stores" in site administration
|
||||
And I click on "Disable" "icon" in the "Standard log" "table_row"
|
||||
And I click on "Enable" "icon" in the "External database log" "table_row"
|
||||
And I navigate to "Reports > Config changes" in site administration
|
||||
Then the following should exist in the "report-configlog-report-table" table:
|
||||
| User | Plugin | Setting | New value | Original value |
|
||||
| Admin User | logstore_standard | tool_logstore_visibility | 0 | 1 |
|
||||
| Admin User | logstore_database | tool_logstore_visibility | 1 | 0 |
|
||||
@@ -37,67 +37,68 @@
|
||||
This template does not have an example context because it includes ajax functionality.
|
||||
}}
|
||||
<div data-region="user-competency-full-info" data-node="user-competency" data-competencyid="{{usercompetency.competencyid}}" data-userid="{{usercompetency.userid}}" data-region-id="{{uniqid}}">
|
||||
<div data-region="competency-summary">
|
||||
{{#competency}}
|
||||
{{> tool_lp/competency_summary }}
|
||||
{{/competency}}
|
||||
<dl>
|
||||
{{#usercompetency}}
|
||||
<dt>{{#str}}reviewstatus, tool_lp{{/str}}</dt>
|
||||
<dd data-region="user-competency-status">{{statusname}}
|
||||
<div data-region="competency-summary">
|
||||
{{#competency}}
|
||||
{{> tool_lp/competency_summary }}
|
||||
{{/competency}}
|
||||
<dl>
|
||||
{{#usercompetency}}
|
||||
<dt>{{#str}}reviewstatus, tool_lp{{/str}}</dt>
|
||||
<dd data-region="user-competency-status">{{statusname}}
|
||||
|
||||
{{#isstatusinreview}}
|
||||
- {{reviewer.fullname}}
|
||||
{{/isstatusinreview}}
|
||||
{{#isstatusinreview}}
|
||||
- {{reviewer.fullname}}
|
||||
{{/isstatusinreview}}
|
||||
|
||||
{{#isrequestreviewallowed}}<button class="btn btn-secondary" data-action="request-review">{{#str}}requestreview, tool_lp{{/str}}</button>{{/isrequestreviewallowed}}
|
||||
{{#iscancelreviewrequestallowed}}<button class="btn btn-secondary" data-action="cancel-review-request">{{#str}}cancelreviewrequest, tool_lp{{/str}}</button>{{/iscancelreviewrequestallowed}}
|
||||
{{#isstartreviewallowed}}<button class="btn btn-secondary" data-action="start-review">{{#str}}startreview, tool_lp{{/str}}</button>{{/isstartreviewallowed}}
|
||||
{{#isstopreviewallowed}}<button class="btn btn-secondary" data-action="stop-review">{{#str}}stopreview, tool_lp{{/str}}</button>{{/isstopreviewallowed}}
|
||||
</dd>
|
||||
<dt>{{#str}}proficient, tool_lp{{/str}}</dt>
|
||||
<dd>
|
||||
<span class="badge {{#proficiency}}badge-success{{/proficiency}}{{^proficiency}}badge-danger{{/proficiency}} float-left">
|
||||
{{proficiencyname}}
|
||||
</span>
|
||||
</dd>
|
||||
<dt>{{#str}}rating, tool_lp{{/str}}</dt>
|
||||
<dd>{{gradename}}
|
||||
{{#cangrade}}
|
||||
<button class="btn btn-secondary" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
|
||||
{{/cangrade}}
|
||||
</dd>
|
||||
{{#js}}
|
||||
require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info', 'tool_lp/user_competency_workflow'], function($, mod, info, UserCompWorkflow) {
|
||||
{{#isrequestreviewallowed}}<button class="btn btn-secondary" data-action="request-review">{{#str}}requestreview, tool_lp{{/str}}</button>{{/isrequestreviewallowed}}
|
||||
{{#iscancelreviewrequestallowed}}<button class="btn btn-secondary" data-action="cancel-review-request">{{#str}}cancelreviewrequest, tool_lp{{/str}}</button>{{/iscancelreviewrequestallowed}}
|
||||
{{#isstartreviewallowed}}<button class="btn btn-secondary" data-action="start-review">{{#str}}startreview, tool_lp{{/str}}</button>{{/isstartreviewallowed}}
|
||||
{{#isstopreviewallowed}}<button class="btn btn-secondary" data-action="stop-review">{{#str}}stopreview, tool_lp{{/str}}</button>{{/isstopreviewallowed}}
|
||||
</dd>
|
||||
<dt>{{#str}}proficient, tool_lp{{/str}}</dt>
|
||||
<dd>
|
||||
<span class="badge {{#proficiency}}badge-success{{/proficiency}}{{^proficiency}}badge-danger{{/proficiency}} float-left">
|
||||
{{proficiencyname}}
|
||||
</span>
|
||||
</dd>
|
||||
<dt>{{#str}}rating, tool_lp{{/str}}</dt>
|
||||
<dd>{{gradename}}
|
||||
{{#cangrade}}
|
||||
<button class="btn btn-secondary" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
|
||||
{{/cangrade}}
|
||||
</dd>
|
||||
{{#js}}
|
||||
require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info', 'tool_lp/user_competency_workflow'], function($, mod, info, UserCompWorkflow) {
|
||||
|
||||
var competencyElement = $('[data-region-id="{{uniqid}}"]');
|
||||
var infoReloader = new info(competencyElement, '{{competency.competency.id}}', '{{user.id}}');
|
||||
var competencyElement = $('[data-region-id="{{uniqid}}"]');
|
||||
var infoReloader = new info(competencyElement, '{{competency.competency.id}}', '{{user.id}}');
|
||||
|
||||
var ucw = new UserCompWorkflow();
|
||||
ucw.registerEvents('[data-region="user-competency-status"]');
|
||||
ucw.on('status-changed', infoReloader.reload.bind(infoReloader));
|
||||
ucw.on('error-occured', infoReloader.reload.bind(infoReloader));
|
||||
var ucw = new UserCompWorkflow();
|
||||
ucw.registerEvents('[data-region="user-competency-status"]');
|
||||
ucw.on('status-changed', infoReloader.reload.bind(infoReloader));
|
||||
ucw.on('error-occured', infoReloader.reload.bind(infoReloader));
|
||||
|
||||
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
|
||||
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
|
||||
});
|
||||
{{/js}}
|
||||
{{/usercompetency}}
|
||||
</dl>
|
||||
{{#commentarea}}
|
||||
{{#canpostorhascomments}}
|
||||
{{>tool_lp/comment_area}}
|
||||
{{/canpostorhascomments}}
|
||||
{{/commentarea}}
|
||||
<dl data-region="evidence-listing">
|
||||
<dt>{{#str}}evidence, tool_lp{{/str}}</dt>
|
||||
<dd>
|
||||
{{#evidence}}
|
||||
{{> tool_lp/evidence_summary }}
|
||||
{{/evidence}}
|
||||
{{^evidence}}
|
||||
<p>{{#str}}noevidence, tool_lp{{/str}}</p>
|
||||
{{/evidence}}
|
||||
</dd>
|
||||
</dl>
|
||||
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
|
||||
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
|
||||
});
|
||||
{{/js}}
|
||||
{{/usercompetency}}
|
||||
</dl>
|
||||
{{#commentarea}}
|
||||
{{#canpostorhascomments}}
|
||||
{{>tool_lp/comment_area}}
|
||||
{{/canpostorhascomments}}
|
||||
{{/commentarea}}
|
||||
<dl data-region="evidence-listing">
|
||||
<dt>{{#str}}evidence, tool_lp{{/str}}</dt>
|
||||
<dd>
|
||||
{{#evidence}}
|
||||
{{> tool_lp/evidence_summary }}
|
||||
{{/evidence}}
|
||||
{{^evidence}}
|
||||
<p>{{#str}}noevidence, tool_lp{{/str}}</p>
|
||||
{{/evidence}}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,9 +24,7 @@ Feature: See the competencies for an activity on the course competencies page.
|
||||
| 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"
|
||||
And I am on the "Course 1" course page logged in as admin
|
||||
And I follow "Competencies"
|
||||
And I press "Add competencies to course"
|
||||
And "Competency picker" "dialogue" should be visible
|
||||
@@ -36,9 +34,7 @@ Feature: See the competencies for an activity on the course competencies page.
|
||||
And "Competency picker" "dialogue" should be visible
|
||||
And I select "Test-Comp2" of the competency tree
|
||||
And I click on "Add" "button" in the "Competency picker" "dialogue"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the PageName1 "page activity editing" page
|
||||
And I follow "Expand all"
|
||||
And I set the field "Course competencies" to "Test-Comp1"
|
||||
And I press "Save and return to course"
|
||||
@@ -46,10 +42,8 @@ Feature: See the competencies for an activity on the course competencies page.
|
||||
|
||||
@javascript
|
||||
Scenario: Go to the competency course competencies page.
|
||||
When I log in as "student1"
|
||||
And I am on site homepage
|
||||
And I follow "Course 1"
|
||||
And I follow "Competencies"
|
||||
Given I am on the "Course 1" course page logged in as student1
|
||||
When I follow "Competencies"
|
||||
Then I should see "Test-Comp1"
|
||||
And I should see "Test-Comp2"
|
||||
And I set the field "Filter competencies by resource or activity" to "PageName1"
|
||||
@@ -64,12 +58,5 @@ Feature: See the competencies for an activity on the course competencies page.
|
||||
|
||||
@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"
|
||||
When I am on the PageName1 "page activity" page logged in as student1
|
||||
Then I should see "Test page content"
|
||||
|
||||
@@ -42,9 +42,7 @@ Feature: Backup user data
|
||||
| Feedback for the response 'True'. | So you think it is true |
|
||||
| Feedback for the response 'False'. | So you think it is false |
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Quiz 1"
|
||||
When I am on the "Quiz 1" "quiz activity" page logged in as student1
|
||||
And I press "Attempt quiz now"
|
||||
And I click on "True" "radio" in the "First question" "question"
|
||||
And I click on "False" "radio" in the "Second question" "question"
|
||||
|
||||
@@ -438,17 +438,7 @@ class manager {
|
||||
$filename = 'tour_export_' . $tour->get_id() . '_' . time() . '.json';
|
||||
|
||||
// Force download.
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
|
||||
header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
|
||||
header('Expires: ' . gmdate('D, d M Y H:i:s', 0) . 'GMT');
|
||||
header('Pragma: no-cache');
|
||||
header('Accept-Ranges: none');
|
||||
header('Content-disposition: attachment; filename=' . $filename);
|
||||
header('Content-length: ' . strlen($exportstring));
|
||||
header('Content-type: text/calendar; charset=utf-8');
|
||||
|
||||
echo $exportstring;
|
||||
die;
|
||||
send_file($exportstring, $filename, 0, 0, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,7 +68,9 @@ if ($dataformat) {
|
||||
$downloadusers = new ArrayObject($SESSION->bulk_users);
|
||||
$iterator = $downloadusers->getIterator();
|
||||
|
||||
\core\dataformat::download_data($filename, $dataformat, $fields, $iterator, function($userid) use ($extrafields, $fields) {
|
||||
\core\dataformat::download_data($filename, $dataformat, $fields, $iterator, function($userid, $supportshtml)
|
||||
use ($extrafields, $fields) {
|
||||
|
||||
global $DB;
|
||||
|
||||
if (!$user = $DB->get_record('user', array('id' => $userid))) {
|
||||
@@ -86,6 +88,8 @@ if ($dataformat) {
|
||||
// We only take the text.
|
||||
if (is_array($user->$field)) {
|
||||
$userprofiledata[$field] = reset($user->$field);
|
||||
} else if ($supportshtml) {
|
||||
$userprofiledata[$field] = s($user->$field);
|
||||
} else {
|
||||
$userprofiledata[$field] = $user->$field;
|
||||
}
|
||||
|
||||
@@ -178,6 +178,33 @@ class prediction {
|
||||
\core\event\prediction_action_started::create($eventdata)->trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the executed actions.
|
||||
*
|
||||
* Actions could be filtered by actionname.
|
||||
*
|
||||
* @param array $actionnamefilter Limit the results obtained to this list of action names.
|
||||
* @param int $userid the user id. Current user by default.
|
||||
* @return array of actions.
|
||||
*/
|
||||
public function get_executed_actions(array $actionnamefilter = null, int $userid = 0): array {
|
||||
global $USER, $DB;
|
||||
|
||||
$conditions[] = "predictionid = :predictionid";
|
||||
$params['predictionid'] = $this->get_prediction_data()->id;
|
||||
if (!$userid) {
|
||||
$userid = $USER->id;
|
||||
}
|
||||
$conditions[] = "userid = :userid";
|
||||
$params['userid'] = $userid;
|
||||
if ($actionnamefilter) {
|
||||
list($actionsql, $actionparams) = $DB->get_in_or_equal($actionnamefilter, SQL_PARAMS_NAMED);
|
||||
$conditions[] = "actionname $actionsql";
|
||||
$params = $params + $actionparams;
|
||||
}
|
||||
return $DB->get_records_select('analytics_prediction_actions', implode(' AND ', $conditions), $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* format_calculations
|
||||
*
|
||||
|
||||
@@ -112,6 +112,86 @@ class analytics_prediction_actions_testcase extends advanced_testcase {
|
||||
$this->assertEquals(2, $DB->count_records('analytics_prediction_actions'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_get_executed_actions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function execute_actions_provider(): array {
|
||||
return [
|
||||
'Empty actions with no filter' => [
|
||||
[],
|
||||
[],
|
||||
0
|
||||
],
|
||||
'Empty actions with filter' => [
|
||||
[],
|
||||
[\core_analytics\prediction::ACTION_FIXED],
|
||||
0
|
||||
],
|
||||
'Multiple actions with no filter' => [
|
||||
[
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
|
||||
],
|
||||
[],
|
||||
3
|
||||
],
|
||||
'Multiple actions applying filter' => [
|
||||
[
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
|
||||
],
|
||||
[\core_analytics\prediction::ACTION_FIXED],
|
||||
2
|
||||
],
|
||||
'Multiple actions not applying filter' => [
|
||||
[
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
|
||||
],
|
||||
[\core_analytics\prediction::ACTION_NOT_APPLICABLE],
|
||||
0
|
||||
],
|
||||
'Multiple actions with multiple filter' => [
|
||||
[
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
|
||||
],
|
||||
[\core_analytics\prediction::ACTION_FIXED, \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED],
|
||||
3
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for get_executed_actions() function.
|
||||
*
|
||||
* @dataProvider execute_actions_provider
|
||||
* @param array $actionstoexecute An array of actions to execute
|
||||
* @param array $actionnamefilter Actions to filter
|
||||
* @param int $returned Number of actions returned
|
||||
*
|
||||
* @covers \core_analytics\prediction::get_executed_actions
|
||||
*/
|
||||
public function test_get_executed_actions(array $actionstoexecute, array $actionnamefilter, int $returned) {
|
||||
|
||||
$this->setUser($this->teacher2);
|
||||
list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
|
||||
$prediction = reset($predictions);
|
||||
$target = $this->model->get_target();
|
||||
foreach($actionstoexecute as $action) {
|
||||
$prediction->action_executed($action, $target);
|
||||
}
|
||||
|
||||
$filteredactions = $prediction->get_executed_actions($actionnamefilter);
|
||||
$this->assertCount($returned, $filteredactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* test_get_predictions
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
This files describes API changes in analytics sub system,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 3.9.7 ===
|
||||
|
||||
* A new function get_executed_actions() has been added to \core_analytics\prediction class
|
||||
to get all (or filtered by action name) executed actions of a prediction
|
||||
|
||||
=== 3.8 ===
|
||||
|
||||
* "Time-splitting method" have been replaced by "Analysis interval" for the language strings that are
|
||||
|
||||
+3
-3
@@ -131,11 +131,11 @@ class auth_plugin_db extends auth_plugin_base {
|
||||
$authdb->Close();
|
||||
|
||||
if ($this->config->passtype === 'plaintext') {
|
||||
return ($fromdb == $extpassword);
|
||||
return ($fromdb === $extpassword);
|
||||
} else if ($this->config->passtype === 'md5') {
|
||||
return (strtolower($fromdb) == md5($extpassword));
|
||||
return (strtolower($fromdb) === md5($extpassword));
|
||||
} else if ($this->config->passtype === 'sha1') {
|
||||
return (strtolower($fromdb) == sha1($extpassword));
|
||||
return (strtolower($fromdb) === sha1($extpassword));
|
||||
} else if ($this->config->passtype === 'saltedcrypt') {
|
||||
return password_verify($extpassword, $fromdb);
|
||||
} else {
|
||||
|
||||
@@ -45,7 +45,7 @@ $string['auth_dbname'] = 'Name of the database itself. Leave empty if using an O
|
||||
$string['auth_dbname_key'] = 'DB name';
|
||||
$string['auth_dbpass'] = 'Password matching the above username';
|
||||
$string['auth_dbpass_key'] = 'Password';
|
||||
$string['auth_dbpasstype'] = '<p>Specify the format that the password field is using.</p> <p>Use \'internal\' if you want the external database to manage usernames and email addresses, but Moodle to manage passwords. If you use \'internal\', you <i>must</i> provide a populated email address field in the external database, and you must execute both admin/cron.php and auth/db/cli/sync_users.php regularly. Moodle will send an email to new users with a temporary password.</p>';
|
||||
$string['auth_dbpasstype'] = '<p>Specify the format that the password field is using.</p> <p>Use \'internal\' if you want the external database to manage usernames and email addresses, but Moodle to manage passwords. If you use \'internal\', you must provide a populated email address field in the external database, and you must enable the \auth_db\task\sync_users scheduled task. Moodle will send an email to new users with a temporary password.</p>';
|
||||
$string['auth_dbpasstype_key'] = 'Password format';
|
||||
$string['auth_dbreviveduser'] = 'Revived user {$a->name} id {$a->id}';
|
||||
$string['auth_dbrevivedusererror'] = 'Error reviving user {$a}';
|
||||
|
||||
@@ -335,12 +335,28 @@ class auth_db_testcase extends advanced_testcase {
|
||||
$DB->update_record('auth_db_users', $user3);
|
||||
$this->assertTrue($auth->user_login('u3', 'heslo'));
|
||||
|
||||
// Test user created to see if the checking happens strictly.
|
||||
$usermd5 = (object)['name' => 'usermd5', 'pass' => '0e462097431906509019562988736854'];
|
||||
$usermd5->id = $DB->insert_record('auth_db_users', $usermd5);
|
||||
|
||||
// md5('240610708') === '0e462097431906509019562988736854'.
|
||||
$this->assertTrue($auth->user_login('usermd5', '240610708'));
|
||||
$this->assertFalse($auth->user_login('usermd5', 'QNKCDZO'));
|
||||
|
||||
set_config('passtype', 'sh1', 'auth_db');
|
||||
$auth->config->passtype = 'sha1';
|
||||
$user3->pass = sha1('heslo');
|
||||
$DB->update_record('auth_db_users', $user3);
|
||||
$this->assertTrue($auth->user_login('u3', 'heslo'));
|
||||
|
||||
// Test user created to see if the checking happens strictly.
|
||||
$usersha1 = (object)['name' => 'usersha1', 'pass' => '0e66507019969427134894567494305185566735'];
|
||||
$usersha1->id = $DB->insert_record('auth_db_users', $usersha1);
|
||||
|
||||
// sha1('aaroZmOk') === '0e66507019969427134894567494305185566735'.
|
||||
$this->assertTrue($auth->user_login('usersha1', 'aaroZmOk'));
|
||||
$this->assertFalse($auth->user_login('usersha1', 'aaK1STfY'));
|
||||
|
||||
set_config('passtype', 'saltedcrypt', 'auth_db');
|
||||
$auth->config->passtype = 'saltedcrypt';
|
||||
$user3->pass = password_hash('heslo', PASSWORD_BCRYPT);
|
||||
|
||||
@@ -75,7 +75,7 @@ $string['auth_ldap_passwdexpire_settings'] = 'LDAP password expiry settings';
|
||||
$string['auth_ldap_preventpassindb'] = 'Select yes to prevent passwords from being stored in Moodle\'s DB.';
|
||||
$string['auth_ldap_preventpassindb_key'] = 'Prevent password caching';
|
||||
$string['auth_ldap_rolecontext'] = '{$a->localname} context';
|
||||
$string['auth_ldap_rolecontext_help'] = 'LDAP context used to select for <i>{$a->localname}</i> mapping. Separate multiple groups with \';\'. Usually something like "cn={$a->shortname},ou=staff,o=myorg".';
|
||||
$string['auth_ldap_rolecontext_help'] = 'LDAP context used to select for <i>{$a->localname}</i> mapping. Separate multiple groups with \';\'. Usually something like "cn={$a->shortname},ou=first-ou-with-role-groups,o=myorg; cn={$a->shortname},ou=second-ou-with-role-groups,o=myorg".';
|
||||
$string['auth_ldap_search_sub'] = 'Search users from subcontexts.';
|
||||
$string['auth_ldap_search_sub_key'] = 'Search subcontexts';
|
||||
$string['auth_ldap_server_settings'] = 'LDAP server settings';
|
||||
|
||||
+1
-3
@@ -716,9 +716,7 @@ class auth_plugin_mnet extends auth_plugin_base {
|
||||
|
||||
foreach($superArray as $subArray) {
|
||||
$subArray = array_values($subArray);
|
||||
$instring = "('".implode("', '",$subArray)."')";
|
||||
$query = "select id, session_id, username from {mnet_session} where username in $instring";
|
||||
$results = $DB->get_records_sql($query);
|
||||
$results = $DB->get_records_list('mnet_session', 'username', $subArray, '', 'id, session_id, username');
|
||||
|
||||
if ($results == false) {
|
||||
// We seem to have a username that breaks our query:
|
||||
|
||||
@@ -93,13 +93,12 @@ class helper {
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
// Get user session from DB.
|
||||
if (session_decode(base64_decode($session->sessdata))) {
|
||||
if (isset($_SESSION['SESSION']) && isset($_SESSION['SESSION']->shibboleth_session_id)) {
|
||||
// If there is a match, kill the session.
|
||||
if ($_SESSION['SESSION']->shibboleth_session_id == trim($spsessionid)) {
|
||||
// Delete this user's sessions.
|
||||
\core\session\manager::kill_user_sessions($session->userid);
|
||||
}
|
||||
$usersession = self::unserializesession(base64_decode($session->sessdata));
|
||||
if (isset($usersession['SESSION']) && isset($usersession['SESSION']->shibboleth_session_id)) {
|
||||
// If there is a match, kill the session.
|
||||
if ($usersession['SESSION']->shibboleth_session_id == trim($spsessionid)) {
|
||||
// Delete this user's sessions.
|
||||
\core\session\manager::kill_user_sessions($session->userid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,11 +112,22 @@ class helper {
|
||||
*/
|
||||
private static function unserializesession($serializedstring) {
|
||||
$variables = array();
|
||||
$a = preg_split("/(\w+)\|/", $serializedstring, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
|
||||
$counta = count($a);
|
||||
for ($i = 0; $i < $counta; $i = $i + 2) {
|
||||
$variables[$a[$i]] = unserialize($a[$i + 1]);
|
||||
|
||||
$index = 0;
|
||||
|
||||
// Find next delimiter after current index. It's key being the characters between those points.
|
||||
while ($delimiterpos = strpos($serializedstring, '|', $index)) {
|
||||
$key = substr($serializedstring, $index, $delimiterpos - $index);
|
||||
|
||||
// Start unserializing immediately after the delimiter. PHP will read as much valid data as possible.
|
||||
$value = unserialize(substr($serializedstring, $delimiterpos + 1),
|
||||
['allowed_classes' => ['stdClass']]);
|
||||
$variables[$key] = $value;
|
||||
|
||||
// Advance index beyond the length of the previously captured serialized value.
|
||||
$index = $delimiterpos + 1 + strlen(serialize($value));
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,8 @@ Feature: Authentication
|
||||
Then I should see "You are not logged in" in the "page-footer" "region"
|
||||
|
||||
Scenario Outline: Checking the display of the Remember username checkbox
|
||||
Given I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
Given the following config values are set as admin:
|
||||
| rememberusername | <settingvalue> |
|
||||
And I log out
|
||||
And I am on homepage
|
||||
When I click on "Log in" "link" in the ".logininfo" "css_element"
|
||||
Then I should <expect> "Remember username"
|
||||
|
||||
@@ -290,9 +290,9 @@ abstract class info {
|
||||
*/
|
||||
protected function warn_about_invalid_availability(\coding_exception $e) {
|
||||
$name = $this->get_thing_name();
|
||||
// If it occurs while building modinfo based on somebody calling $cm->name,
|
||||
// we can't get $cm->name, and this line will cause a warning.
|
||||
$htmlname = @$this->format_info($name, $this->course);
|
||||
$htmlname = $this->format_info($name, $this->course);
|
||||
// Because we call format_info here, likely in the middle of building dynamic data for the
|
||||
// activity, there could be a chance that the name might not be available.
|
||||
if ($htmlname === '') {
|
||||
// So instead use the numbers (cmid) from the tag.
|
||||
$htmlname = preg_replace('~[^0-9]~', '', $name);
|
||||
@@ -739,11 +739,11 @@ abstract class info {
|
||||
$info = preg_replace_callback('~<AVAILABILITY_CMNAME_([0-9]+)/>~',
|
||||
function($matches) use($modinfo, $context) {
|
||||
$cm = $modinfo->get_cm($matches[1]);
|
||||
if ($cm->has_view() and $cm->uservisible) {
|
||||
if ($cm->has_view() and $cm->get_user_visible()) {
|
||||
// Help student by providing a link to the module which is preventing availability.
|
||||
return \html_writer::link($cm->url, format_string($cm->name, true, array('context' => $context)));
|
||||
return \html_writer::link($cm->get_url(), format_string($cm->get_name(), true, ['context' => $context]));
|
||||
} else {
|
||||
return format_string($cm->name, true, array('context' => $context));
|
||||
return format_string($cm->get_name(), true, ['context' => $context]);
|
||||
}
|
||||
}, $info);
|
||||
|
||||
|
||||
@@ -48,10 +48,10 @@ Feature: Confirm that conditions on completion no longer cause a bug
|
||||
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
|
||||
And I set the field with xpath "//div[contains(concat(' ', normalize-space(@class), ' '), ' availability-item ')][preceding-sibling::div]//select[@name='cm']" to "Page2"
|
||||
And I press "Save and return to course"
|
||||
And I should see "Not available unless:" in the ".activity.glossary" "css_element"
|
||||
Then I should see "Not available unless:" in the ".activity.glossary" "css_element"
|
||||
And I should see "The activity Page1 is marked complete" in the ".activity.glossary" "css_element"
|
||||
And I should see "The activity Page2 is marked complete" in the ".activity.glossary" "css_element"
|
||||
And I follow "TestGlossary"
|
||||
|
||||
# Behat will automatically check there is no error on this page.
|
||||
Then I should see "TestGlossary"
|
||||
And I am on the TestGlossary "glossary activity" page
|
||||
And I should see "TestGlossary"
|
||||
|
||||
@@ -86,13 +86,12 @@ Feature: availability_grade
|
||||
And I set the field "Minimum grade percentage (inclusive)" to "10"
|
||||
And I press "Save and return to course"
|
||||
|
||||
And I log out
|
||||
|
||||
# Log in as student without a grade yet.
|
||||
When I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I am on the "A1" "assign activity" page logged in as student1
|
||||
|
||||
# Do the assignment.
|
||||
And I follow "A1"
|
||||
And I click on "Add submission" "button"
|
||||
And I set the field "Online text" to "Q"
|
||||
And I click on "Save changes" "button"
|
||||
@@ -103,14 +102,12 @@ Feature: availability_grade
|
||||
And I should not see "P3" in the "region-main" "region"
|
||||
And I should not see "P4" in the "region-main" "region"
|
||||
And I should see "A1" in the "region-main" "region"
|
||||
And I log out
|
||||
|
||||
# Log back in as teacher.
|
||||
When I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I am on the "A1" "assign activity" page logged in as teacher1
|
||||
|
||||
# Give the assignment 40%.
|
||||
And I follow "A1"
|
||||
And I navigate to "View all submissions" in current page administration
|
||||
# Pick the grade link in the row that has s@example.com in it.
|
||||
And I click on "Grade" "link" in the "s@example.com" "table_row"
|
||||
@@ -118,11 +115,10 @@ Feature: availability_grade
|
||||
And I click on "Save changes" "button"
|
||||
And I press "OK"
|
||||
And I click on "Edit settings" "link"
|
||||
And I log out
|
||||
|
||||
# Log back in as student.
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I am on the "Course 1" course page logged in as student1
|
||||
|
||||
# Check pages are visible.
|
||||
Then I should see "P2" in the "region-main" "region"
|
||||
|
||||
@@ -95,8 +95,7 @@ Feature: availability_profile
|
||||
And I click on "Save and return to course" "button"
|
||||
|
||||
# Edit it again and check the setting still works.
|
||||
When I follow "P1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
When I am on the P1 "page activity editing" page
|
||||
And I expand all fieldsets
|
||||
Then the field "User profile field" matches value "Super field"
|
||||
And the field "Value to compare against" matches value "Bananaman"
|
||||
|
||||
@@ -511,4 +511,33 @@ class info_testcase extends advanced_testcase {
|
||||
sort($result);
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the info_module class when involved in a recursive call to $cm->name.
|
||||
*/
|
||||
public function test_info_recursive_name_call() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create a course and page.
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$page1 = $generator->create_module('page', ['course' => $course->id, 'name' => 'Page1']);
|
||||
|
||||
// Set invalid availability.
|
||||
$DB->set_field('course_modules', 'availability', 'not valid', ['id' => $page1->cmid]);
|
||||
|
||||
// Get the cm_info object.
|
||||
$this->setAdminUser();
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$cm1 = $modinfo->get_cm($page1->cmid);
|
||||
|
||||
// At this point we will generate dynamic data for $cm1, which will cause the debugging
|
||||
// call below.
|
||||
$this->assertEquals('Page1', $cm1->name);
|
||||
|
||||
$this->assertDebuggingCalled('Error processing availability data for ' .
|
||||
'‘Page1’: Invalid availability text');
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+17
-8
@@ -198,8 +198,11 @@ M.core_availability.form = {
|
||||
// If the groupmode and grouping id aren't set, disable it.
|
||||
var groupmode = Y.one('#id_groupmode');
|
||||
var groupingid = Y.one('#id_groupingid');
|
||||
if ((!groupmode || Number(groupmode.get('value')) === 0) &&
|
||||
(!groupingid || Number(groupingid.get('value')) === 0)) {
|
||||
var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1;
|
||||
var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1;
|
||||
|
||||
if ((!groupmode || Number(groupmode.get('value')) === 0 || !groupavailability) &&
|
||||
(!groupingid || Number(groupingid.get('value')) === 0 || !groupingavailability)) {
|
||||
this.restrictByGroup.set('disabled', true);
|
||||
return;
|
||||
}
|
||||
@@ -220,22 +223,28 @@ M.core_availability.form = {
|
||||
e.preventDefault();
|
||||
|
||||
// Add the condition.
|
||||
var groupmode = Y.one('#id_groupmode');
|
||||
var groupingid = Y.one('#id_groupingid');
|
||||
var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1;
|
||||
var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1;
|
||||
|
||||
var newChild;
|
||||
if (groupingid && Number(groupingid.get('value')) !== 0) {
|
||||
if (groupingid && Number(groupingid.get('value')) !== 0 && groupingavailability) {
|
||||
// Add a grouping restriction if one is specified.
|
||||
newChild = new M.core_availability.Item(
|
||||
{type: 'grouping', id: Number(groupingid.get('value'))}, true);
|
||||
} else {
|
||||
} else if (groupmode && groupavailability) {
|
||||
// Otherwise just add a group restriction.
|
||||
newChild = new M.core_availability.Item({type: 'group'}, true);
|
||||
}
|
||||
|
||||
// Refresh HTML.
|
||||
this.rootList.addChild(newChild);
|
||||
this.update();
|
||||
this.rootList.renumber();
|
||||
this.rootList.updateHtml();
|
||||
if (newChild !== null) {
|
||||
this.rootList.addChild(newChild);
|
||||
this.update();
|
||||
this.rootList.renumber();
|
||||
this.rootList.updateHtml();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+3
-3
File diff suppressed because one or more lines are too long
+17
-8
@@ -198,8 +198,11 @@ M.core_availability.form = {
|
||||
// If the groupmode and grouping id aren't set, disable it.
|
||||
var groupmode = Y.one('#id_groupmode');
|
||||
var groupingid = Y.one('#id_groupingid');
|
||||
if ((!groupmode || Number(groupmode.get('value')) === 0) &&
|
||||
(!groupingid || Number(groupingid.get('value')) === 0)) {
|
||||
var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1;
|
||||
var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1;
|
||||
|
||||
if ((!groupmode || Number(groupmode.get('value')) === 0 || !groupavailability) &&
|
||||
(!groupingid || Number(groupingid.get('value')) === 0 || !groupingavailability)) {
|
||||
this.restrictByGroup.set('disabled', true);
|
||||
return;
|
||||
}
|
||||
@@ -220,22 +223,28 @@ M.core_availability.form = {
|
||||
e.preventDefault();
|
||||
|
||||
// Add the condition.
|
||||
var groupmode = Y.one('#id_groupmode');
|
||||
var groupingid = Y.one('#id_groupingid');
|
||||
var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1;
|
||||
var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1;
|
||||
|
||||
var newChild;
|
||||
if (groupingid && Number(groupingid.get('value')) !== 0) {
|
||||
if (groupingid && Number(groupingid.get('value')) !== 0 && groupingavailability) {
|
||||
// Add a grouping restriction if one is specified.
|
||||
newChild = new M.core_availability.Item(
|
||||
{type: 'grouping', id: Number(groupingid.get('value'))}, true);
|
||||
} else {
|
||||
} else if (groupmode && groupavailability) {
|
||||
// Otherwise just add a group restriction.
|
||||
newChild = new M.core_availability.Item({type: 'group'}, true);
|
||||
}
|
||||
|
||||
// Refresh HTML.
|
||||
this.rootList.addChild(newChild);
|
||||
this.update();
|
||||
this.rootList.renumber();
|
||||
this.rootList.updateHtml();
|
||||
if (newChild !== null) {
|
||||
this.rootList.addChild(newChild);
|
||||
this.update();
|
||||
this.rootList.renumber();
|
||||
this.rootList.updateHtml();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Vendored
+17
-8
@@ -196,8 +196,11 @@ M.core_availability.form = {
|
||||
// If the groupmode and grouping id aren't set, disable it.
|
||||
var groupmode = Y.one('#id_groupmode');
|
||||
var groupingid = Y.one('#id_groupingid');
|
||||
if ((!groupmode || Number(groupmode.get('value')) === 0) &&
|
||||
(!groupingid || Number(groupingid.get('value')) === 0)) {
|
||||
var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1;
|
||||
var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1;
|
||||
|
||||
if ((!groupmode || Number(groupmode.get('value')) === 0 || !groupavailability) &&
|
||||
(!groupingid || Number(groupingid.get('value')) === 0 || !groupingavailability)) {
|
||||
this.restrictByGroup.set('disabled', true);
|
||||
return;
|
||||
}
|
||||
@@ -218,22 +221,28 @@ M.core_availability.form = {
|
||||
e.preventDefault();
|
||||
|
||||
// Add the condition.
|
||||
var groupmode = Y.one('#id_groupmode');
|
||||
var groupingid = Y.one('#id_groupingid');
|
||||
var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1;
|
||||
var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1;
|
||||
|
||||
var newChild;
|
||||
if (groupingid && Number(groupingid.get('value')) !== 0) {
|
||||
if (groupingid && Number(groupingid.get('value')) !== 0 && groupingavailability) {
|
||||
// Add a grouping restriction if one is specified.
|
||||
newChild = new M.core_availability.Item(
|
||||
{type: 'grouping', id: Number(groupingid.get('value'))}, true);
|
||||
} else {
|
||||
} else if (groupmode && groupavailability) {
|
||||
// Otherwise just add a group restriction.
|
||||
newChild = new M.core_availability.Item({type: 'group'}, true);
|
||||
}
|
||||
|
||||
// Refresh HTML.
|
||||
this.rootList.addChild(newChild);
|
||||
this.update();
|
||||
this.rootList.renumber();
|
||||
this.rootList.updateHtml();
|
||||
if (newChild !== null) {
|
||||
this.rootList.addChild(newChild);
|
||||
this.update();
|
||||
this.rootList.renumber();
|
||||
this.rootList.updateHtml();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -358,6 +358,15 @@ class backup_controller extends base_controller {
|
||||
return $this->plan;
|
||||
}
|
||||
|
||||
/**
|
||||
* For debug only. Get a simple test display of all the settings.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function debug_display_all_settings_values(): string {
|
||||
return $this->get_plan()->debug_display_all_settings_values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user roles that should be kept in the destination course
|
||||
* for a course copy operation.
|
||||
|
||||
@@ -350,6 +350,15 @@ class restore_controller extends base_controller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For debug only. Get a simple test display of all the settings.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function debug_display_all_settings_values(): string {
|
||||
return $this->get_plan()->debug_display_all_settings_values();
|
||||
}
|
||||
|
||||
public function get_info() {
|
||||
return $this->info;
|
||||
}
|
||||
|
||||
@@ -2110,7 +2110,6 @@ class restore_ras_and_caps_structure_step extends restore_structure_step {
|
||||
|
||||
public function process_override($data) {
|
||||
$data = (object)$data;
|
||||
|
||||
// Check roleid is one of the mapped ones
|
||||
$newrole = $this->get_mapping('role', $data->roleid);
|
||||
$newroleid = $newrole->newitemid ?? false;
|
||||
@@ -2128,8 +2127,8 @@ class restore_ras_and_caps_structure_step extends restore_structure_step {
|
||||
// Check if the new role is an overrideable role AND if the user performing the restore has the
|
||||
// capability to assign the capability.
|
||||
if (in_array($newrole->info['shortname'], $overrideableroles) &&
|
||||
($safecapability && has_capability('moodle/role:safeoverride', $context, $userid) ||
|
||||
!$safecapability && has_capability('moodle/role:override', $context, $userid))
|
||||
(has_capability('moodle/role:override', $context, $userid) ||
|
||||
($safecapability && has_capability('moodle/role:safeoverride', $context, $userid)))
|
||||
) {
|
||||
assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
|
||||
} else {
|
||||
@@ -4046,9 +4045,30 @@ class restore_contentbankcontent_structure_step extends restore_structure_step {
|
||||
$exists = $DB->record_exists('contentbank_content', $params);
|
||||
if (!$exists) {
|
||||
$params['configdata'] = $data->configdata;
|
||||
$params['usercreated'] = $this->get_mappingid('user', $data->usercreated);
|
||||
$params['usermodified'] = $this->get_mappingid('user', $data->usermodified);
|
||||
$params['timemodified'] = time();
|
||||
|
||||
// Trying to map users. Users cannot always be mapped, e.g. when copying.
|
||||
$params['usercreated'] = $this->get_mappingid('user', $data->usercreated);
|
||||
if (!$params['usercreated']) {
|
||||
// Leave the content creator unchanged when we are restoring the same site.
|
||||
// Otherwise use current user id.
|
||||
if ($this->task->is_samesite()) {
|
||||
$params['usercreated'] = $data->usercreated;
|
||||
} else {
|
||||
$params['usercreated'] = $this->task->get_userid();
|
||||
}
|
||||
}
|
||||
$params['usermodified'] = $this->get_mappingid('user', $data->usermodified);
|
||||
if (!$params['usermodified']) {
|
||||
// Leave the content modifier unchanged when we are restoring the same site.
|
||||
// Otherwise use current user id.
|
||||
if ($this->task->is_samesite()) {
|
||||
$params['usermodified'] = $data->usermodified;
|
||||
} else {
|
||||
$params['usermodified'] = $this->task->get_userid();
|
||||
}
|
||||
}
|
||||
|
||||
$newitemid = $DB->insert_record('contentbank_content', $params);
|
||||
$this->set_mapping('contentbank_content', $oldid, $newitemid, true);
|
||||
}
|
||||
|
||||
@@ -278,6 +278,73 @@ class core_backup_automated_backup_testcase extends advanced_testcase {
|
||||
$this->assertTrue($skipped);
|
||||
$this->expectOutputRegex("/Skipping $course->fullname \(Not modified since previous backup\)/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the task completes when coureid is missing.
|
||||
*/
|
||||
public function test_task_complete_when_courseid_is_missing() {
|
||||
global $DB;
|
||||
$admin = get_admin();
|
||||
$classobject = $this->backupcronautomatedhelper->return_this();
|
||||
|
||||
// Create this backup course.
|
||||
$backupcourse = new stdClass;
|
||||
$backupcourse->courseid = $this->course->id;
|
||||
$backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN;
|
||||
$DB->insert_record('backup_courses', $backupcourse);
|
||||
$backupcourse = $DB->get_record('backup_courses', ['courseid' => $this->course->id]);
|
||||
|
||||
// Create a backup task.
|
||||
$method = new ReflectionMethod('\backup_cron_automated_helper', 'push_course_backup_adhoc_task');
|
||||
$method->setAccessible(true); // Allow accessing of private method.
|
||||
$method->invokeArgs($classobject, [$backupcourse, $admin]);
|
||||
|
||||
// Delete course for this test.
|
||||
delete_course($this->course->id, false);
|
||||
|
||||
$task = core\task\manager::get_next_adhoc_task(time());
|
||||
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertStringContainsString('Invalid course id: ' . $this->course->id . ', task aborted.', $output);
|
||||
core\task\manager::adhoc_task_complete($task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the task completes when backup course is missing.
|
||||
*/
|
||||
public function test_task_complete_when_backup_course_is_missing() {
|
||||
global $DB;
|
||||
$admin = get_admin();
|
||||
$classobject = $this->backupcronautomatedhelper->return_this();
|
||||
|
||||
// Create this backup course.
|
||||
$backupcourse = new stdClass;
|
||||
$backupcourse->courseid = $this->course->id;
|
||||
$backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN;
|
||||
$DB->insert_record('backup_courses', $backupcourse);
|
||||
$backupcourse = $DB->get_record('backup_courses', ['courseid' => $this->course->id]);
|
||||
|
||||
// Create a backup task.
|
||||
$method = new ReflectionMethod('\backup_cron_automated_helper', 'push_course_backup_adhoc_task');
|
||||
$method->setAccessible(true); // Allow accessing of private method.
|
||||
$method->invokeArgs($classobject, [$backupcourse, $admin]);
|
||||
|
||||
// Delete backup course for this test.
|
||||
$DB->delete_records('backup_courses', ['courseid' => $this->course->id]);
|
||||
|
||||
$task = core\task\manager::get_next_adhoc_task(time());
|
||||
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertStringContainsString('Automated backup for course: ' . $this->course->fullname . ' encounters an error.',
|
||||
$output);
|
||||
core\task\manager::adhoc_task_complete($task);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
<?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/>.
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
// Include all the needed stuff.
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
|
||||
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for how backup and restore handles role-related things.
|
||||
*
|
||||
* @package core_backup
|
||||
* @copyright 2021 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class roles_backup_restore_test extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Create a course where the (non-editing) Teacher role is overridden
|
||||
* to have 'moodle/user:loginas' and 'moodle/site:accessallgroups'.
|
||||
*
|
||||
* @return stdClass the new course.
|
||||
*/
|
||||
protected function create_course_with_role_overrides(): stdClass {
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$teacher = $generator->create_user();
|
||||
|
||||
$context = context_course::instance($course->id);
|
||||
$generator->enrol_user($teacher->id, $course->id, 'teacher');
|
||||
|
||||
$editingteacherrole = $this->get_role('teacher');
|
||||
role_change_permission($editingteacherrole->id, $context, 'moodle/user:loginas', CAP_ALLOW);
|
||||
role_change_permission($editingteacherrole->id, $context, 'moodle/site:accessallgroups', CAP_ALLOW);
|
||||
|
||||
return $course;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role id from a shortname.
|
||||
*
|
||||
* @param string $shortname the role shortname.
|
||||
* @return stdClass the role from the DB.
|
||||
*/
|
||||
protected function get_role(string $shortname): stdClass {
|
||||
global $DB;
|
||||
return $DB->get_record('role', ['shortname' => $shortname]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array capability => CAP_... constant for all the orverrides set for a given role on a given context.
|
||||
*
|
||||
* @param string $shortname role shortname.
|
||||
* @param context $context context.
|
||||
* @return array the overrides set here.
|
||||
*/
|
||||
protected function get_overrides_for_role_on_context(string $shortname, context $context): array {
|
||||
$overridedata = get_capabilities_from_role_on_context($this->get_role($shortname), $context);
|
||||
$overrides = [];
|
||||
foreach ($overridedata as $override) {
|
||||
$overrides[$override->capability] = $override->permission;
|
||||
}
|
||||
return $overrides;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a backup of the course.
|
||||
*
|
||||
* @param stdClass $course The course object.
|
||||
* @return string Unique identifier for this backup.
|
||||
*/
|
||||
protected function backup_course(\stdClass $course): string {
|
||||
global $CFG, $USER;
|
||||
|
||||
// Turn off file logging, otherwise it can't delete the file (Windows).
|
||||
$CFG->backup_file_logger_level = backup::LOG_NONE;
|
||||
|
||||
// Do backup with default settings. MODE_IMPORT means it will just
|
||||
// create the directory and not zip it.
|
||||
$bc = new \backup_controller(backup::TYPE_1COURSE, $course->id,
|
||||
backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
|
||||
$USER->id);
|
||||
$backupid = $bc->get_backupid();
|
||||
$bc->execute_plan();
|
||||
$bc->destroy();
|
||||
|
||||
return $backupid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a backup that has been made earlier.
|
||||
*
|
||||
* @param string $backupid The unique identifier of the backup.
|
||||
* @param string $asroleshortname Which role in the new cousre the restorer should have.
|
||||
* @return int The new course id.
|
||||
*/
|
||||
protected function restore_adding_to_course(string $backupid, string $asroleshortname): int {
|
||||
global $CFG, $USER;
|
||||
|
||||
// Create course to restore into, and a user to do the restore.
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$restorer = $generator->create_user();
|
||||
|
||||
$generator->enrol_user($restorer->id, $course->id, $asroleshortname);
|
||||
$this->setUser($restorer);
|
||||
|
||||
// Turn off file logging, otherwise it can't delete the file (Windows).
|
||||
$CFG->backup_file_logger_level = backup::LOG_NONE;
|
||||
|
||||
// Do restore to new course with default settings.
|
||||
$rc = new \restore_controller($backupid, $course->id,
|
||||
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
|
||||
backup::TARGET_CURRENT_ADDING);
|
||||
|
||||
$precheck = $rc->execute_precheck();
|
||||
$this->assertTrue($precheck);
|
||||
$rc->get_plan()->get_setting('role_assignments')->set_value(true);
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
return $course->id;
|
||||
}
|
||||
|
||||
public function test_restore_role_overrides_as_manager(): void {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Create a course and back it up.
|
||||
$course = $this->create_course_with_role_overrides();
|
||||
$backupid = $this->backup_course($course);
|
||||
|
||||
// When manager restores, both role overrides should be restored.
|
||||
$newcourseid = $this->restore_adding_to_course($backupid, 'manager');
|
||||
|
||||
// Verify.
|
||||
$overrides = $this->get_overrides_for_role_on_context('teacher',
|
||||
context_course::instance($newcourseid));
|
||||
$this->assertArrayHasKey('moodle/user:loginas', $overrides);
|
||||
$this->assertEquals(CAP_ALLOW, $overrides['moodle/user:loginas']);
|
||||
$this->assertArrayHasKey('moodle/site:accessallgroups', $overrides);
|
||||
$this->assertEquals(CAP_ALLOW, $overrides['moodle/site:accessallgroups']);
|
||||
}
|
||||
|
||||
public function test_restore_role_overrides_as_teacher(): void {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Create a course and back it up.
|
||||
$course = $this->create_course_with_role_overrides();
|
||||
$backupid = $this->backup_course($course);
|
||||
|
||||
// When teacher restores, only the safe override should be restored.
|
||||
$newcourseid = $this->restore_adding_to_course($backupid, 'editingteacher');
|
||||
|
||||
// Verify.
|
||||
$overrides = $this->get_overrides_for_role_on_context('teacher',
|
||||
context_course::instance($newcourseid));
|
||||
$this->assertArrayNotHasKey('moodle/user:loginas', $overrides);
|
||||
$this->assertArrayHasKey('moodle/site:accessallgroups', $overrides);
|
||||
$this->assertEquals(CAP_ALLOW, $overrides['moodle/site:accessallgroups']);
|
||||
}
|
||||
}
|
||||
@@ -126,6 +126,19 @@ abstract class base_plan implements checksumable, executable {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* For debug only. Get a simple test display of all the settings.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function debug_display_all_settings_values(): string {
|
||||
$result = '';
|
||||
foreach ($this->settings as $name => $setting) {
|
||||
$result .= $name . ': ' . $setting->get_value() . "\n";
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper over @get_setting() that returns if the requested setting exists or no
|
||||
*/
|
||||
|
||||
@@ -14,10 +14,8 @@ Feature: Duplicate activities
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
| backup_import_activities | 0 |
|
||||
And I log out
|
||||
And the following config values are set as admin:
|
||||
| backup_import_activities | 0 | backup |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Database" to section "1" and I fill the form with:
|
||||
|
||||
@@ -52,7 +52,7 @@ if (!is_null($action)) {
|
||||
} else { // Revoked badge.
|
||||
header("HTTP/1.0 410 Gone");
|
||||
$assertion = array();
|
||||
if ($obversion == OPEN_BADGES_V2) {
|
||||
if ($obversion >= OPEN_BADGES_V2) {
|
||||
$assertionurl = new moodle_url('/badges/assertion.php', array('b' => $hash));
|
||||
$assertion['id'] = $assertionurl->out();
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ if ($persistedissuer) {
|
||||
$returnurl = new moodle_url('/badges/backpack-connect.php',
|
||||
['action' => 'authorization', 'sesskey' => sesskey()]);
|
||||
|
||||
// If scope is not passed as parameter, use the issuer supported scopes.
|
||||
if (empty($scope)) {
|
||||
$scope = $issuer->get('scopessupported');
|
||||
}
|
||||
$client = new core_badges\oauth2\client($issuer, $returnurl, $scope, $externalbackpack);
|
||||
if ($client) {
|
||||
if (!$client->is_logged_in()) {
|
||||
|
||||
@@ -128,7 +128,7 @@ class core_badges_assertion {
|
||||
$email = empty($this->_data->backpackemail) ? $this->_data->email : $this->_data->backpackemail;
|
||||
$assertionurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'obversion' => $this->_obversion));
|
||||
|
||||
if ($this->_obversion == OPEN_BADGES_V2) {
|
||||
if ($this->_obversion >= OPEN_BADGES_V2) {
|
||||
$classurl = new moodle_url('/badges/badge_json.php', array('id' => $this->get_badge_id()));
|
||||
} else {
|
||||
$classurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'action' => 1));
|
||||
@@ -325,7 +325,7 @@ class core_badges_assertion {
|
||||
*/
|
||||
protected function embed_data_badge_version2 (&$json, $type = OPEN_BADGES_V2_TYPE_ASSERTION) {
|
||||
// Specification Version 2.0.
|
||||
if ($this->_obversion == OPEN_BADGES_V2) {
|
||||
if ($this->_obversion >= OPEN_BADGES_V2) {
|
||||
$badge = new badge($this->_data->id);
|
||||
if (empty($this->_data->courseid)) {
|
||||
$context = context_system::instance();
|
||||
|
||||
@@ -61,12 +61,12 @@ class external_backpack extends \moodleform {
|
||||
$mform->addElement('text', 'backpackapiurl', get_string('backpackapiurl', 'core_badges'));
|
||||
$mform->setType('backpackapiurl', PARAM_URL);
|
||||
$mform->addRule('backpackapiurl', null, 'required', null, 'client');
|
||||
$mform->addRule('backpackapiurl', get_string('maximumchars', '', 255), 'maxlength', 50, 'client');
|
||||
$mform->addRule('backpackapiurl', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
|
||||
|
||||
$mform->addElement('text', 'backpackweburl', get_string('backpackweburl', 'core_badges'));
|
||||
$mform->setType('backpackweburl', PARAM_URL);
|
||||
$mform->addRule('backpackweburl', null, 'required', null, 'client');
|
||||
$mform->addRule('backpackweburl', get_string('maximumchars', '', 255), 'maxlength', 50, 'client');
|
||||
$mform->addRule('backpackweburl', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
|
||||
|
||||
$apiversions = badges_get_badge_api_versions();
|
||||
$mform->addElement('select', 'apiversion', get_string('apiversion', 'core_badges'), $apiversions);
|
||||
|
||||
@@ -222,6 +222,7 @@ class client extends \core\oauth2\client {
|
||||
$callbackurl = self::callback_url();
|
||||
|
||||
if ($granttype == 'authorization_code') {
|
||||
$this->basicauth = true;
|
||||
$params = array('code' => $code,
|
||||
'grant_type' => $granttype,
|
||||
'redirect_uri' => $callbackurl->out(false),
|
||||
@@ -236,7 +237,7 @@ class client extends \core\oauth2\client {
|
||||
);
|
||||
}
|
||||
if ($this->basicauth) {
|
||||
$idsecret = urlencode($this->clientid) . ':' . urlencode($this->clientsecret);
|
||||
$idsecret = $this->clientid . ':' . $this->clientsecret;
|
||||
$this->setHeader('Authorization: Basic ' . base64_encode($idsecret));
|
||||
} else {
|
||||
$params['client_id'] = $this->clientid;
|
||||
@@ -244,11 +245,13 @@ class client extends \core\oauth2\client {
|
||||
}
|
||||
// Requests can either use http GET or POST.
|
||||
$response = $this->post($this->token_url(), $this->build_post_data($params));
|
||||
$r = json_decode($response);
|
||||
if ($this->info['http_code'] !== 200) {
|
||||
throw new moodle_exception('Could not upgrade oauth token');
|
||||
$debuginfo = !empty($this->error) ? $this->error : $response;
|
||||
throw new moodle_exception('oauth2refreshtokenerror', 'core_error', '', $this->info['http_code'], $debuginfo);
|
||||
}
|
||||
|
||||
$r = json_decode($response);
|
||||
|
||||
if (is_null($r)) {
|
||||
throw new moodle_exception("Could not decode JSON token response");
|
||||
}
|
||||
|
||||
@@ -240,13 +240,23 @@ class award_criteria_cohort extends award_criteria {
|
||||
return array($join, $where, $params);
|
||||
} else {
|
||||
// User is a member of ALL of the specified cohorts.
|
||||
$join = " LEFT JOIN {cohort_members} cm ON cm.userid = u.id";
|
||||
$join = ' LEFT JOIN {cohort_members} cm ON cm.userid = u.id';
|
||||
$where = ' AND (';
|
||||
$i = 0;
|
||||
foreach ($this->params as $param) {
|
||||
$i++;
|
||||
$where = ' AND cm.cohortid = :cohortid'.$i;
|
||||
if ($i == 0) {
|
||||
$where .= 'cm.cohortid = :cohortid'.$i;
|
||||
} else {
|
||||
$where .= ' OR cm.cohortid = :cohortid'.$i;
|
||||
}
|
||||
$params['cohortid'.$i] = $param['cohort'];
|
||||
$i++;
|
||||
}
|
||||
$where .= ')
|
||||
GROUP BY u.id, bi.badgeid
|
||||
HAVING COUNT(cm.cohortid) = :cohortcount';
|
||||
$params['cohortcount'] = $i;
|
||||
|
||||
return array($join, $where, $params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,35 +620,127 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test badges observer when cohort_member_added event is fired.
|
||||
* Test badges observer when cohort_member_added event is fired and user required to belong to any cohort.
|
||||
*
|
||||
* @covers award_criteria_cohort
|
||||
*/
|
||||
public function test_badges_observer_cohort_criteria_review() {
|
||||
public function test_badges_observer_any_cohort_criteria_review() {
|
||||
global $CFG;
|
||||
|
||||
require_once("$CFG->dirroot/cohort/lib.php");
|
||||
|
||||
$cohort = $this->getDataGenerator()->create_cohort();
|
||||
$cohort1 = $this->getDataGenerator()->create_cohort();
|
||||
$cohort2 = $this->getDataGenerator()->create_cohort();
|
||||
|
||||
$this->preventResetByRollback(); // Messaging is not compatible with transactions.
|
||||
|
||||
$badge = new badge($this->badgeid);
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
|
||||
// Set up the badge criteria.
|
||||
$criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
|
||||
$criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
|
||||
$criteriaoverall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_COHORT, 'badgeid' => $badge->id));
|
||||
$criteriaoverall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'cohort_cohorts' => array('0' => $cohort->id)));
|
||||
|
||||
// Make the badge active.
|
||||
$criteriaoverall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY,
|
||||
'cohort_cohorts' => array('0' => $cohort1->id, '1' => $cohort2->id)));
|
||||
$badge->set_status(BADGE_STATUS_ACTIVE);
|
||||
|
||||
// Reload it to contain criteria.
|
||||
$badge = new badge($this->badgeid);
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
|
||||
// Add the user to the cohort.
|
||||
cohort_add_member($cohort->id, $this->user->id);
|
||||
cohort_add_member($cohort2->id, $this->user->id);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
// Verify that the badge was awarded.
|
||||
$this->assertDebuggingCalled();
|
||||
$this->assertTrue($badge->is_issued($this->user->id));
|
||||
// As the badge has been awarded to user because core_badges_observer been called when the member has been added to the
|
||||
// cohort, there are no other users that can award this badge.
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
}
|
||||
|
||||
/**
|
||||
* Test badges observer when cohort_member_added event is fired and user required to belong to multiple (all) cohorts.
|
||||
*
|
||||
* @covers award_criteria_cohort
|
||||
*/
|
||||
public function test_badges_observer_all_cohort_criteria_review() {
|
||||
global $CFG;
|
||||
|
||||
require_once("$CFG->dirroot/cohort/lib.php");
|
||||
|
||||
$cohort1 = $this->getDataGenerator()->create_cohort();
|
||||
$cohort2 = $this->getDataGenerator()->create_cohort();
|
||||
$cohort3 = $this->getDataGenerator()->create_cohort();
|
||||
|
||||
// Add user2 to cohort1 and cohort3.
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
cohort_add_member($cohort3->id, $user2->id);
|
||||
cohort_add_member($cohort1->id, $user2->id);
|
||||
|
||||
// Add user3 to cohort1, cohort2 and cohort3.
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
cohort_add_member($cohort1->id, $user3->id);
|
||||
cohort_add_member($cohort2->id, $user3->id);
|
||||
cohort_add_member($cohort3->id, $user3->id);
|
||||
|
||||
$this->preventResetByRollback(); // Messaging is not compatible with transactions.
|
||||
|
||||
// Cohort criteria are used in site badges.
|
||||
$badge = new badge($this->badgeid);
|
||||
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
|
||||
// Set up the badge criteria.
|
||||
$criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
|
||||
$criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
|
||||
$criteriaoverall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_COHORT, 'badgeid' => $badge->id));
|
||||
$criteriaoverall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL,
|
||||
'cohort_cohorts' => array('0' => $cohort1->id, '1' => $cohort2->id, '2' => $cohort3->id)));
|
||||
$badge->set_status(BADGE_STATUS_ACTIVE);
|
||||
|
||||
// Reload it to contain criteria.
|
||||
$badge = new badge($this->badgeid);
|
||||
|
||||
// Verify that the badge was not awarded yet (ALL cohorts are needed and review_all_criteria has to be called).
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertFalse($badge->is_issued($user2->id));
|
||||
$this->assertFalse($badge->is_issued($user3->id));
|
||||
|
||||
// Verify that after calling review_all_criteria, users with the criteria (user3) award the badge instantly.
|
||||
$this->assertSame(1, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertFalse($badge->is_issued($user2->id));
|
||||
$this->assertTrue($badge->is_issued($user3->id));
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
// Add the user to the cohort1.
|
||||
cohort_add_member($cohort1->id, $this->user->id);
|
||||
|
||||
// Verify that the badge was not awarded yet (ALL cohorts are needed).
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
|
||||
// Add the user to the cohort3.
|
||||
cohort_add_member($cohort3->id, $this->user->id);
|
||||
|
||||
// Verify that the badge was not awarded yet (ALL cohorts are needed).
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
|
||||
// Add user to cohort2.
|
||||
cohort_add_member($cohort2->id, $this->user->id);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
// Verify that the badge was awarded (ALL cohorts).
|
||||
$this->assertTrue($badge->is_issued($this->user->id));
|
||||
// As the badge has been awarded to user because core_badges_observer been called when the member has been added to the
|
||||
// cohort, there are no other users that can award this badge.
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -694,6 +786,16 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
|
||||
$this->assertStringMatchesFormat($testassertion2->badge, json_encode($assertion2->get_badge_assertion()));
|
||||
$this->assertStringMatchesFormat($testassertion2->class, json_encode($assertion2->get_badge_class()));
|
||||
$this->assertStringMatchesFormat($testassertion2->issuer, json_encode($assertion2->get_issuer()));
|
||||
|
||||
// Test Openbadge specification version 2.1. It has the same format as OBv2.0.
|
||||
// Get assertion version 2.1.
|
||||
$award = reset($awards);
|
||||
$assertion2 = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V2P1);
|
||||
|
||||
// Make sure JSON strings have the same structure.
|
||||
$this->assertStringMatchesFormat($testassertion2->badge, json_encode($assertion2->get_badge_assertion()));
|
||||
$this->assertStringMatchesFormat($testassertion2->class, json_encode($assertion2->get_badge_class()));
|
||||
$this->assertStringMatchesFormat($testassertion2->issuer, json_encode($assertion2->get_issuer()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,18 +51,18 @@ Feature: Award badges based on activity completion
|
||||
And I log out
|
||||
|
||||
Scenario: Student earns a badge using activity completion, but does not get passing grade
|
||||
When I log in as "student1"
|
||||
Given I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And the "Test quiz name" "quiz" activity with "auto" completion should be marked as not complete
|
||||
And I follow "Test quiz name"
|
||||
When I am on the "Test quiz name" "quiz activity" page
|
||||
And I press "Re-attempt quiz"
|
||||
And I set the field "False" to "1"
|
||||
And I press "Finish attempt ..."
|
||||
And I press "Submit all and finish"
|
||||
And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
|
||||
And I log out
|
||||
Then I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I should see "Recipients (1)"
|
||||
Then I should see "Recipients (1)"
|
||||
|
||||
@@ -64,6 +64,7 @@ Feature: Award badges based on competency completion
|
||||
And I set the following fields to these values:
|
||||
| Rating | C |
|
||||
And I click on "Rate" "button" in the "Rate" "dialogue"
|
||||
And I should see "The competency rating was manually set in the course"
|
||||
And I log out
|
||||
# See if we got the badge
|
||||
Then I log in as "user1"
|
||||
@@ -114,8 +115,8 @@ Feature: Award badges based on competency completion
|
||||
And I press "Rate"
|
||||
And I set the following fields to these values:
|
||||
| Rating | C |
|
||||
And I wait until the page is ready
|
||||
And I click on "Rate" "button" in the "Rate" "dialogue"
|
||||
And I should see "The competency rating was manually set in the course"
|
||||
And I log out
|
||||
# See if we got the badge
|
||||
Then I log in as "user1"
|
||||
@@ -168,8 +169,8 @@ Feature: Award badges based on competency completion
|
||||
And I press "Rate"
|
||||
And I set the following fields to these values:
|
||||
| Rating | C |
|
||||
And I wait until the page is ready
|
||||
And I click on "Rate" "button" in the "Rate" "dialogue"
|
||||
And I should see "The competency rating was manually set in the course"
|
||||
And I log out
|
||||
# We should not get the badge yet.
|
||||
Then I log in as "user1"
|
||||
@@ -184,8 +185,8 @@ Feature: Award badges based on competency completion
|
||||
And I press "Rate"
|
||||
And I set the following fields to these values:
|
||||
| Rating | C |
|
||||
And I wait until the page is ready
|
||||
And I click on "Rate" "button" in the "Rate" "dialogue"
|
||||
And I should see "The competency rating was manually set in the course"
|
||||
And I log out
|
||||
# See if we got the badge now.
|
||||
Then I log in as "user1"
|
||||
|
||||
@@ -147,7 +147,6 @@ Feature: My overview block pagination
|
||||
| student1 | C13 | student |
|
||||
When I log in as "student1"
|
||||
And I click on "[data-control='next']" "css_element" in the "Course overview" "block"
|
||||
And I wait until ".block_myoverview [data-control='next']" "css_element" exists
|
||||
Then the "class" attribute of ".block_myoverview [data-control='next']" "css_element" should contain "disabled"
|
||||
And I log out
|
||||
|
||||
@@ -181,7 +180,6 @@ Feature: My overview block pagination
|
||||
| student1 | C25 | student |
|
||||
When I log in as "student1"
|
||||
And I click on "[data-control='next']" "css_element" in the "Course overview" "block"
|
||||
And I wait until ".block_myoverview [data-control='next']" "css_element" exists
|
||||
Then the "class" attribute of ".block_myoverview [data-control='next']" "css_element" should not contain "disabled"
|
||||
And the "class" attribute of ".block_myoverview [data-control='previous']" "css_element" should not contain "disabled"
|
||||
And I should see "Course 13" in the "Course overview" "block"
|
||||
|
||||
@@ -27,9 +27,7 @@ Feature: Course overview block show users their progress on courses
|
||||
Then I should not see "0%" in the "Course overview" "block"
|
||||
|
||||
Scenario: User complete activity and verify his progress
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I follow "Test choice 1"
|
||||
Given I am on the "Test choice 1" "choice activity" page logged in as teacher1
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Completion tracking | Show activity as complete when conditions are met |
|
||||
@@ -40,8 +38,8 @@ Feature: Course overview block show users their progress on courses
|
||||
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
|
||||
Then I should see "Course 1" in the "Course overview" "block"
|
||||
And I should see "0%" in the "Course overview" "block"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Test choice 1"
|
||||
|
||||
And I am on the "Test choice 1" "choice activity" page
|
||||
And I follow "Dashboard" in the user menu
|
||||
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
|
||||
And I should see "100%" in the "Course overview" "block"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['otherusers'] = 'Other Users ({$a})';
|
||||
$string['otherusers'] = 'Other users ({$a})';
|
||||
$string['onlinestatushiding_desc'] = 'If enabled, users have the option to hide their online status from other users.';
|
||||
$string['configtimetosee'] = 'Number of minutes determining the period of inactivity after which a user is no longer considered to be online.';
|
||||
$string['onlinestatushiding'] = 'Online status hiding';
|
||||
|
||||
@@ -37,7 +37,7 @@ Feature: The online users block allow you to see who is currently online on fron
|
||||
When I log in as "student1"
|
||||
And I am on site homepage
|
||||
Then I should not see "Admin User" in the "Online users" "block"
|
||||
And I should see "Other Users (1)" in the "Online users" "block"
|
||||
And I should see "Other users (1)" in the "Online users" "block"
|
||||
And I should see "Student 1" in the "Online users" "block"
|
||||
And I should see "Student 2" in the "Online users" "block"
|
||||
And I should see "3 online users" in the "Online users" "block"
|
||||
@@ -79,7 +79,7 @@ Feature: The online users block allow you to see who is currently online on fron
|
||||
And I am on site homepage
|
||||
Then I should see "2 online user" in the "Online users" "block"
|
||||
And I should not see "Admin" in the "Online users" "block"
|
||||
And I should see "Other Users (1)" in the "Online users" "block"
|
||||
And I should see "Other users (1)" in the "Online users" "block"
|
||||
And I should see "Student 2" in the "Online users" "block"
|
||||
And I should not see "Student 1" in the "Online users" "block"
|
||||
And I log out
|
||||
@@ -94,7 +94,7 @@ Feature: The online users block allow you to see who is currently online on fron
|
||||
And I am on site homepage
|
||||
Then I should see "3 online users" in the "Online users" "block"
|
||||
And I should not see "Admin" in the "Online users" "block"
|
||||
And I should see "Other Users (1)" in the "Online users" "block"
|
||||
And I should see "Other users (1)" in the "Online users" "block"
|
||||
And I should see "Student 2" in the "Online users" "block"
|
||||
And I should see "Student 1" in the "Online users" "block"
|
||||
|
||||
@@ -138,7 +138,7 @@ Feature: The online users block allow you to see who is currently online on fron
|
||||
And I am on site homepage
|
||||
And I should see "2 online user" in the "Online users" "block"
|
||||
And I should not see "Admin" in the "Online users" "block"
|
||||
And I should see "Other Users (1)" in the "Online users" "block"
|
||||
And I should see "Other users (1)" in the "Online users" "block"
|
||||
And I should see "Student 2" in the "Online users" "block"
|
||||
And I should not see "Student 1" in the "Online users" "block"
|
||||
And I log out
|
||||
@@ -148,6 +148,6 @@ Feature: The online users block allow you to see who is currently online on fron
|
||||
When I am on site homepage
|
||||
Then I should see "3 online users" in the "Online users" "block"
|
||||
And I should not see "Admin" in the "Online users" "block"
|
||||
And I should see "Other Users (1)" in the "Online users" "block"
|
||||
And I should see "Other users (1)" in the "Online users" "block"
|
||||
And I should see "Student 2" in the "Online users" "block"
|
||||
And I should see "Student 1" in the "Online users" "block"
|
||||
|
||||
@@ -19,7 +19,7 @@ Feature: The private files block allows users to store files privately in moodle
|
||||
| page | C1 | page1 | Test page name | Test page description |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I follow "Test page name"
|
||||
And I am on the "Test page name" "page activity" page
|
||||
And I add the "Private files" block
|
||||
And I should see "No files available" in the "Private files" "block"
|
||||
When I follow "Manage private files..."
|
||||
|
||||
+8
-12
@@ -16,24 +16,20 @@ Feature: The recently accessed items block allows users to easily access their m
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student1 | C2 | student |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage
|
||||
And I turn editing mode on
|
||||
And I add a "Forum" to section "1" and I fill the form with:
|
||||
| Forum name | Test forum name |
|
||||
| Forum type | Standard forum for general use |
|
||||
| Description | Test forum description |
|
||||
And I log out
|
||||
And the following "activity" exists:
|
||||
| course | C1 |
|
||||
| activity | forum |
|
||||
| idnumber | Test forum name |
|
||||
| name | Test forum name |
|
||||
And I log in as "student1"
|
||||
When I press "Customise this page"
|
||||
And I press "Customise this page"
|
||||
And I add the "Recently accessed items" block
|
||||
|
||||
Scenario: User has not accessed any item
|
||||
Then I should see "No recent items" in the "Recently accessed items" "block"
|
||||
|
||||
Scenario: User has accessed some items
|
||||
Given I am on "Course 1" course homepage
|
||||
When I follow "Test forum name"
|
||||
Given I change window size to "large"
|
||||
When I am on the "Test forum name" "forum activity" page
|
||||
And I follow "Dashboard" in the user menu
|
||||
And I change window size to "large"
|
||||
Then I should see "Test forum name" in the "Recently accessed items" "block"
|
||||
|
||||
@@ -24,9 +24,9 @@ Feature: Edit activities in main menu block
|
||||
|
||||
@javascript
|
||||
Scenario: Activities in main menu block can be made available but not visible on a course page
|
||||
And I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
Given the following config values are set as admin:
|
||||
| allowstealth | 1 |
|
||||
And I log in as "admin"
|
||||
And I am on site homepage
|
||||
And I navigate to "Turn editing on" in current page administration
|
||||
And I add the "Main menu" block
|
||||
|
||||
@@ -33,10 +33,8 @@ Feature: Edit activities in social activities block
|
||||
|
||||
@javascript
|
||||
Scenario: Activities in social activities block can be made available but not visible on a course page
|
||||
And I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
Given the following config values are set as admin:
|
||||
| allowstealth | 1 |
|
||||
And I log out
|
||||
And I log in as "user1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add the "Recent activity" block
|
||||
|
||||
@@ -42,19 +42,21 @@ class block_tag_youtube_edit_form extends block_edit_form {
|
||||
|
||||
// Category setting.
|
||||
$categorychoices = ['0' => get_string('anycategory', 'block_tag_youtube')];
|
||||
$hascategoryerror = false;
|
||||
$categoryerror = '';
|
||||
|
||||
try {
|
||||
// Get all video categories through an API call and add them to the category list.
|
||||
$categorychoices += $this->block->get_categories();
|
||||
} catch (Exception $e) {
|
||||
$hascategoryerror = true;
|
||||
$categoryerror = $e->getMessage();
|
||||
}
|
||||
$mform->addElement('select', 'config_category', get_string('category', 'block_tag_youtube'),
|
||||
$categorychoices);
|
||||
$mform->setDefault('config_category', 0);
|
||||
|
||||
if ($categoryerror) {
|
||||
if ($hascategoryerror) {
|
||||
$notification = $OUTPUT->notification(get_string('categoryerror', 'block_tag_youtube', $categoryerror),
|
||||
'error');
|
||||
$mform->addElement('static', 'config_category_error', '', $notification);
|
||||
|
||||
+2
-1
@@ -3204,6 +3204,7 @@ function calendar_update_subscription($subscription) {
|
||||
* @return bool true if current user can edit the subscription else false
|
||||
*/
|
||||
function calendar_can_edit_subscription($subscriptionorid) {
|
||||
global $USER;
|
||||
if (is_array($subscriptionorid)) {
|
||||
$subscription = (object)$subscriptionorid;
|
||||
} else if (is_object($subscriptionorid)) {
|
||||
@@ -3224,7 +3225,7 @@ function calendar_can_edit_subscription($subscriptionorid) {
|
||||
calendar_get_allowed_types($allowed, $courseid, null, $category);
|
||||
switch ($subscription->eventtype) {
|
||||
case 'user':
|
||||
return $allowed->user;
|
||||
return ($USER->id == $subscription->userid && $allowed->user);
|
||||
case 'course':
|
||||
if (isset($allowed->courses[$courseid])) {
|
||||
return $allowed->courses[$courseid];
|
||||
|
||||
@@ -66,6 +66,102 @@ function create_event($properties) {
|
||||
return $event->create($record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create a x number of events for each event type.
|
||||
*
|
||||
* @param int $quantity The quantity of events to be created.
|
||||
* @return array List of created events.
|
||||
*/
|
||||
function create_standard_events(int $quantity): array {
|
||||
$types = ['site', 'category', 'course', 'group', 'user'];
|
||||
|
||||
$events = [];
|
||||
foreach ($types as $eventtype) {
|
||||
// Create five events of each event type.
|
||||
for ($i = 0; $i < $quantity; $i++) {
|
||||
$events[] = create_event(['eventtype' => $eventtype]);
|
||||
}
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create an action event.
|
||||
*
|
||||
* @param array $data The event data.
|
||||
* @return bool|calendar_event
|
||||
*/
|
||||
function create_action_event(array $data) {
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/calendar/lib.php');
|
||||
|
||||
if (!isset($data['modulename']) || !isset($data['instance'])) {
|
||||
throw new coding_exception('Module and instance should be specified when creating an action event.');
|
||||
}
|
||||
|
||||
$isuseroverride = isset($data->priority) && $data->priority == CALENDAR_EVENT_USER_OVERRIDE_PRIORITY;
|
||||
if ($isuseroverride) {
|
||||
if (!in_array($data['modulename'], ['assign', 'lesson', 'quiz'])) {
|
||||
throw new coding_exception('Only assign, lesson and quiz modules supports overrides');
|
||||
}
|
||||
}
|
||||
|
||||
$event = array_merge($data, [
|
||||
'eventtype' => isset($data['eventtype']) ? $data['eventtype'] : 'open',
|
||||
'courseid' => isset($data['courseid']) ? $data['courseid'] : 0,
|
||||
'instance' => $data['instance'],
|
||||
'modulename' => $data['modulename'],
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
]);
|
||||
|
||||
return create_event($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create an user override calendar event.
|
||||
*
|
||||
* @param string $modulename The modulename.
|
||||
* @param int $instanceid The instance id.
|
||||
* @param int $userid The user id.
|
||||
* @return calendar_event|false
|
||||
*/
|
||||
function create_user_override_event(string $modulename, int $instanceid, int $userid) {
|
||||
if (!isset($userid)) {
|
||||
throw new coding_exception('Must specify userid when creating a user override.');
|
||||
}
|
||||
|
||||
return create_action_event([
|
||||
'modulename' => $modulename,
|
||||
'instance' => $instanceid,
|
||||
'userid' => $userid,
|
||||
'priority' => CALENDAR_EVENT_USER_OVERRIDE_PRIORITY,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create an group override calendar event.
|
||||
*
|
||||
* @param string $modulename The modulename.
|
||||
* @param int $instanceid The instance id.
|
||||
* @param int $courseid The course id.
|
||||
* @param int $groupid The group id.
|
||||
* @return calendar_event|false
|
||||
*/
|
||||
function create_group_override_event(string $modulename, int $instanceid, int $courseid, int $groupid) {
|
||||
if (!isset($groupid)) {
|
||||
throw new coding_exception('Must specify groupid when creating a group override.');
|
||||
}
|
||||
|
||||
return create_action_event([
|
||||
'groupid' => $groupid,
|
||||
'courseid' => $courseid,
|
||||
'modulename' => $modulename,
|
||||
'instance' => $instanceid,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A test factory that will create action events.
|
||||
*
|
||||
|
||||
@@ -2154,11 +2154,7 @@ class core_calendar_rrule_manager_testcase extends advanced_testcase {
|
||||
// Change our event's date to the 20th Monday of the current year.
|
||||
$twentiethmonday = new DateTime(date('Y-01-01'));
|
||||
$twentiethmonday->modify('+20 Monday');
|
||||
$startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T090000'), 'US/Eastern');
|
||||
|
||||
$startdate = new DateTime($startdatetime->format('Y-m-d'));
|
||||
|
||||
$offset = $startdatetime->diff($startdate, true);
|
||||
$startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T000000'), 'US/Eastern');
|
||||
|
||||
$interval = new DateInterval('P1Y');
|
||||
|
||||
@@ -2183,7 +2179,6 @@ class core_calendar_rrule_manager_testcase extends advanced_testcase {
|
||||
$expecteddate->modify('January 1');
|
||||
$expecteddate->add($interval);
|
||||
$expecteddate->modify("+20 Monday");
|
||||
$expecteddate->add($offset);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,12 +36,10 @@ Feature: Restrict activity availability through date conditions
|
||||
| x[year] | 2037 |
|
||||
And I press "Save and return to course"
|
||||
And I log out
|
||||
When I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I am on the "Course 1" course page logged in as student1
|
||||
Then I should see "Available from 31 December 2037"
|
||||
And "Test assignment 1" "link" should not exist in the "page" "region"
|
||||
And "Test assignment 1" activity should be dimmed
|
||||
And "Test assignment 1" "link" should not exist
|
||||
And I log out
|
||||
|
||||
@javascript
|
||||
Scenario: Show activity hidden to students when available until date is in past
|
||||
@@ -60,6 +58,5 @@ Feature: Restrict activity availability through date conditions
|
||||
And I click on ".availability-item .availability-eye img" "css_element"
|
||||
And I press "Save and return to course"
|
||||
And I log out
|
||||
When I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
Then I should not see "Test assignment 2"
|
||||
When I am on the "Course 1" course page logged in as student1
|
||||
Then I should not see "Test assignment 2" in the "page" "region"
|
||||
|
||||
@@ -17,13 +17,16 @@ Feature: Restrict activity availability through grade conditions
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And I log in as "teacher1"
|
||||
And the following "activity" exists:
|
||||
| course | C1 |
|
||||
| activity | assign |
|
||||
| idnumber | Grade assignment |
|
||||
| name | Grade assignment |
|
||||
| assignsubmission_onlinetext_enabled | 1 |
|
||||
| assignsubmission_file_enabled | 0 |
|
||||
| submissiondrafts | 0 |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Assignment" to section "1" and I fill the form with:
|
||||
| Assignment name | Grade assignment |
|
||||
| Description | Grade this assignment to revoke restriction on restricted assignment |
|
||||
| assignsubmission_onlinetext_enabled | 1 |
|
||||
| assignsubmission_file_enabled | 0 |
|
||||
# Adding the page like this because id_availableform_enabled needs to be clicked to trigger the action.
|
||||
And I add a "Page" to section "2"
|
||||
And I expand all fieldsets
|
||||
@@ -31,15 +34,15 @@ Feature: Restrict activity availability through grade conditions
|
||||
And I click on "Grade" "button" in the "Add restriction..." "dialogue"
|
||||
And I click on "min" "checkbox"
|
||||
And I set the following fields to these values:
|
||||
| Name | Test page name |
|
||||
| Description | Restricted page, till grades in Grade assignment is at least 20% |
|
||||
| Page content | Test page contents |
|
||||
| id | Grade assignment |
|
||||
| minval | 20 |
|
||||
| Name | Test page name |
|
||||
| Description | Restricted page, till grades in Grade assignment is at least 20% |
|
||||
| Page content | Test page contents |
|
||||
| id | Grade assignment |
|
||||
| minval | 20 |
|
||||
And I press "Save and return to course"
|
||||
And I log out
|
||||
When I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
When I am on the "Course 1" course page logged in as student1
|
||||
Then I should see "Not available unless: You achieve a required score in Grade assignment"
|
||||
And "Test page name" activity should be dimmed
|
||||
And "Test page name" "link" should not exist
|
||||
@@ -50,9 +53,8 @@ Feature: Restrict activity availability through grade conditions
|
||||
And I press "Save changes"
|
||||
And I should see "Submitted for grading"
|
||||
And I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Grade assignment"
|
||||
|
||||
And I am on the "Grade assignment" "assign activity" page logged in as teacher1
|
||||
And I navigate to "View all submissions" in current page administration
|
||||
And I click on "Grade" "link" in the "Student First" "table_row"
|
||||
And I set the following fields to these values:
|
||||
@@ -61,7 +63,7 @@ Feature: Restrict activity availability through grade conditions
|
||||
And I press "OK"
|
||||
And I follow "Edit settings"
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
And I am on the "Course 1" course page logged in as student1
|
||||
And "Test page name" activity should be visible
|
||||
And I should not see "Not available unless: You achieve a required score in Grade assignment"
|
||||
|
||||
@@ -76,16 +76,14 @@ Feature: Restrict sections availability through completion or grade conditions
|
||||
And I am on "Course 1" course homepage
|
||||
Then I should see "Not available unless: You achieve a required score in Grade assignment"
|
||||
And "Test page name" activity should be hidden
|
||||
And I follow "Grade assignment"
|
||||
And I am on the "Grade assignment" "assign activity" page
|
||||
And I press "Add submission"
|
||||
And I set the following fields to these values:
|
||||
| Online text | I'm the student submission |
|
||||
And I press "Save changes"
|
||||
And I should see "Submitted for grading"
|
||||
And I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Grade assignment"
|
||||
And I am on the "Grade assignment" "assign activity" page logged in as teacher1
|
||||
And I navigate to "View all submissions" in current page administration
|
||||
And I click on "Grade" "link" in the "Student First" "table_row"
|
||||
And I set the following fields to these values:
|
||||
@@ -94,7 +92,6 @@ Feature: Restrict sections availability through completion or grade conditions
|
||||
And I press "OK"
|
||||
And I follow "Edit settings"
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I am on the "Course 1" Course page logged in as student1
|
||||
And "Test page name" activity should be visible
|
||||
And I should not see "Not available unless: You achieve a required score in Grade assignment"
|
||||
|
||||
@@ -159,3 +159,37 @@ Feature: Content bank use editor feature
|
||||
And I click on "Content bank" "link"
|
||||
And I follow "filltheblanks.h5p"
|
||||
Then "Edit" "link" should not exist in the "region-main" "region"
|
||||
|
||||
Scenario: Teachers keep their content authoring in copied courses
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
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 | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
|
||||
| Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p |
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Copy course" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Course full name | Copy |
|
||||
| Course short name | Copy |
|
||||
| Teacher | 1 |
|
||||
When I press "Copy and view"
|
||||
And I trigger cron
|
||||
And I am on homepage
|
||||
And I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Copy" course homepage
|
||||
And I expand "Site pages" node
|
||||
And I click on "Content bank" "link"
|
||||
And I follow "ipsums.h5p"
|
||||
Then "Edit" "link" should exist in the "region-main" "region"
|
||||
And I click on "Content bank" "link"
|
||||
And I follow "filltheblanks.h5p"
|
||||
Then "Edit" "link" should not exist in the "region-main" "region"
|
||||
|
||||
@@ -103,6 +103,10 @@ class no_recent_accesses extends course_enrolments {
|
||||
return get_string('nocoursestudents', 'course');
|
||||
}
|
||||
|
||||
if (!$fortraining && !$course->get_course_data()->visible) {
|
||||
return get_string('hiddenfromstudents');
|
||||
}
|
||||
|
||||
if ($course->get_end() && $course->get_end() < $course->get_start()) {
|
||||
return get_string('errorendbeforestart', 'course');
|
||||
}
|
||||
|
||||
+24
-25
@@ -57,8 +57,6 @@ M.course_dndupload = {
|
||||
// The selector identifying the list of modules within a section (note changing this may require
|
||||
// changes to the get_mods_element function)
|
||||
modslistselector: 'ul.section',
|
||||
// Original onbeforeunload event.
|
||||
originalUnloadEvent: null,
|
||||
|
||||
/**
|
||||
* Initalise the drag and drop upload interface
|
||||
@@ -765,7 +763,10 @@ M.course_dndupload = {
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 1) {
|
||||
this.originalUnloadEvent = window.onbeforeunload;
|
||||
self.reportUploadDirtyState(true);
|
||||
// Trigger form upload start events.
|
||||
require(['core_form/events'], function(FormEvent) {
|
||||
FormEvent.triggerUploadStarted(section.get('id'));
|
||||
});
|
||||
}
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 200) {
|
||||
@@ -793,7 +794,10 @@ M.course_dndupload = {
|
||||
} else {
|
||||
new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
|
||||
}
|
||||
self.reportUploadDirtyState(false);
|
||||
// Trigger form upload complete events.
|
||||
require(['core_form/events'], function(FormEvent) {
|
||||
FormEvent.triggerUploadCompleted(section.get('id'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1025,7 +1029,10 @@ M.course_dndupload = {
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 1) {
|
||||
this.originalUnloadEvent = window.onbeforeunload;
|
||||
self.reportUploadDirtyState(true);
|
||||
// Trigger form upload start events.
|
||||
require(['core_form/events'], function(FormEvent) {
|
||||
FormEvent.triggerUploadStarted(section.get('id'));
|
||||
});
|
||||
}
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 200) {
|
||||
@@ -1043,13 +1050,24 @@ M.course_dndupload = {
|
||||
} else {
|
||||
// Error - remove the dummy element
|
||||
resel.parent.removeChild(resel.li);
|
||||
// Trigger form upload complete events.
|
||||
require(['core_form/events'], function(FormEvent) {
|
||||
FormEvent.triggerUploadCompleted(section.get('id'));
|
||||
});
|
||||
new M.core.alert({message: result.error});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Trigger form upload complete events.
|
||||
require(['core_form/events'], function(FormEvent) {
|
||||
FormEvent.triggerUploadCompleted(section.get('id'));
|
||||
});
|
||||
new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
|
||||
}
|
||||
self.reportUploadDirtyState(false);
|
||||
// Trigger form upload complete events.
|
||||
require(['core_form/events'], function(FormEvent) {
|
||||
FormEvent.triggerUploadCompleted(section.get('id'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1083,24 +1101,5 @@ M.course_dndupload = {
|
||||
if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {
|
||||
M.core.actionmenu.newDOMNode(node);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the event to prevent user navigate away when upload progress still running.
|
||||
*
|
||||
* @param {bool} enable true if upload progress is running, false otherwise
|
||||
*/
|
||||
reportUploadDirtyState: function(enable) {
|
||||
if (!enable) {
|
||||
window.onbeforeunload = this.originalUnloadEvent;
|
||||
} else {
|
||||
window.onbeforeunload = function(e) {
|
||||
var warningMessage = M.util.get_string('changesmadereallygoaway', 'moodle');
|
||||
if (e) {
|
||||
e.returnValue = warningMessage;
|
||||
}
|
||||
return warningMessage;
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+24
-6
@@ -4720,21 +4720,39 @@ function course_get_recent_courses(int $userid = null, int $limit = 0, int $offs
|
||||
$basefields = array('id', 'idnumber', 'summary', 'summaryformat', 'startdate', 'enddate', 'category',
|
||||
'shortname', 'fullname', 'timeaccess', 'component', 'visible');
|
||||
|
||||
$sort = trim($sort);
|
||||
if (empty($sort)) {
|
||||
$sort = 'timeaccess DESC';
|
||||
} else {
|
||||
// The SQL string for sorting can define sorting by multiple columns.
|
||||
$rawsorts = explode(',', $sort);
|
||||
$sorts = array();
|
||||
// Validate and trim the sort parameters in the SQL string for sorting.
|
||||
foreach ($rawsorts as $rawsort) {
|
||||
$rawsort = trim($rawsort);
|
||||
$sorts[] = trim($rawsort);
|
||||
$sort = trim($rawsort);
|
||||
$sortparams = explode(' ', $sort);
|
||||
// A valid sort statement can not have more than 2 params (ex. 'summary desc' or 'timeaccess').
|
||||
if (count($sortparams) > 2) {
|
||||
throw new invalid_parameter_exception(
|
||||
'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].');
|
||||
}
|
||||
$sortfield = trim($sortparams[0]);
|
||||
// Validate the value which defines the field to sort by.
|
||||
if (!in_array($sortfield, $basefields)) {
|
||||
throw new invalid_parameter_exception('Invalid field in the sort parameter, allowed fields: ' .
|
||||
implode(', ', $basefields) . '.');
|
||||
}
|
||||
$sortdirection = isset($sortparams[1]) ? trim($sortparams[1]) : '';
|
||||
// Validate the value which defines the sort direction (if present).
|
||||
$allowedsortdirections = ['asc', 'desc'];
|
||||
if (!empty($sortdirection) && !in_array(strtolower($sortdirection), $allowedsortdirections)) {
|
||||
throw new invalid_parameter_exception('Invalid sort direction in the sort parameter, allowed values: ' .
|
||||
implode(', ', $allowedsortdirections) . '.');
|
||||
}
|
||||
$sorts[] = $sort;
|
||||
}
|
||||
$sort = implode(',', $sorts);
|
||||
}
|
||||
|
||||
$orderby = "ORDER BY $sort";
|
||||
|
||||
$ctxfields = context_helper::get_preload_record_columns_sql('ctx');
|
||||
|
||||
$coursefields = 'c.' .join(',', $basefields);
|
||||
@@ -4767,7 +4785,7 @@ function course_get_recent_courses(int $userid = null, int $limit = 0, int $offs
|
||||
AND ue.timestart < :now1
|
||||
AND (ue.timeend = 0 OR ue.timeend > :now2)
|
||||
))
|
||||
$orderby";
|
||||
ORDER BY $sort";
|
||||
|
||||
$now = round(time(), -2); // Improves db caching.
|
||||
$params = ['userid' => $userid, 'contextlevel' => CONTEXT_COURSE, 'visible' => 1, 'status' => ENROL_USER_ACTIVE,
|
||||
|
||||
@@ -621,10 +621,23 @@ abstract class moodleform_mod extends moodleform {
|
||||
if (!empty($CFG->enableavailability)) {
|
||||
// Add special button to end of previous section if groups/groupings
|
||||
// are enabled.
|
||||
if ($this->_features->groups || $this->_features->groupings) {
|
||||
|
||||
$availabilityplugins = \core\plugininfo\availability::get_enabled_plugins();
|
||||
$groupavailability = $this->_features->groups && array_key_exists('group', $availabilityplugins);
|
||||
$groupingavailability = $this->_features->groupings && array_key_exists('grouping', $availabilityplugins);
|
||||
|
||||
if ($groupavailability || $groupingavailability) {
|
||||
// When creating the button, we need to set type=button to prevent it behaving as a submit.
|
||||
$mform->addElement('static', 'restrictgroupbutton', '',
|
||||
html_writer::tag('button', get_string('restrictbygroup', 'availability'),
|
||||
array('id' => 'restrictbygroup', 'disabled' => 'disabled', 'class' => 'btn btn-secondary')));
|
||||
html_writer::tag('button', get_string('restrictbygroup', 'availability'), [
|
||||
'id' => 'restrictbygroup',
|
||||
'type' => 'button',
|
||||
'disabled' => 'disabled',
|
||||
'class' => 'btn btn-secondary',
|
||||
'data-groupavailability' => $groupavailability,
|
||||
'data-groupingavailability' => $groupingavailability
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
// Availability field. This is just a textarea; the user interface
|
||||
|
||||
@@ -8,32 +8,26 @@ Feature: Edit completion settings of an activity
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | enablecompletion |
|
||||
| Course 1 | C1 | 1 |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Page" to section "1" and I fill the form with:
|
||||
| Name | TestPage |
|
||||
| Description | x |
|
||||
| Page content | x |
|
||||
| Completion tracking | 2 |
|
||||
| Require view | 1 |
|
||||
And I am on "Course 1" course homepage
|
||||
And the following "activities" exist:
|
||||
| activity | course | idnumber | intro | name | completion | completionview |
|
||||
| page | C1 | p1 | x | TestPage | 2 | 1 |
|
||||
|
||||
Scenario: Completion is not locked when the activity has not yet been viewed
|
||||
Given I click on "Edit settings" "link" in the "TestPage" activity
|
||||
Given I am on the TestPage "Page Activity editing" page logged in as admin
|
||||
When I expand all fieldsets
|
||||
Then I should see "Completion tracking"
|
||||
And I should not see "Completion options locked"
|
||||
|
||||
Scenario: Completion is locked after the activity has been viewed
|
||||
Given I follow "TestPage"
|
||||
When I follow "Edit settings"
|
||||
Given I am on the TestPage "Page Activity" page logged in as admin
|
||||
When I am on the TestPage "Page Activity editing" page
|
||||
And I expand all fieldsets
|
||||
Then I should see "Completion options locked"
|
||||
|
||||
@javascript
|
||||
Scenario: Pressing the unlock button allows the user to edit completion settings
|
||||
Given I follow "TestPage"
|
||||
When I navigate to "Edit settings" in current page administration
|
||||
Given I am on the TestPage "Page Activity" page logged in as admin
|
||||
When I am on the TestPage "Page Activity editing" page
|
||||
And I expand all fieldsets
|
||||
And I press "Unlock completion options"
|
||||
Then I should see "Completion options unlocked"
|
||||
@@ -45,8 +39,8 @@ Feature: Edit completion settings of an activity
|
||||
|
||||
@javascript
|
||||
Scenario: Even when completion is locked, the user can still set the date
|
||||
Given I follow "TestPage"
|
||||
When I navigate to "Edit settings" in current page administration
|
||||
Given I am on the TestPage "Page Activity" page logged in as admin
|
||||
And I am on the TestPage "Page Activity editing" page
|
||||
And I expand all fieldsets
|
||||
When I click on "id_completionexpected_enabled" "checkbox"
|
||||
And I set the field "id_completionexpected_year" to "2013"
|
||||
|
||||
@@ -131,10 +131,8 @@ Feature: Toggle activities visibility from the course page
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
And the following config values are set as admin:
|
||||
| allowstealth | 1 |
|
||||
And I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add the "Recent activity" block
|
||||
|
||||
@@ -42,9 +42,9 @@ Feature: Activity navigation
|
||||
| wiki | Wiki 1 | Test wiki description | C1 | wiki1 | 6 |
|
||||
| workshop | Workshop 1 | Test workshop description | C1 | workshop1 | 6 |
|
||||
| assign | Assignment 1 | Test assignment description | C2 | assign21 | 0 |
|
||||
And I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
And the following config values are set as admin:
|
||||
| allowstealth | 1 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
# Stealth activity.
|
||||
And I click on "Hide" "link" in the "Forum 1" activity
|
||||
|
||||
@@ -45,9 +45,7 @@ Feature: Activity navigation involving activities with access restrictions
|
||||
|
||||
@javascript
|
||||
Scenario: Activity navigation involving activities with access restrictions
|
||||
Given I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I follow "Page 1"
|
||||
Given I am on the "Page 1" "page activity" page logged in as student1
|
||||
Then I should see "Page 2" in the "#next-activity-link" "css_element"
|
||||
# Activity that has access restriction should not show up in the dropdown.
|
||||
And the "Jump to..." select box should not contain "Page 3"
|
||||
@@ -56,7 +54,7 @@ Feature: Activity navigation involving activities with access restrictions
|
||||
And I should see "Page 2" in the "#prev-activity-link" "css_element"
|
||||
And the "Jump to..." select box should not contain "Page 3"
|
||||
# Navigate to Page 2.
|
||||
And I follow "Page 2"
|
||||
And I click on "Page 2" "link" in the "page-content" "region"
|
||||
# Since Page 2 has now been viewed and deemed completed, Page 3 can now be accessed.
|
||||
And I should see "Page 3" in the "#next-activity-link" "css_element"
|
||||
And the "Jump to..." select box should contain "Page 3"
|
||||
|
||||
@@ -11,16 +11,16 @@ Feature: Add activities to courses
|
||||
| student2 | Student | 2 | student2@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | topics |
|
||||
| Course 1 | Course 1 | topics |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
| student1 | Course 1 | student |
|
||||
| student2 | Course 1 | student |
|
||||
|
||||
@javascript
|
||||
Scenario: Add an activity to a course
|
||||
Given I am on the "Course 1" Course page logged in as admin
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
When I add a "Database" to section "3" and I fill the form with:
|
||||
| Name | Test name |
|
||||
| Description | Test database description |
|
||||
@@ -38,15 +38,18 @@ Feature: Add activities to courses
|
||||
|
||||
@javascript
|
||||
Scenario: Add an activity supplying only the name
|
||||
Given I am on the "Course 1" Course page logged in as admin
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
When I add a "Database" to section "3" and I fill the form with:
|
||||
| Name | Test name |
|
||||
Then I should see "Test name"
|
||||
|
||||
@javascript
|
||||
Scenario: Set activity description to required then add an activity supplying only the name
|
||||
Given I set the following administration settings values:
|
||||
| requiremodintro | Yes |
|
||||
When I am on "Course 1" course homepage
|
||||
Given the following config values are set as admin:
|
||||
| requiremodintro | 1 |
|
||||
And I am on the "Course 1" Course page logged in as admin
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Database" to section "3" and I fill the form with:
|
||||
| Name | Test name |
|
||||
Then I should see "Required"
|
||||
|
||||
@@ -1346,44 +1346,6 @@ class behat_course extends behat_base {
|
||||
return $this->getSession()->getPage()->findButton(get_string('turneditingoff')) ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the category with the given idnumber.
|
||||
*
|
||||
* Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return string
|
||||
* @throws ExpectationException
|
||||
*/
|
||||
protected function get_category_id($idnumber) {
|
||||
global $DB;
|
||||
try {
|
||||
return $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber), MUST_EXIST);
|
||||
} catch (dml_missing_record_exception $ex) {
|
||||
throw new ExpectationException(sprintf("There is no category in the database with the idnumber '%s'", $idnumber),
|
||||
$this->getSession());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the course with the given idnumber.
|
||||
*
|
||||
* Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return string
|
||||
* @throws ExpectationException
|
||||
*/
|
||||
protected function get_course_id($idnumber) {
|
||||
global $DB;
|
||||
try {
|
||||
return $DB->get_field('course', 'id', array('idnumber' => $idnumber), MUST_EXIST);
|
||||
} catch (dml_missing_record_exception $ex) {
|
||||
throw new ExpectationException(sprintf("There is no course in the database with the idnumber '%s'", $idnumber),
|
||||
$this->getSession());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the category node from within the listing on the management page.
|
||||
*
|
||||
|
||||
@@ -20,15 +20,12 @@ Feature: Managers can create courses
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add the "Latest announcements" block
|
||||
Then "Latest announcements" "block" should exist
|
||||
And I follow "Announcements"
|
||||
And I am on the Announcements "forum activity" page
|
||||
And "Add a new topic" "link" should exist
|
||||
And "Subscription mode > Forced subscription" "link" should not exist in current page administration
|
||||
And "Subscription mode > Forced subscription" "text" should exist in current page administration
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Announcements"
|
||||
And I am on the Announcements "forum activity" page logged in as student1
|
||||
And "Add a new topic" "link" should not exist
|
||||
And "Forced subscription" "text" should exist in current page administration
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ Feature: Courses can be set up to display dates relative to the user's enrolment
|
||||
|
||||
@javascript
|
||||
Scenario: Create a course with relative dates feature disabled
|
||||
Given I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
Given the following config values are set as admin:
|
||||
| enablecourserelativedates | 0 |
|
||||
And I log in as "admin"
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
When I press "Add a new course"
|
||||
@@ -18,20 +18,19 @@ Feature: Courses can be set up to display dates relative to the user's enrolment
|
||||
|
||||
@javascript
|
||||
Scenario: Create a course with relative dates feature enabled
|
||||
Given I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
Given the following config values are set as admin:
|
||||
| enablecourserelativedates | 1 |
|
||||
And I log in as "admin"
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
When I press "Add a new course"
|
||||
And I wait until the page is ready
|
||||
Then I should see "Relative dates mode"
|
||||
And I should see "Relative dates mode cannot be changed once the course has been created."
|
||||
|
||||
Scenario: Edit courses with relative dates feature enabled
|
||||
Given I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
Given the following config values are set as admin:
|
||||
| enablecourserelativedates | 1 |
|
||||
And I log in as "admin"
|
||||
And I create a course with:
|
||||
| Course full name | Course 1 |
|
||||
| Course short name | C1 |
|
||||
|
||||
@@ -13,11 +13,11 @@ Feature: Users can request and approve courses
|
||||
|
||||
Scenario: Simple course request workflow
|
||||
Given the following "system role assigns" exist:
|
||||
| user | course | role |
|
||||
| user | course | role |
|
||||
| user2 | Acceptance test site | manager |
|
||||
Given I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
And the following config values are set as admin:
|
||||
| lockrequestcategory | 1 |
|
||||
Given I log in as "admin"
|
||||
And I set the following system permissions of "Authenticated user" role:
|
||||
| capability | permission |
|
||||
| moodle/course:request | Allow |
|
||||
|
||||
@@ -20,8 +20,7 @@ Feature: View subfolders in a course in-line
|
||||
| Name | Test folder |
|
||||
| Display folder contents | On a separate page |
|
||||
| Show subfolders expanded | |
|
||||
And I should see "Test folder"
|
||||
And I follow "Test folder"
|
||||
And I am on the "Test folder" "folder activity" page
|
||||
And I press "Edit"
|
||||
And I press "Create folder"
|
||||
And I set the field "New folder name" to "Test subfolder 1"
|
||||
@@ -32,7 +31,7 @@ Feature: View subfolders in a course in-line
|
||||
Scenario: Add a folder with two subfolders - view on separate page
|
||||
Given I am on "Course 1" course homepage
|
||||
And I should not see "Test subfolder 1"
|
||||
And I follow "Test folder"
|
||||
And I am on the "Test folder" "folder activity" page
|
||||
And I should see "Test subfolder 1"
|
||||
And I press "Edit"
|
||||
And I press "Create folder"
|
||||
@@ -41,13 +40,13 @@ Feature: View subfolders in a course in-line
|
||||
And I press "Save changes"
|
||||
When I am on "Course 1" course homepage
|
||||
Then I should not see "Test subfolder 2"
|
||||
And I follow "Test folder"
|
||||
And I am on the "Test folder" "folder activity" page
|
||||
And I should see "Test subfolder 2"
|
||||
Given I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test folder" "folder activity editing" page
|
||||
And I set the field "Show subfolders expanded" to "1"
|
||||
When I am on "Course 1" course homepage
|
||||
Then I should not see "Test subfolder 2"
|
||||
And I follow "Test folder"
|
||||
And I am on the "Test folder" "folder activity" page
|
||||
And I should see "Test subfolder 2"
|
||||
|
||||
@javascript
|
||||
@@ -63,8 +62,7 @@ Feature: View subfolders in a course in-line
|
||||
And I press "Save and return to course"
|
||||
Then I should see "Test subfolder 1"
|
||||
And I should not see "Test sub subfolder"
|
||||
Given I open "Test folder" actions menu
|
||||
When I click on "Edit settings" "link" in the "Test folder" activity
|
||||
And I am on the "Test folder" "folder activity editing" page
|
||||
And I set the field "Show subfolders expanded" to "1"
|
||||
And I press "Save and return to course"
|
||||
Then I should see "Test subfolder 1"
|
||||
|
||||
@@ -4550,7 +4550,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 0,
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 1,
|
||||
'expecteddbqueries' => 4,
|
||||
'expectedresult' => $buildexpectedresult(0, 0)
|
||||
],
|
||||
'less than query limit' => [
|
||||
@@ -4558,7 +4558,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 2,
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 1,
|
||||
'expecteddbqueries' => 2,
|
||||
'expectedresult' => $buildexpectedresult(2, 0)
|
||||
],
|
||||
'more than query limit' => [
|
||||
@@ -4566,7 +4566,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 7,
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 3,
|
||||
'expecteddbqueries' => 4,
|
||||
'expectedresult' => $buildexpectedresult(7, 0)
|
||||
],
|
||||
'limit less than query limit' => [
|
||||
@@ -4574,7 +4574,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 7,
|
||||
'limit' => 2,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 1,
|
||||
'expecteddbqueries' => 2,
|
||||
'expectedresult' => $buildexpectedresult(2, 0)
|
||||
],
|
||||
'limit less than query limit with offset' => [
|
||||
@@ -4582,7 +4582,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 7,
|
||||
'limit' => 2,
|
||||
'offset' => 2,
|
||||
'expecteddbqueries' => 1,
|
||||
'expecteddbqueries' => 2,
|
||||
'expectedresult' => $buildexpectedresult(2, 2)
|
||||
],
|
||||
'limit less than total' => [
|
||||
@@ -4590,7 +4590,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 9,
|
||||
'limit' => 6,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 2,
|
||||
'expecteddbqueries' => 3,
|
||||
'expectedresult' => $buildexpectedresult(6, 0)
|
||||
],
|
||||
'less results than limit' => [
|
||||
@@ -4598,7 +4598,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 9,
|
||||
'limit' => 20,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 3,
|
||||
'expecteddbqueries' => 4,
|
||||
'expectedresult' => $buildexpectedresult(9, 0)
|
||||
],
|
||||
'less results than limit exact divisible' => [
|
||||
@@ -4606,7 +4606,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 9,
|
||||
'limit' => 20,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 4,
|
||||
'expecteddbqueries' => 5,
|
||||
'expectedresult' => $buildexpectedresult(9, 0)
|
||||
],
|
||||
'less results than limit with offset' => [
|
||||
@@ -4614,7 +4614,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 9,
|
||||
'limit' => 10,
|
||||
'offset' => 5,
|
||||
'expecteddbqueries' => 2,
|
||||
'expecteddbqueries' => 3,
|
||||
'expectedresult' => $buildexpectedresult(4, 5)
|
||||
],
|
||||
];
|
||||
@@ -5442,7 +5442,16 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
// Every course accessed, order by shortname DESC. The last create course ($course[2]) should have the greater shortname.
|
||||
$result = course_get_recent_courses($student->id, 0, 0, 'shortname DESC');
|
||||
$this->assertCount(3, $result);
|
||||
$this->assertEquals($courses[2]->id, array_shift($result)->id);
|
||||
$this->assertEquals($courses[2]->id, array_values($result)[0]->id);
|
||||
$this->assertEquals($courses[1]->id, array_values($result)[1]->id);
|
||||
$this->assertEquals($courses[0]->id, array_values($result)[2]->id);
|
||||
|
||||
// Every course accessed, order by shortname ASC.
|
||||
$result = course_get_recent_courses($student->id, 0, 0, 'shortname ASC');
|
||||
$this->assertCount(3, $result);
|
||||
$this->assertEquals($courses[0]->id, array_values($result)[0]->id);
|
||||
$this->assertEquals($courses[1]->id, array_values($result)[1]->id);
|
||||
$this->assertEquals($courses[2]->id, array_values($result)[2]->id);
|
||||
|
||||
$guestcourse = $generator->create_course(
|
||||
(object)array('shortname' => 'guestcourse',
|
||||
@@ -5464,6 +5473,56 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
$this->assertArrayNotHasKey($courses[0]->id, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the validation of the sort value in course_get_recent_courses().
|
||||
*
|
||||
* @dataProvider course_get_recent_courses_sort_validation_provider
|
||||
* @param string $sort The sort value
|
||||
* @param string $expectedexceptionmsg The expected exception message
|
||||
*/
|
||||
public function test_course_get_recent_courses_sort_validation(string $sort, string $expectedexceptionmsg) {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
if (!empty($expectedexceptionmsg)) {
|
||||
$this->expectException('invalid_parameter_exception');
|
||||
$this->expectExceptionMessage($expectedexceptionmsg);
|
||||
}
|
||||
course_get_recent_courses($user->id, 0, 0, $sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_course_get_recent_courses_sort_validation().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function course_get_recent_courses_sort_validation_provider() {
|
||||
return [
|
||||
'Invalid sort format (SQL injection attempt)' =>
|
||||
[
|
||||
'shortname DESC LIMIT 1--',
|
||||
'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].',
|
||||
],
|
||||
'Sort uses \'sort by\' field that does not exist' =>
|
||||
[
|
||||
'shortname DESC, xyz ASC',
|
||||
'Invalid field in the sort parameter, allowed fields: id, idnumber, summary, summaryformat, ' .
|
||||
'startdate, enddate, category, shortname, fullname, timeaccess, component, visible.',
|
||||
],
|
||||
'Sort uses invalid value for the sorting direction' =>
|
||||
[
|
||||
'shortname xyz, lastaccess',
|
||||
'Invalid sort direction in the sort parameter, allowed values: asc, desc.',
|
||||
],
|
||||
'Valid sort format' =>
|
||||
[
|
||||
'shortname asc, timeaccess',
|
||||
''
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the course_get_recent_courses function.
|
||||
*/
|
||||
|
||||
@@ -2765,7 +2765,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
/**
|
||||
* Test cases for the get_enrolled_courses_by_timeline_classification test.
|
||||
*/
|
||||
public function get_get_enrolled_courses_by_timeline_classification_test_cases() {
|
||||
public function get_get_enrolled_courses_by_timeline_classification_test_cases():array {
|
||||
$now = time();
|
||||
$day = 86400;
|
||||
|
||||
@@ -2864,6 +2864,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 2,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0
|
||||
],
|
||||
@@ -2873,6 +2874,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
|
||||
'expectednextoffset' => 15
|
||||
],
|
||||
@@ -2881,6 +2883,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 2,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => ['afuture', 'bfuture'],
|
||||
'expectednextoffset' => 4
|
||||
],
|
||||
@@ -2889,6 +2892,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 2,
|
||||
'offset' => 2,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => ['bfuture', 'cfuture'],
|
||||
'expectednextoffset' => 7
|
||||
],
|
||||
@@ -2897,6 +2901,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 5,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
|
||||
'expectednextoffset' => 13
|
||||
],
|
||||
@@ -2905,6 +2910,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 10,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
|
||||
'expectednextoffset' => 15
|
||||
],
|
||||
@@ -2913,6 +2919,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 10,
|
||||
'offset' => 5,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
|
||||
'expectednextoffset' => 15
|
||||
],
|
||||
@@ -2921,6 +2928,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'all',
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => [
|
||||
'afuture',
|
||||
'ainprogress',
|
||||
@@ -2945,6 +2953,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => [
|
||||
'afuture',
|
||||
'ainprogress',
|
||||
@@ -2959,6 +2968,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => [
|
||||
'bpast',
|
||||
'cfuture',
|
||||
@@ -2973,9 +2983,161 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 50,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 50
|
||||
],
|
||||
'all limit and offset with sort ul.timeaccess desc' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'inprogress',
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'sort' => 'ul.timeaccess desc',
|
||||
'expectedcourses' => [
|
||||
'ainprogress',
|
||||
'binprogress',
|
||||
'cinprogress',
|
||||
'dinprogress',
|
||||
'einprogress'
|
||||
],
|
||||
'expectednextoffset' => 15
|
||||
],
|
||||
'all limit and offset with sort sql injection for sort or 1==1' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => 'ul.timeaccess desc or 1==1',
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with sql injection of sort a custom one' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "ul.timeaccess LIMIT 1--",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong sort direction' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "ul.timeaccess abcdasc",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong sort direction' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "ul.timeaccess.foo ascd",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong sort param' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "foobar",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong field name' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "ul.foobar",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong field separator' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "ul.timeaccess.foo",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong field separator #' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "ul#timeaccess",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong field separator $' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => 'ul$timeaccess',
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong field name' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => 'timeaccess123',
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with no sort direction for ul' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'inprogress',
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'sort' => "ul.timeaccess",
|
||||
'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
|
||||
'expectednextoffset' => 15,
|
||||
],
|
||||
'all limit and offset with valid field name and no prefix, test for ul' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'inprogress',
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'sort' => "timeaccess",
|
||||
'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
|
||||
'expectednextoffset' => 15,
|
||||
],
|
||||
'all limit and offset with valid field name and no prefix' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "fullname",
|
||||
'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
|
||||
'expectednextoffset' => 10,
|
||||
],
|
||||
'all limit and offset with valid field name and no prefix and with sort direction' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "fullname desc",
|
||||
'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
|
||||
'expectednextoffset' => 10,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2987,16 +3149,20 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
* @param string $classification Timeline classification
|
||||
* @param int $limit Maximum number of results
|
||||
* @param int $offset Offset the unfiltered courses result set by this amount
|
||||
* @param string $sort sort the courses
|
||||
* @param array $expectedcourses Expected courses in result
|
||||
* @param int $expectednextoffset Expected next offset value in result
|
||||
* @param string|null $expectedexception Expected exception string
|
||||
*/
|
||||
public function test_get_enrolled_courses_by_timeline_classification(
|
||||
$coursedata,
|
||||
$classification,
|
||||
$limit,
|
||||
$offset,
|
||||
$sort,
|
||||
$expectedcourses,
|
||||
$expectednextoffset
|
||||
$expectednextoffset,
|
||||
$expectedexception = null
|
||||
) {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
@@ -3013,6 +3179,11 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
|
||||
$this->setUser($student);
|
||||
|
||||
if (isset($expectedexception)) {
|
||||
$this->expectException('coding_exception');
|
||||
$this->expectExceptionMessage($expectedexception);
|
||||
}
|
||||
|
||||
// NOTE: The offset applies to the unfiltered full set of courses before the classification
|
||||
// filtering is done.
|
||||
// E.g. In our example if an offset of 2 is given then it would mean the first
|
||||
@@ -3021,7 +3192,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
$classification,
|
||||
$limit,
|
||||
$offset,
|
||||
'shortname ASC'
|
||||
$sort
|
||||
);
|
||||
$result = external_api::clean_returnvalue(
|
||||
core_course_external::get_enrolled_courses_by_timeline_classification_returns(),
|
||||
@@ -3032,7 +3203,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
return $course['shortname'];
|
||||
}, $result['courses']);
|
||||
|
||||
$this->assertEquals($expectedcourses, $actual);
|
||||
$this->assertEqualsCanonicalizing($expectedcourses, $actual);
|
||||
$this->assertEquals($expectednextoffset, $result['nextoffset']);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,12 +55,16 @@ class field_controller extends \core_customfield\field_controller {
|
||||
|
||||
$mform->addElement('text', 'configdata[displaysize]', get_string('displaysize', 'customfield_text'), ['size' => 6]);
|
||||
$mform->setType('configdata[displaysize]', PARAM_INT);
|
||||
$mform->setDefault('configdata[displaysize]', 50);
|
||||
if (!$this->get_configdata_property('displaysize')) {
|
||||
$mform->setDefault('configdata[displaysize]', 50);
|
||||
}
|
||||
$mform->addRule('configdata[displaysize]', null, 'numeric', null, 'client');
|
||||
|
||||
$mform->addElement('text', 'configdata[maxlength]', get_string('maxlength', 'customfield_text'), ['size' => 6]);
|
||||
$mform->setType('configdata[maxlength]', PARAM_INT);
|
||||
$mform->setDefault('configdata[maxlength]', 1333);
|
||||
if (!$this->get_configdata_property('maxlength')) {
|
||||
$mform->setDefault('configdata[maxlength]', 1333);
|
||||
}
|
||||
$mform->addRule('configdata[maxlength]', null, 'numeric', null, 'client');
|
||||
|
||||
$mform->addElement('selectyesno', 'configdata[ispassword]', get_string('ispassword', 'customfield_text'));
|
||||
@@ -113,7 +117,7 @@ class field_controller extends \core_customfield\field_controller {
|
||||
$errors['configdata[link]'] = get_string('errorconfiglinkplaceholder', 'customfield_text');
|
||||
} else if (!validateUrlSyntax(str_replace('$$', 'XYZ', $link), 's+H?S?F-E-u-P-a?I?p?f?q?r?')) {
|
||||
// This validation is more strict than PARAM_URL - it requires the protocol and it must be either http or https.
|
||||
$errors['configdata[link]'] = get_string('errorconfigdisplaysize', 'customfield_text');
|
||||
$errors['configdata[link]'] = get_string('errorconfiglinksyntax', 'customfield_text');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
This files describes API changes in /dataformat/ download system,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 3.9.8 ===
|
||||
* Optional $callback for the following \core\dataformat methods now receive a second argument to define
|
||||
whether the current format writer supports HTML:
|
||||
- download_data()
|
||||
- write_data()
|
||||
- write_data_to_filearea()
|
||||
|
||||
=== 3.9 ===
|
||||
* The following methods have been added to the base dataformat class to allow instances to export to a local
|
||||
file. They can be overridden in extending classes to define how files should be created:
|
||||
|
||||
@@ -188,7 +188,9 @@ $removeenabled = $canunenrol ? '' : 'disabled="disabled"';
|
||||
</td>
|
||||
<td id="buttonscell">
|
||||
<div id="addcontrols">
|
||||
<input name="add" <?php echo $addenabled; ?> id="add" type="submit" value="<?php echo $OUTPUT->larrow().' '.get_string('add'); ?>" title="<?php print_string('add'); ?>" /><br />
|
||||
<input class="btn btn-secondary" name="add" <?php echo $addenabled; ?> id="add" type="submit"
|
||||
value="<?php echo $OUTPUT->larrow() . ' ' . get_string('add'); ?>"
|
||||
title="<?php print_string('add'); ?>" /><br />
|
||||
|
||||
<div class="enroloptions">
|
||||
|
||||
@@ -205,7 +207,9 @@ $removeenabled = $canunenrol ? '' : 'disabled="disabled"';
|
||||
</div>
|
||||
|
||||
<div id="removecontrols">
|
||||
<input name="remove" id="remove" <?php echo $removeenabled; ?> type="submit" value="<?php echo get_string('remove').' '.$OUTPUT->rarrow(); ?>" title="<?php print_string('remove'); ?>" />
|
||||
<input class="btn btn-secondary" name="remove" id="remove" <?php echo $removeenabled; ?> type="submit"
|
||||
value="<?php echo get_string('remove') . ' ' . $OUTPUT->rarrow(); ?>"
|
||||
title="<?php print_string('remove'); ?>" />
|
||||
</div>
|
||||
</td>
|
||||
<td id="potentialcell">
|
||||
|
||||
@@ -98,6 +98,7 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I press "Enrol me"
|
||||
And I should see "You are enrolled in the course"
|
||||
And I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
@@ -118,6 +119,7 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I press "Enrol me"
|
||||
And I should see "You are enrolled in the course"
|
||||
And I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
@@ -1036,6 +1036,59 @@ class core_enrollib_testcase extends advanced_testcase {
|
||||
$this->assertEquals([$course1->id, $course2->id, $course3->id, $course4->id], array_keys($courses));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for {@see test_enrol_get_my_courses_by_time}
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function enrol_get_my_courses_by_time_provider(): array {
|
||||
return [
|
||||
'No start or end time' =>
|
||||
[null, null, true],
|
||||
'Start time now, no end time' =>
|
||||
[0, null, true],
|
||||
'Start time now, end time in the future' =>
|
||||
[0, MINSECS, true],
|
||||
'Start time in the past, no end time' =>
|
||||
[-MINSECS, null, true],
|
||||
'Start time in the past, end time in the future' =>
|
||||
[-MINSECS, MINSECS, true],
|
||||
'Start time in the past, end time in the past' =>
|
||||
[-DAYSECS, -HOURSECS, false],
|
||||
'Start time in the future' =>
|
||||
[MINSECS, null, false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that expected course enrolments are returned when they have timestart / timeend specified
|
||||
*
|
||||
* @param int|null $timestartoffset Null for 0, otherwise offset from current time
|
||||
* @param int|null $timeendoffset Null for 0, otherwise offset from current time
|
||||
* @param bool $expectreturn
|
||||
*
|
||||
* @dataProvider enrol_get_my_courses_by_time_provider
|
||||
*/
|
||||
public function test_enrol_get_my_courses_by_time(?int $timestartoffset, ?int $timeendoffset, bool $expectreturn): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$time = time();
|
||||
$timestart = $timestartoffset === null ? 0 : $time + $timestartoffset;
|
||||
$timeend = $timeendoffset === null ? 0 : $time + $timeendoffset;
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$user = $this->getDataGenerator()->create_and_enrol($course, 'student', null, 'manual', $timestart, $timeend);
|
||||
$this->setUser($user);
|
||||
|
||||
$courses = enrol_get_my_courses();
|
||||
if ($expectreturn) {
|
||||
$this->assertCount(1, $courses);
|
||||
$this->assertEquals($course->id, reset($courses)->id);
|
||||
} else {
|
||||
$this->assertEmpty($courses);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* test_course_users
|
||||
*
|
||||
@@ -1362,4 +1415,77 @@ class core_enrollib_testcase extends advanced_testcase {
|
||||
$durationinday = $duration / DAYSECS;
|
||||
$this->assertEquals(9, $durationinday);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_enrolled_with_capabilities_join cannotmatchanyrows attribute.
|
||||
*
|
||||
* @dataProvider get_enrolled_with_capabilities_join_cannotmatchanyrows_data()
|
||||
* @param string $capability the tested capability
|
||||
* @param bool $useprohibit if the capability must be assigned to prohibit
|
||||
* @param int $expectedmatch expected cannotmatchanyrows value
|
||||
* @param int $expectedcount expceted count value
|
||||
*/
|
||||
public function test_get_enrolled_with_capabilities_join_cannotmatchanyrows(
|
||||
string $capability,
|
||||
bool $useprohibit,
|
||||
int $expectedmatch,
|
||||
int $expectedcount
|
||||
) {
|
||||
global $DB, $CFG;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$context = context_course::instance($course->id);
|
||||
|
||||
$roleid = $CFG->defaultuserroleid;
|
||||
|
||||
// Override capability if necessary.
|
||||
if ($useprohibit && $capability) {
|
||||
assign_capability($capability, CAP_PROHIBIT, $roleid, $context);
|
||||
}
|
||||
|
||||
// Check if we must enrol or not.
|
||||
$this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
|
||||
|
||||
$join = get_enrolled_with_capabilities_join($context, '', $capability);
|
||||
|
||||
// Execute query.
|
||||
$sql = "SELECT COUNT(DISTINCT u.id)
|
||||
FROM {user} u {$join->joins}
|
||||
WHERE {$join->wheres}";
|
||||
$countrecords = $DB->count_records_sql($sql, $join->params);
|
||||
|
||||
// Validate cannotmatchanyrows.
|
||||
$this->assertEquals($expectedmatch, $join->cannotmatchanyrows);
|
||||
$this->assertEquals($expectedcount, $countrecords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_get_enrolled_with_capabilities_join_cannotmatchanyrows
|
||||
*
|
||||
* @return @array of testing scenarios
|
||||
*/
|
||||
public function get_enrolled_with_capabilities_join_cannotmatchanyrows_data() {
|
||||
return [
|
||||
'no prohibits, no capability' => [
|
||||
'capability' => '',
|
||||
'useprohibit' => false,
|
||||
'expectedmatch' => 0,
|
||||
'expectedcount' => 1,
|
||||
],
|
||||
'no prohibits with capability' => [
|
||||
'capability' => 'moodle/course:manageactivities',
|
||||
'useprohibit' => false,
|
||||
'expectedmatch' => 0,
|
||||
'expectedcount' => 1,
|
||||
],
|
||||
'prohibits with capability' => [
|
||||
'capability' => 'moodle/course:manageactivities',
|
||||
'useprohibit' => true,
|
||||
'expectedmatch' => 1,
|
||||
'expectedcount' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,7 @@ Feature: Render H5P content using filters
|
||||
|
||||
@javascript @external
|
||||
Scenario: Render an external H5P content URL.
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
Given I am on the PageName1 "page activity editing" page logged in as teacher1
|
||||
And I set the field "Page content" to "<div>Go for it</div>https://moodle.h5p.com/content/1290772960722742119/embed"
|
||||
When I click on "Save and display" "button"
|
||||
And I wait until the page is ready
|
||||
@@ -36,10 +33,7 @@ Feature: Render H5P content using filters
|
||||
|
||||
@javascript
|
||||
Scenario: Add an external H5P content URL in a link with the URL. Should be rendered.
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
Given I am on the PageName1 "page activity editing" page logged in as teacher1
|
||||
# This content won't be displayed, so this scenario shouldn't be labeled as external.
|
||||
And I set the field "Page content" to "<a href='https://moodle.h5p.com/content/1290772960722742119/embed'>https://moodle.h5p.com/content/1290772960722742119/embed</a>"
|
||||
When I click on "Save and display" "button"
|
||||
@@ -48,10 +42,7 @@ Feature: Render H5P content using filters
|
||||
Then I should see "Lorum ipsum"
|
||||
|
||||
Scenario: Add an external H5P content URL in a link with text. Shouldn't be rendered.
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
Given I am on the PageName1 "page activity editing" page logged in as teacher1
|
||||
# This content won't be displayed, so this scenario shouldn't be labeled as external.
|
||||
And I set the field "Page content" to "<a href='https://moodle.h5p.com/content/1290772960722742119/embed'>Here you are the content</a>"
|
||||
When I click on "Save and display" "button"
|
||||
@@ -67,8 +58,7 @@ Feature: Render H5P content using filters
|
||||
| Name | ipsumFile |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager
|
||||
And I press "Save and return to course"
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the PageName1 "page activity editing" page
|
||||
And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
|
||||
And I click on "Browse repositories..." "button" in the "Insert H5P" "dialogue"
|
||||
And I click on "Server files" "link" in the ".fp-repo-area" "css_element"
|
||||
@@ -85,9 +75,7 @@ Feature: Render H5P content using filters
|
||||
Then I should see "Lorum ipsum"
|
||||
And I switch to the main frame
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "PageName1"
|
||||
And I am on the PageName1 "page activity" page logged in as student1
|
||||
# Switch to iframe created by filter
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
# Switch to iframe created by embed.php page
|
||||
@@ -104,8 +92,7 @@ Feature: Render H5P content using filters
|
||||
| Name | ipsumFile |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager
|
||||
And I press "Save and return to course"
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the PageName1 "page activity editing" page
|
||||
And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
|
||||
And I click on "Browse repositories..." "button" in the "Insert H5P" "dialogue"
|
||||
And I click on "Server files" "link" in the ".fp-repo-area" "css_element"
|
||||
@@ -129,8 +116,7 @@ Feature: Render H5P content using filters
|
||||
| Name | ipsumFileTeacher |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager
|
||||
And I press "Save and return to course"
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the PageName1 "page activity editing" page
|
||||
And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
|
||||
And I click on "Browse repositories..." "button" in the "Insert H5P" "dialogue"
|
||||
And I click on "Server files" "link" in the ".fp-repo-area" "css_element"
|
||||
@@ -153,8 +139,7 @@ Feature: Render H5P content using filters
|
||||
| Name | ipsumFile |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager
|
||||
And I press "Save and return to course"
|
||||
And I follow "PageName2"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the PageName2 "page activity editing" page
|
||||
And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
|
||||
And I click on "Browse repositories..." "button" in the "Insert H5P" "dialogue"
|
||||
And I click on "Server files" "link" in the ".fp-repo-area" "css_element"
|
||||
@@ -172,9 +157,7 @@ Feature: Render H5P content using filters
|
||||
And I should see "Lorum ipsum"
|
||||
And I switch to the main frame
|
||||
And I log out
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "PageName1"
|
||||
And I am on the PageName1 "page activity" page logged in as teacher1
|
||||
# Switch to iframe created by filter
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
# Switch to iframe created by embed.php page
|
||||
|
||||
+13
-10
@@ -47,23 +47,23 @@
|
||||
* @param int $fontsize the font size
|
||||
* @return string the latex document
|
||||
*/
|
||||
function construct_latex_document( $formula, $fontsize=12 ) {
|
||||
global $CFG;
|
||||
|
||||
$formula = filter_tex_sanitize_formula($formula);
|
||||
|
||||
function construct_latex_document($formula, $fontsize = 12) {
|
||||
// $fontsize don't affects to formula's size. $density can change size
|
||||
$doc = "\\documentclass[{$fontsize}pt]{article}\n";
|
||||
$doc = "\\documentclass[{$fontsize}pt]{article}\n";
|
||||
$doc .= get_config('filter_tex', 'latexpreamble');
|
||||
$doc .= "\\pagestyle{empty}\n";
|
||||
$doc .= "\\begin{document}\n";
|
||||
//dlnsk $doc .= "$ {$formula} $\n";
|
||||
if (preg_match("/^[[:space:]]*\\\\begin\\{(gather|align|alignat|multline).?\\}/i",$formula)) {
|
||||
if (preg_match("/^[[:space:]]*\\\\begin\\{(gather|align|alignat|multline).?\\}/i", $formula)) {
|
||||
$doc .= "$formula\n";
|
||||
} else {
|
||||
$doc .= "$ {$formula} $\n";
|
||||
}
|
||||
$doc .= "\\end{document}\n";
|
||||
|
||||
// Sanitize the whole document (rather than just the formula) to make sure no one can bypass sanitization
|
||||
// by using \newcommand in preamble to give an alias to a blocked command.
|
||||
$doc = filter_tex_sanitize_formula($doc);
|
||||
|
||||
return $doc;
|
||||
}
|
||||
|
||||
@@ -114,11 +114,14 @@
|
||||
$convertformat = 'png';
|
||||
}
|
||||
$filename = str_replace(".{$convertformat}", '', $filename);
|
||||
$tex = "{$this->temp_dir}/$filename.tex";
|
||||
$tex = "$filename.tex"; // Absolute paths won't work with openin_any = p setting.
|
||||
$dvi = "{$this->temp_dir}/$filename.dvi";
|
||||
$ps = "{$this->temp_dir}/$filename.ps";
|
||||
$img = "{$this->temp_dir}/$filename.{$convertformat}";
|
||||
|
||||
// Change directory to temp dir so that we can work with relative paths.
|
||||
chdir($this->temp_dir);
|
||||
|
||||
// turn the latex doc into a .tex file in the temp area
|
||||
$fh = fopen( $tex, 'w' );
|
||||
fputs( $fh, $doc );
|
||||
@@ -126,7 +129,7 @@
|
||||
|
||||
// run latex on document
|
||||
$command = "$pathlatex --interaction=nonstopmode --halt-on-error $tex";
|
||||
chdir( $this->temp_dir );
|
||||
|
||||
if ($this->execute($command, $log)) { // It allways False on Windows
|
||||
// return false;
|
||||
}
|
||||
|
||||
+30
-2
@@ -61,7 +61,7 @@ function filter_tex_get_executable($debug=false) {
|
||||
|
||||
function filter_tex_sanitize_formula($texexp) {
|
||||
/// Check $texexp against blacklist (whitelisting could be more complete but also harder to maintain)
|
||||
$tex_blacklist = array(
|
||||
$denylist = [
|
||||
'include','command','loop','repeat','open','toks','output',
|
||||
'input','catcode','name','^^',
|
||||
'\def','\edef','\gdef','\xdef',
|
||||
@@ -71,9 +71,37 @@ function filter_tex_sanitize_formula($texexp) {
|
||||
'\afterassignment','\expandafter','\noexpand','\special',
|
||||
'\let', '\futurelet','\else','\fi','\chardef','\makeatletter','\afterground',
|
||||
'\noexpand','\line','\mathcode','\item','\section','\mbox','\declarerobustcommand'
|
||||
];
|
||||
|
||||
$allowlist = ['inputenc'];
|
||||
|
||||
// Prepare the denylist for regular expression.
|
||||
$denylist = array_map(function($value){
|
||||
return '/' . preg_quote($value, '/') . '/i';
|
||||
}, $denylist);
|
||||
|
||||
// Prepare the allowlist for regular expression.
|
||||
$allowlist = array_map(function($value){
|
||||
return '/\bforbiddenkeyword_(' . preg_quote($value, '/') . ')\b/i';
|
||||
}, $allowlist);
|
||||
|
||||
// First, mangle all denied words.
|
||||
$texexp = preg_replace_callback($denylist,
|
||||
function($matches) {
|
||||
return 'forbiddenkeyword_' . $matches[0];
|
||||
},
|
||||
$texexp
|
||||
);
|
||||
|
||||
return str_ireplace($tex_blacklist, 'forbiddenkeyword', $texexp);
|
||||
// Then, change back the allowed words.
|
||||
$texexp = preg_replace_callback($allowlist,
|
||||
function($matches) {
|
||||
return $matches[1];
|
||||
},
|
||||
$texexp
|
||||
);
|
||||
|
||||
return $texexp;
|
||||
}
|
||||
|
||||
function filter_tex_get_cmd($pathname, $texexp) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user