Compare commits
272 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 76fe404dd5 | |||
| f3400f4fc5 | |||
| c872490dcf | |||
| e518de15f3 | |||
| 4fffa81e41 | |||
| fcd946c862 | |||
| b272406c74 | |||
| d0e0225a1a | |||
| 2649d1a01e | |||
| aa77a76c7a | |||
| 1b27cfd795 | |||
| da10ccd8d4 | |||
| cc7ea9fecd | |||
| 2cf0da5dc6 | |||
| a6fde372ab | |||
| 7dc747ea99 | |||
| 5a8881ac37 | |||
| b4eb274d7a | |||
| 21443080c8 | |||
| ea1a8336e5 | |||
| f68fedcf77 | |||
| cf3ae5bc2a | |||
| c8bb7a6911 | |||
| bf448de7aa | |||
| cbacf8ee58 | |||
| 37a29f7950 | |||
| b1dcb08e3f | |||
| 0ccf800f01 | |||
| b097d38fb3 | |||
| 1858e29d23 | |||
| bd5329b1a5 | |||
| 69596a6bb2 | |||
| 79510d5d73 | |||
| fce2d2f407 | |||
| 3b8e3ba59e | |||
| 2279eee0ab | |||
| 411f43dc5a | |||
| ef2a553bd4 | |||
| af1bd51b99 | |||
| b89ece2484 | |||
| 0c3b3c9b92 | |||
| 7fd423acfc | |||
| abac304ba9 | |||
| 677b67ac05 | |||
| 5f30a47660 | |||
| 7abeaf37ea | |||
| 79981290d9 | |||
| e96dab8594 | |||
| 6843ba0aad | |||
| 257e2b414a | |||
| b17d91c6f8 | |||
| 7debcb85b4 | |||
| 03b605f2eb | |||
| de0bab848d | |||
| dfdf8ba7c9 | |||
| f1f87dbd6b | |||
| 2885b47377 | |||
| 036cb9a997 | |||
| 732f27c770 | |||
| d6a8581abe | |||
| d37efc5160 | |||
| e0dc9aa297 | |||
| e939cee281 | |||
| 17be7f62d6 | |||
| b512186fce | |||
| ee3752e639 | |||
| e47c5527f6 | |||
| 99a48299f0 | |||
| 5a84debc90 | |||
| 70e6fb0a7d | |||
| 95ec4161f5 | |||
| bd41a5bbec | |||
| f02891c647 | |||
| 2d99010fc9 | |||
| 23ba0d96ef | |||
| 11205ec5ef | |||
| b01dd76331 | |||
| 278ce49b7f | |||
| 4cbe98f2e7 | |||
| a22aabb29f | |||
| 1ff279717e | |||
| 017f8a9c4d | |||
| 16c5835a80 | |||
| 090baf556e | |||
| 0b46994b0c | |||
| a229905760 | |||
| 1c7f1e0bed | |||
| 59596ddedc | |||
| 34dd7e7978 | |||
| e7d2f494c3 | |||
| 9c5febc47c | |||
| 4ddbcf2aec | |||
| 9dc72be2d4 | |||
| 05a5ed676c | |||
| aaf17fc8b6 | |||
| b870bc0577 | |||
| 9e582a6099 | |||
| ff402c26e5 | |||
| b12f914708 | |||
| e7d32b89ac | |||
| 22705fa4fb | |||
| 7c0954ccd1 | |||
| 0479c711c2 | |||
| de6c14ee71 | |||
| 1936b30c88 | |||
| f19f31736a | |||
| 2b01af17be | |||
| 439f75e6e1 | |||
| dec3d56032 | |||
| 452c7c72e0 | |||
| 0d0868ee97 | |||
| c98e7e7dbf | |||
| 90e18e28a2 | |||
| db7c650f8c | |||
| 79d0e63b18 | |||
| 962c8ef477 | |||
| 2036f2439e | |||
| 6bb4351e8b | |||
| 80fa181a26 | |||
| 6ca70dd59f | |||
| 712737e955 | |||
| 5e32d574c6 | |||
| 5620e0059a | |||
| 85b1e992e9 | |||
| 8d50911af5 | |||
| 5c871bff29 | |||
| 096f1e67c8 | |||
| 21cc09fa2d | |||
| e2a7a44d3e | |||
| b28a28ef0f | |||
| 30e9810589 | |||
| cf83791269 | |||
| 3f07fc246f | |||
| 3d023db5b0 | |||
| 13b48a0ae8 | |||
| 87223abc7a | |||
| e7458d017a | |||
| d2b81dd116 | |||
| 361f0fa524 | |||
| 7a23da59ce | |||
| 01ae473823 | |||
| 0aff5ccd01 | |||
| 7a511874b8 | |||
| a1b37845a8 | |||
| 071adb2fb1 | |||
| aa39ebbb99 | |||
| 4818dae2fc | |||
| dc38a92ec6 | |||
| 946405d44b | |||
| bdeda3ea9b | |||
| 01c04f4cf8 | |||
| 9ef8025e4c | |||
| efe895f1bb | |||
| a532f407bb | |||
| 86932a330b | |||
| db9a0e296a | |||
| 9ff59e99ff | |||
| 997990f94f | |||
| e5132eff2f | |||
| 7093578baf | |||
| 28026b923a | |||
| 0e4426818a | |||
| e9f6b9c2d6 | |||
| f1dd604fce | |||
| 017a3274fe | |||
| 3b9bfc1d79 | |||
| 77eb2ed79b | |||
| 90350915c2 | |||
| 825609ae08 | |||
| 5b3d55def7 | |||
| 67effa603e | |||
| 20b4efad5c | |||
| 11af0c5388 | |||
| 683840999d | |||
| f8537a9536 | |||
| 7b1e0a6980 | |||
| 838047106d | |||
| 84175f399f | |||
| 9e0f8fc7bf | |||
| c228866a46 | |||
| d82f68742b | |||
| da78523b15 | |||
| 5ceb711355 | |||
| 1d5ef6aa9a | |||
| d3b4e4a9a5 | |||
| d7ba3f656d | |||
| 48963bb7a7 | |||
| f36c96411d | |||
| 50d1e69df6 | |||
| f6c5fa3bfc | |||
| d838917d28 | |||
| 1af4ace4d6 | |||
| 5c85fb975d | |||
| 2ad6fecca5 | |||
| 1201526dbc | |||
| 6ccb50ea9d | |||
| 25498b6b62 | |||
| a2179f028c | |||
| a7920b3860 | |||
| 893fe94c89 | |||
| ba904934ec | |||
| b3feea921d | |||
| 22c346831e | |||
| 0703a8db55 | |||
| 176a323b4e | |||
| 42f3874b80 | |||
| 1f07219332 | |||
| 0c7e7645d5 | |||
| 9117af48ac | |||
| d1c24b02ab | |||
| 79fe493ab4 | |||
| d66398c356 | |||
| 6064ce62f5 | |||
| 88113fc983 | |||
| e19573d6bf | |||
| 3994ab206b | |||
| b9d84d03ca | |||
| f8fdd198a9 | |||
| 394d592fab | |||
| 64c625e590 | |||
| be0eb4369b | |||
| b3424d620d | |||
| 767419571e | |||
| aa63dca768 | |||
| 2dc4853ba5 | |||
| 5ab74afad4 | |||
| f25ecd9bfa | |||
| 88496af850 | |||
| d28386a29d | |||
| 4fa9ed7956 | |||
| 569a71daea | |||
| 4901ffe1ba | |||
| b10f6f5036 | |||
| 30e419078f | |||
| f90a55979a | |||
| 013735af51 | |||
| 6c890a6330 | |||
| 4f7dd1a54b | |||
| de8607067f | |||
| 15ff1352c2 | |||
| 384ab12c55 | |||
| 4a9c0b1410 | |||
| d2d9d762ad | |||
| a9fa52295a | |||
| eca7a9ce2b | |||
| bba6cc2f02 | |||
| e5c89630af | |||
| 491ee67cd9 | |||
| 9a3430f3fe | |||
| 79982a82a4 | |||
| 4ee0bdfda6 | |||
| d10d553890 | |||
| 22b481f73c | |||
| b867e4db40 | |||
| ccdaef7a68 | |||
| 952c8770b6 | |||
| 132ac7486d | |||
| a9f629fbf9 | |||
| c6708db889 | |||
| 0d7393c7ee | |||
| f3ae5116d0 | |||
| b01ce337f6 | |||
| bb433919cc | |||
| 1e16260ba7 | |||
| 4ec5aac734 | |||
| b04fab1a2a | |||
| 60a2798da7 | |||
| 5d000c6f35 | |||
| df0f9fa06d | |||
| af7422edc0 | |||
| 25edc77d10 | |||
| 5fa1ec0dfa |
@@ -0,0 +1,144 @@
|
||||
name: Windows Testing
|
||||
|
||||
on:
|
||||
workflow_dispatch
|
||||
env:
|
||||
php: 8.1
|
||||
|
||||
jobs:
|
||||
Grunt:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Set git to use LF
|
||||
run: |
|
||||
git config --global core.autocrlf false
|
||||
git config --global core.eol lf
|
||||
|
||||
- name: Checking out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Configuring node & npm
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Installing node stuff
|
||||
run: npm ci
|
||||
|
||||
- name: Running grunt
|
||||
run: npx grunt
|
||||
|
||||
- name: Looking for uncommitted changes
|
||||
# Add all files to the git index and then run diff --cached to see all changes.
|
||||
# This ensures that we get the status of all files, including new files.
|
||||
# We ignore npm-shrinkwrap.json to make the tasks immune to npm changes.
|
||||
run: |
|
||||
git add .
|
||||
git reset -- npm-shrinkwrap.json
|
||||
git diff --cached --exit-code
|
||||
|
||||
PHPUnit:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
php: 8.1
|
||||
# Ideally we should use mysql/mariadb, but they are 4x slower without tweaks and configuration
|
||||
# so let's run only postgres (1.5h vs 6h) only, If some day we want to improve the mysql runs,
|
||||
# this is the place to enable them.
|
||||
# db: mysqli
|
||||
db: pgsql
|
||||
extensions: exif, fileinfo, gd, intl, pgsql, mysql, redis, soap, sodium
|
||||
- os: windows-latest
|
||||
php: 8.0
|
||||
db: pgsql
|
||||
extensions: exif, fileinfo, gd, intl, pgsql, mysql, redis, soap, sodium
|
||||
|
||||
steps:
|
||||
- name: Setting up DB mysql
|
||||
if: ${{ matrix.db == 'mysqli' }}
|
||||
uses: shogo82148/actions-setup-mysql@v1
|
||||
with:
|
||||
mysql-version: 8.0
|
||||
user: test
|
||||
password: test
|
||||
|
||||
- name: Creating DB mysql
|
||||
if: ${{ matrix.db == 'mysqli' }}
|
||||
run: mysql --host 127.0.0.1 -utest -ptest -e 'CREATE DATABASE IF NOT EXISTS test COLLATE = utf8mb4_bin;';
|
||||
|
||||
- name: Setting up DB pgsql
|
||||
if: ${{ matrix.db == 'pgsql' }}
|
||||
run: |
|
||||
# TODO: Remove these conf. modifications when php74 or php80 are lowest.
|
||||
# Change to old md5 auth, because php73 does not support it.
|
||||
# #password_encryption = scram-sha-256
|
||||
(Get-Content "$env:PGDATA\postgresql.conf"). `
|
||||
replace('#password_encryption = scram-sha-256', 'password_encryption = md5') | `
|
||||
Set-Content "$env:PGDATA\postgresql.conf"
|
||||
(Get-Content "$env:PGDATA\pg_hba.conf"). `
|
||||
replace('scram-sha-256', 'md5') | `
|
||||
Set-Content "$env:PGDATA\pg_hba.conf"
|
||||
$pgService = Get-Service -Name postgresql*
|
||||
Set-Service -InputObject $pgService -Status running -StartupType automatic
|
||||
Start-Process -FilePath "$env:PGBIN\pg_isready" -Wait -PassThru
|
||||
& $env:PGBIN\psql --command="CREATE USER test PASSWORD 'test'" --command="\du"
|
||||
|
||||
- name: Creating DB pgsql
|
||||
if: ${{ matrix.db == 'pgsql' }}
|
||||
run: |
|
||||
& $env:PGBIN\createdb --owner=test test
|
||||
$env:PGPASSWORD = 'test'
|
||||
& $env:PGBIN\psql --username=test --host=localhost --list test
|
||||
|
||||
- name: Configuring git vars
|
||||
uses: rlespinasse/github-slug-action@v4
|
||||
|
||||
- name: Setting up PHP ${{ matrix.php }}
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: ${{ matrix.extensions }}
|
||||
ini-values: max_input_vars=5000
|
||||
coverage: none
|
||||
|
||||
- name: Set git to use LF
|
||||
run: |
|
||||
git config --global core.autocrlf false
|
||||
git config --global core.eol lf
|
||||
|
||||
- name: Checking out code from ${{ env.GITHUB_REF_SLUG }}
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Needs to be done after php is available, git configured and Moodle checkout has happened.
|
||||
- name: Setting up moodle-exttests service
|
||||
run: |
|
||||
git clone https://github.com/moodlehq/moodle-exttests.git
|
||||
nssm install php-built-in C:\tools\php\php.exe -S localhost:8080 -t D:\a\moodle\moodle\moodle-exttests
|
||||
nssm start php-built-in
|
||||
|
||||
- name: Setting up redis service
|
||||
run: |
|
||||
choco install redis-64 --version 3.0.503 --no-progress
|
||||
nssm install redis redis-server
|
||||
nssm start redis
|
||||
|
||||
- name: Setting up PHPUnit
|
||||
env:
|
||||
dbtype: ${{ matrix.db }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "pathtophp=$(which php)" >> $GITHUB_ENV # Inject installed pathtophp to env. The template config needs it.
|
||||
cp .github/workflows/config-template.php config.php
|
||||
mkdir ../moodledata
|
||||
php admin/tool/phpunit/cli/init.php --no-composer-self-update
|
||||
|
||||
- name: Running PHPUnit tests
|
||||
env:
|
||||
dbtype: ${{ matrix.db }}
|
||||
phpunit_options: ${{ secrets.phpunit_options }}
|
||||
run: vendor/bin/phpunit $phpunit_options
|
||||
@@ -30,10 +30,9 @@
|
||||
* @param {String} srcPath the matched src path
|
||||
* @return {String} The rewritten destination path.
|
||||
*/
|
||||
|
||||
const babelRename = function(destPath, srcPath) {
|
||||
destPath = srcPath.replace('src', 'build');
|
||||
destPath = destPath.replace('.js', '.min.js');
|
||||
destPath = srcPath.replace(`amd/src`, `amd/build`);
|
||||
destPath = destPath.replace(/\.js$/, '.min.js');
|
||||
return destPath;
|
||||
};
|
||||
|
||||
|
||||
@@ -3751,6 +3751,7 @@
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="7.4.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_82" message="unsupportedphpversion82" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
|
||||
@@ -6,7 +6,7 @@ require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->libdir.'/adminlib.php');
|
||||
include_once($CFG->dirroot.'/mnet/lib.php');
|
||||
|
||||
$sort = optional_param('sort', 'username', PARAM_ALPHA);
|
||||
$sort = optional_param('sort', 'username', PARAM_ALPHAEXT);
|
||||
$dir = optional_param('dir', 'ASC', PARAM_ALPHA);
|
||||
$page = optional_param('page', 0, PARAM_INT);
|
||||
$perpage = optional_param('perpage', 30, PARAM_INT);
|
||||
@@ -153,7 +153,15 @@ foreach ($columns as $column) {
|
||||
$headings[$column] = "<a href=\"?sort=$column&dir=$columndir&\">".$string[$column]."</a>$columnicon";
|
||||
}
|
||||
$headings['delete'] = '';
|
||||
$acl = $DB->get_records('mnet_sso_access_control', null, "$sort $dir", '*'); //, $page * $perpage, $perpage);
|
||||
|
||||
$sortorder = get_safe_orderby([
|
||||
'username' => 'username',
|
||||
'mnet_host_id' => 'mnet_host_id',
|
||||
'access' => 'accessctrl',
|
||||
'default' => 'username',
|
||||
], $sort, $dir, false);
|
||||
|
||||
$acl = $DB->get_records('mnet_sso_access_control', null, $sortorder);
|
||||
$aclcount = $DB->count_records('mnet_sso_access_control');
|
||||
|
||||
if (!$acl) {
|
||||
|
||||
@@ -49,7 +49,7 @@ list($options, $unrecognized) = cli_get_params(
|
||||
'torun' => 0,
|
||||
'optimize-runs' => '',
|
||||
'add-core-features-to-theme' => false,
|
||||
'axe' => false,
|
||||
'axe' => null,
|
||||
'disable-composer' => false,
|
||||
'composer-upgrade' => true,
|
||||
'composer-self-update' => true,
|
||||
@@ -69,7 +69,7 @@ Behat utilities to initialise behat tests
|
||||
|
||||
Usage:
|
||||
php init.php [--parallel=value [--maxruns=value] [--fromrun=value --torun=value]]
|
||||
[--axe] [-o | --optimize-runs] [-a | --add-core-features-to-theme]
|
||||
[--no-axe] [-o | --optimize-runs] [-a | --add-core-features-to-theme]
|
||||
[--no-composer-self-update] [--no-composer-upgrade]
|
||||
[--disable-composer]
|
||||
[--help]
|
||||
@@ -79,7 +79,7 @@ Options:
|
||||
-m, --maxruns Max parallel processes to be executed at one time
|
||||
--fromrun Execute run starting from (Used for parallel runs on different vms)
|
||||
--torun Execute run till (Used for parallel runs on different vms)
|
||||
--axe Include axe accessibility tests
|
||||
--no-axe Disable axe accessibility tests.
|
||||
|
||||
-o, --optimize-runs
|
||||
Split features with specified tags in all parallel runs.
|
||||
@@ -102,7 +102,7 @@ Options:
|
||||
Example from Moodle root directory:
|
||||
\$ php admin/tool/behat/cli/init.php --parallel=2
|
||||
|
||||
More info in http://docs.moodle.org/dev/Acceptance_testing#Running_tests
|
||||
More info in https://moodledev.io/general/development/tools/behat/running
|
||||
";
|
||||
|
||||
if (!empty($options['help'])) {
|
||||
@@ -110,6 +110,12 @@ if (!empty($options['help'])) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if ($options['axe']) {
|
||||
echo "Axe accessibility tests are enabled by default, to disable them, use the --no-axe option.\n";
|
||||
} else if ($options['axe'] === false) {
|
||||
echo "Axe accessibility tests have been disabled.\n";
|
||||
}
|
||||
|
||||
// Check which util file to call.
|
||||
$utilfile = 'util_single_run.php';
|
||||
$commandoptions = "";
|
||||
@@ -118,9 +124,7 @@ if ($options['parallel'] && $options['parallel'] > 1) {
|
||||
$utilfile = 'util.php';
|
||||
// Sanitize all input options, so they can be passed to util.
|
||||
foreach ($options as $option => $value) {
|
||||
if ($value) {
|
||||
$commandoptions .= " --$option=\"$value\"";
|
||||
}
|
||||
$commandoptions .= behat_get_command_flags($option, $value);
|
||||
}
|
||||
} else {
|
||||
// Only sanitize options for single run.
|
||||
@@ -130,9 +134,7 @@ if ($options['parallel'] && $options['parallel'] > 1) {
|
||||
];
|
||||
|
||||
foreach ($cmdoptionsforsinglerun as $option) {
|
||||
if (!empty($options[$option])) {
|
||||
$commandoptions .= " --$option='$options[$option]'";
|
||||
}
|
||||
$commandoptions .= behat_get_command_flags($option, $options[$option]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ Options:
|
||||
Example from Moodle root directory:
|
||||
\$ php admin/tool/behat/cli/run.php --tags=\"@javascript\"
|
||||
|
||||
More info in http://docs.moodle.org/dev/Acceptance_testing#Running_tests
|
||||
More info in https://moodledev.io/general/development/tools/behat/running
|
||||
";
|
||||
|
||||
if (!empty($options['help'])) {
|
||||
|
||||
@@ -57,7 +57,7 @@ list($options, $unrecognized) = cli_get_params(
|
||||
'torun' => 0,
|
||||
'optimize-runs' => '',
|
||||
'add-core-features-to-theme' => false,
|
||||
'axe' => false,
|
||||
'axe' => true,
|
||||
),
|
||||
array(
|
||||
'h' => 'help',
|
||||
@@ -73,7 +73,7 @@ $help = "
|
||||
Behat utilities to manage the test environment
|
||||
|
||||
Usage:
|
||||
php util.php [--install|--drop|--enable|--disable|--diag|--updatesteps|--axe|--help] [--parallel=value [--maxruns=value]]
|
||||
php util.php [--install|--drop|--enable|--disable|--diag|--updatesteps|--no-axe|--help] [--parallel=value [--maxruns=value]]
|
||||
|
||||
Options:
|
||||
--install Installs the test environment for acceptance tests
|
||||
@@ -82,7 +82,7 @@ Options:
|
||||
--disable Disables test environment
|
||||
--diag Get behat test environment status code
|
||||
--updatesteps Update feature step file.
|
||||
--axe Include axe accessibility tests
|
||||
--no-axe Disable axe accessibility tests.
|
||||
|
||||
-j, --parallel Number of parallel behat run operation
|
||||
-m, --maxruns Max parallel processes to be executed at one time.
|
||||
@@ -94,7 +94,7 @@ Options:
|
||||
Example from Moodle root directory:
|
||||
\$ php admin/tool/behat/cli/util.php --enable --parallel=4
|
||||
|
||||
More info in http://docs.moodle.org/dev/Acceptance_testing#Running_tests
|
||||
More info in https://moodledev.io/general/development/tools/behat/running
|
||||
";
|
||||
|
||||
if (!empty($options['help'])) {
|
||||
@@ -320,12 +320,7 @@ function commands_to_execute($options) {
|
||||
}
|
||||
|
||||
foreach ($extraoptions as $option => $value) {
|
||||
if ($options[$option]) {
|
||||
$extra .= " --$option";
|
||||
if ($value) {
|
||||
$extra .= "=\"$value\"";
|
||||
}
|
||||
}
|
||||
$extra .= behat_get_command_flags($option, $value);
|
||||
}
|
||||
|
||||
if (empty($options['parallel'])) {
|
||||
|
||||
@@ -52,7 +52,7 @@ list($options, $unrecognized) = cli_get_params(
|
||||
'updatesteps' => false,
|
||||
'optimize-runs' => '',
|
||||
'add-core-features-to-theme' => false,
|
||||
'axe' => false,
|
||||
'axe' => true,
|
||||
),
|
||||
array(
|
||||
'h' => 'help',
|
||||
@@ -79,7 +79,7 @@ Options:
|
||||
--disable Disables test environment
|
||||
--diag Get behat test environment status code
|
||||
--updatesteps Update feature step file.
|
||||
--axe Include axe accessibility tests
|
||||
--no-axe Disable axe accessibility tests.
|
||||
|
||||
-o, --optimize-runs Split features with specified tags in all parallel runs.
|
||||
-a, --add-core-features-to-theme Add all core features to specified theme's
|
||||
@@ -89,7 +89,7 @@ Options:
|
||||
Example from Moodle root directory:
|
||||
\$ php admin/tool/behat/cli/util_single_run.php --enable
|
||||
|
||||
More info in http://docs.moodle.org/dev/Acceptance_testing#Running_tests
|
||||
More info in https://moodledev.io/general/development/tools/behat/running
|
||||
";
|
||||
|
||||
if (!empty($options['help'])) {
|
||||
@@ -186,7 +186,7 @@ if ($options['install']) {
|
||||
behat_config_manager::set_behat_run_config_value('behatsiteenabled', 1);
|
||||
}
|
||||
|
||||
// Define whether to run Behat with axe tests.
|
||||
// Configure axe according to option.
|
||||
behat_config_manager::set_behat_run_config_value('axe', $options['axe']);
|
||||
|
||||
// Enable test mode.
|
||||
|
||||
@@ -30,7 +30,7 @@ $string['errordataroot'] = '$CFG->behat_dataroot is not set or is invalid.';
|
||||
$string['errorsetconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot need to be set in config.php.';
|
||||
$string['erroruniqueconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot values need to be different than $CFG->dataroot, $CFG->prefix, $CFG->wwwroot, $CFG->phpunit_dataroot and $CFG->phpunit_prefix values.<br/>Or, if $CFG->behat_prefix is the same, $CFG->behat_dbname or $CFG->behat_dbhost need to be different from $CFG->phpunit_dbname and $CFG->phpunit_dbhost and from $CFG->dbname and $CFG->dbhost.';
|
||||
$string['fieldvalueargument'] = 'Field value arguments';
|
||||
$string['fieldvalueargument_help'] = 'This argument should be completed by a field value. There are many field types, including simple ones like checkboxes, selects or textareas, or complex ones like date selectors. See the dev documentation <a href="https://docs.moodle.org/dev/Acceptance_testing" target="_blank">Acceptance_testing</a> for details of expected field values.';
|
||||
$string['fieldvalueargument_help'] = 'This argument should be completed by a field value. There are many field types, including simple ones like checkboxes, selects or textareas, or complex ones like date selectors. See the dev documentation <a href="https://moodledev.io/general/development/tools/behat" target="_blank">Acceptance_testing</a> for details of expected field values.';
|
||||
$string['giveninfo'] = 'Given. Processes to set up the environment';
|
||||
$string['infoheading'] = 'Info';
|
||||
$string['installinfo'] = 'Read {$a} for installation and tests execution info';
|
||||
|
||||
@@ -184,9 +184,10 @@ class tool_behat_renderer extends plugin_renderer_base {
|
||||
// Info.
|
||||
$installurl = behat_command::DOCS_URL;
|
||||
$installlink = html_writer::tag('a', $installurl, array('href' => $installurl, 'target' => '_blank'));
|
||||
$writetestsurl = 'https://docs.moodle.org/dev/Writing acceptance tests';
|
||||
$writetestsurl = 'https://moodledev.io/general/development/tools/behat/writing';
|
||||
$writetestslink = html_writer::tag('a', $writetestsurl, array('href' => $writetestsurl, 'target' => '_blank'));
|
||||
$writestepsurl = 'https://docs.moodle.org/dev/Writing_new_acceptance_test_step_definitions';
|
||||
$writestepsurl = 'https://moodledev.io/general/development/tools/behat/writing#' .
|
||||
'writing-new-acceptance-test-step-definitions';
|
||||
$writestepslink = html_writer::tag('a', $writestepsurl, array('href' => $writestepsurl, 'target' => '_blank'));
|
||||
$infos = array(
|
||||
get_string('installinfo', 'tool_behat', $installlink),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
This files describes API changes in the tool_behat code.
|
||||
|
||||
=== 4.1.4 ===
|
||||
* Behat is initialised with Axe accessibility tests enabled by default, if you want to disable them please use the --no-axe option.
|
||||
=== 3.7 ===
|
||||
* Behat will now look for behat step definitions in the current
|
||||
theme and any parents the theme may have.
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2022112801; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2022112802; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2022111800; // Requires this Moodle version.
|
||||
$plugin->component = 'tool_behat'; // Full name of the plugin (used for diagnostics)
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
namespace tool_brickfield\local\tool;
|
||||
|
||||
use tool_brickfield\manager;
|
||||
|
||||
/**
|
||||
* Class advanced.
|
||||
*
|
||||
@@ -51,12 +53,36 @@ class advanced extends tool {
|
||||
return 'advanced';
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds context data used to render a single grid item on the advanced page.
|
||||
* @param string $icon
|
||||
* @param string $heading
|
||||
* @param string $content
|
||||
* @return array
|
||||
*/
|
||||
protected function get_grid_item_context(string $icon, string $heading, string $content): array {
|
||||
return [
|
||||
"icon" => "pix/i/$icon.png",
|
||||
"iconalt" => get_string("icon:$icon", manager::PLUGINNAME),
|
||||
"heading" => get_string($heading, manager::PLUGINNAME),
|
||||
"content" => get_string($content, manager::PLUGINNAME)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the data for renderer / template display.
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function fetch_data(): \stdClass {
|
||||
$data = (object)[
|
||||
'griditems' => [
|
||||
$this->get_grid_item_context("analytics-custom", "headingone", "contentone"),
|
||||
$this->get_grid_item_context("tools-custom", "headingtwo", "contenttwo"),
|
||||
$this->get_grid_item_context("file-edit-custom", "headingthree", "contentthree"),
|
||||
$this->get_grid_item_context("search-plus-custom", "headingfour", "contentfour"),
|
||||
$this->get_grid_item_context("wand-magic-custom", "headingfive", "contentfive"),
|
||||
$this->get_grid_item_context("hands-helping-custom", "headingsix", "contentsix"),
|
||||
],
|
||||
'valid' => true,
|
||||
'error' => '',
|
||||
];
|
||||
|
||||
@@ -270,3 +270,9 @@ $string['headingthree'] = 'Accessible file formats';
|
||||
$string['headingfour'] = 'Focus effort';
|
||||
$string['headingfive'] = 'HTML fixes';
|
||||
$string['headingsix'] = 'Performance support';
|
||||
$string['icon:analytics-custom'] = 'Analytics icon';
|
||||
$string['icon:tools-custom'] = 'Tools icon';
|
||||
$string['icon:file-edit-custom'] = 'Edit file icon';
|
||||
$string['icon:search-plus-custom'] = 'Search icon';
|
||||
$string['icon:wand-magic-custom'] = 'Magic wand icon';
|
||||
$string['icon:hands-helping-custom'] = 'Helping hands icon';
|
||||
|
||||
@@ -10,49 +10,22 @@
|
||||
|
||||
/* Advanced Page CSS */
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
.tool_brickfield_grid_item {
|
||||
width: 33.333333333333336%;
|
||||
}
|
||||
#tool_brickifeld_advanced_page {
|
||||
margin: 20px 20px 20px 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
.tool_brickfield_grid_item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.tool_brickfield_grid {
|
||||
max-width: 1140px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.tool_brickfield_grid_inner {
|
||||
width: 100%;
|
||||
.tool_brickfield_banner_heading {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tool_brickfield_grid_item_shape {
|
||||
height: 96%;
|
||||
background-color: #fff;
|
||||
padding: 10% 10% 10% 10%;
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.05);
|
||||
margin: 1% 1% 1% 1%;
|
||||
border-radius: 15px 15px 15px 70px;
|
||||
}
|
||||
|
||||
.tool_brickfield_grid_display {
|
||||
display: grid;
|
||||
grid-template-columns: 0.6fr 1.4fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
|
||||
.tool_brickfield_grid_spacing {
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
margin-left: auto;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.tool_brickfield_banner_heading > img {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.tool_brickfield_banner > p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tool_brickfield_grid_heading {
|
||||
@@ -60,59 +33,63 @@
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
color: #040d29;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.tool_brickfield_grid_content {
|
||||
color: #040d29;
|
||||
font-size: 16px;
|
||||
line-height: 1.4em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tool_brickfield_logo_height {
|
||||
height: 3em;
|
||||
}
|
||||
|
||||
.tool_brickfield_logo_position {
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
.tool_brickfield_icon_position {
|
||||
line-height: 1;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
.tool_brickfield_icon_height {
|
||||
.tool_brickfield_grid_heading > img {
|
||||
height: 30px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.tool_brickfield_center_button {
|
||||
text-align: center;
|
||||
.tool_brickfield_grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin: 20px 20px 30px;
|
||||
}
|
||||
|
||||
.tool_brickfield_advanced_button {
|
||||
.tool_brickfield_grid > div {
|
||||
margin: 10px;
|
||||
padding: 20px;
|
||||
flex-basis: 30%;
|
||||
border: 1px solid grey;
|
||||
max-width: 500px;
|
||||
min-width: 250px;
|
||||
flex-grow: 4;
|
||||
border-radius: 15px 15px 15px 50px;
|
||||
}
|
||||
|
||||
.tool_brickfield_grid_heading {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tool_brickfield_footer > a {
|
||||
font-size: 18px;
|
||||
letter-spacing: 1px;
|
||||
color: #fff;
|
||||
background-color: #4054b2;
|
||||
border-style: solid;
|
||||
border-color: #fff;
|
||||
border-radius: 5px 5px 5px 5px;
|
||||
padding: 15px 50px 15px 50px;
|
||||
border-radius: 5px;
|
||||
padding: 15px 50px;
|
||||
transition: 0.3s;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tool_brickfield_advanced_button:hover {
|
||||
.tool_brickfield_footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tool_brickfield_footer > a:hover {
|
||||
color: #fff;
|
||||
background-color: #253687;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tool_brickfield_banner_heading {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.tool_brickfield_banner {
|
||||
height: 10em;
|
||||
padding: 2.5em;
|
||||
margin-bottom: 2em;
|
||||
.tool_brickfield_footer > p {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
@@ -21,184 +21,42 @@
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"pagetitle": "Advanced"
|
||||
"griditems": [
|
||||
{
|
||||
"icon": "some-icon-name",
|
||||
"iconalt": "some-icon-alt",
|
||||
"heading": "some-heading",
|
||||
"content": "some-content"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
|
||||
<h3>{{{pagetitle}}}</h3>
|
||||
|
||||
<section class="tool_brickfield_banner">
|
||||
<div class="tool_brickfield_banner_heading">
|
||||
<img src="pix/i/Brickfield-logo-black.png" alt="Brickfield Logo" class="pull-right tool_brickfield_logo_height">
|
||||
<h2>{{# str }}bannerheadingone, tool_brickfield{{/ str }}</h2>
|
||||
<div id="tool_brickifeld_advanced_page">
|
||||
<div class="tool_brickfield_banner">
|
||||
<div class="tool_brickfield_banner_heading">
|
||||
<h2>{{#str}} bannerheadingone, tool_brickfield{{/str}}</h2>
|
||||
<img src="pix/i/Brickfield-logo-black.png" alt="Brickfield Logo">
|
||||
</div>
|
||||
<p>{{#str}} bannercontentone, tool_brickfield, https://www.brickfield.ie/moodle_talk_to_sales/{{/str}}</p>
|
||||
</div>
|
||||
<p>
|
||||
{{# str }}bannercontentone, tool_brickfield, https://www.brickfield.ie/moodle_talk_to_sales/{{/ str }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="tool_brickfield_grid">
|
||||
<div class="tool_brickfield_grid_inner">
|
||||
|
||||
<div class="tool_brickfield_grid">
|
||||
{{#griditems}}
|
||||
<div class="tool_brickfield_grid_item">
|
||||
<div class="tool_brickfield_grid_item_shape">
|
||||
<div class="tool_brickfield_grid_display" style="grid-template-areas: 'al ar ar' 'al ar ar'; grid-area: a;">
|
||||
<div style="grid-area: al;">
|
||||
<div class="tool_brickfield_grid_spacing">
|
||||
<span class="tool_brickfield_icon_position">
|
||||
<img src="pix/i/analytics-custom.png" alt="Analytics icon" class="tool_brickfield_icon_height">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="grid-area: ar;">
|
||||
<h3>
|
||||
<span class="tool_brickfield_grid_heading">
|
||||
{{# str }}headingone, tool_brickfield{{/ str }}
|
||||
</span>
|
||||
</h3>
|
||||
<p class="tool_brickfield_grid_content">
|
||||
{{# str }}contentone, tool_brickfield{{/ str }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool_brickfield_grid_heading">
|
||||
<img src="{{icon}}" alt="{{iconalt}}">
|
||||
<h3>{{heading}}</h3>
|
||||
</div>
|
||||
<div class="tool_brickfield_grid_content">
|
||||
<p>{{content}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool_brickfield_grid_item">
|
||||
<div class="tool_brickfield_grid_item_shape">
|
||||
<div class="tool_brickfield_grid_display" style="grid-template-areas: 'bl br br' 'bl br br'; grid-area: b;">
|
||||
<div style="grid-area: bl;">
|
||||
<div class="tool_brickfield_grid_spacing">
|
||||
<span class="tool_brickfield_icon_position">
|
||||
<img src="pix/i/tools-custom.png" alt="Tools icon" class="tool_brickfield_icon_height">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="grid-area: br;">
|
||||
<h3>
|
||||
<span class="tool_brickfield_grid_heading">
|
||||
{{# str }}headingtwo, tool_brickfield{{/ str }}
|
||||
</span>
|
||||
</h3>
|
||||
<p class="tool_brickfield_grid_content">
|
||||
{{# str }}contenttwo, tool_brickfield{{/ str }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool_brickfield_grid_item">
|
||||
<div class="tool_brickfield_grid_item_shape">
|
||||
<div class="tool_brickfield_grid_display" style="grid-template-areas: 'cl cr cr' 'cl cr cr'; grid-area: c;">
|
||||
<div style="grid-area: cl;">
|
||||
<div class="tool_brickfield_grid_spacing">
|
||||
<span class="tool_brickfield_icon_position">
|
||||
<img src="pix/i/file-edit-custom.png" alt="Edit file icon" class="tool_brickfield_icon_height">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="grid-area: cr;">
|
||||
<h3>
|
||||
<span class="tool_brickfield_grid_heading">
|
||||
{{# str }}headingthree, tool_brickfield{{/ str }}
|
||||
</span>
|
||||
</h3>
|
||||
<p class="tool_brickfield_grid_content">
|
||||
{{# str }}contentthree, tool_brickfield{{/ str }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool_brickfield_grid_item">
|
||||
<div class="tool_brickfield_grid_item_shape">
|
||||
<div class="tool_brickfield_grid_display" style="grid-template-areas: 'dl dr dr' 'dl dr dr'; grid-area: d;">
|
||||
<div style="grid-area: dl;">
|
||||
<div class="tool_brickfield_grid_spacing">
|
||||
<span class="tool_brickfield_icon_position">
|
||||
<img src="pix/i/search-plus-custom.png" alt="Search icon" class="tool_brickfield_icon_height">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="grid-area: dr;">
|
||||
<h3>
|
||||
<span class="tool_brickfield_grid_heading">
|
||||
{{# str }}headingfour, tool_brickfield{{/ str }}
|
||||
</span>
|
||||
</h3>
|
||||
<p class="tool_brickfield_grid_content">
|
||||
{{# str }}contentfour, tool_brickfield{{/ str }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool_brickfield_grid_item">
|
||||
<div class="tool_brickfield_grid_item_shape">
|
||||
<div class="tool_brickfield_grid_display" style="grid-template-areas: 'el er er' 'el er er'; grid-area: e;">
|
||||
<div style="grid-area: el;">
|
||||
<div class="tool_brickfield_grid_spacing">
|
||||
<span class="tool_brickfield_icon_position">
|
||||
<img src="pix/i/wand-magic-custom.png" alt="Magic wand icon" class="tool_brickfield_icon_height">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="grid-area: er;">
|
||||
<h3>
|
||||
<span class="tool_brickfield_grid_heading">
|
||||
{{# str }}headingfive, tool_brickfield{{/ str }}
|
||||
</span>
|
||||
</h3>
|
||||
<p class="tool_brickfield_grid_content">
|
||||
{{# str }}contentfive, tool_brickfield{{/ str }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool_brickfield_grid_item">
|
||||
<div class="tool_brickfield_grid_item_shape">
|
||||
<div class="tool_brickfield_grid_display" style="grid-template-areas: 'fl fr fr' 'fl fr fr'; grid-area: f;">
|
||||
<div style="grid-area: fl;">
|
||||
<div class="tool_brickfield_grid_spacing">
|
||||
<span class="tool_brickfield_icon_position">
|
||||
<img src="pix/i/hands-helping-custom.png" alt="Helping hands icon" class="tool_brickfield_icon_height">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="grid-area: fr;">
|
||||
<h3>
|
||||
<span class="tool_brickfield_grid_heading">
|
||||
{{# str }}headingsix, tool_brickfield{{/ str }}
|
||||
</span>
|
||||
</h3>
|
||||
<p class="tool_brickfield_grid_content">
|
||||
{{# str }}contentsix, tool_brickfield{{/ str }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/griditems}}
|
||||
</div>
|
||||
<div class="tool_brickfield_footer">
|
||||
<h2 class="accesshide">{{#str}} footerheading, tool_brickfield {{/str}}</h2>
|
||||
<p>{{#str}}bannercontenttwo, tool_brickfield{{/str}}</p>
|
||||
<a href="https://www.brickfield.ie/moodle_free_demo/">
|
||||
{{#str}}buttonone, tool_brickfield{{/str}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="tool_brickfield_banner">
|
||||
<h2 class="accesshide">{{# str }}footerheading, tool_brickfield{{/ str }}</h2>
|
||||
<p>
|
||||
{{# str }}bannercontenttwo, tool_brickfield{{/ str }}
|
||||
</p>
|
||||
<div class="tool_brickfield_center_button">
|
||||
<form action="https://www.brickfield.ie/moodle_free_demo/">
|
||||
<button class="tool_brickfield_advanced_button">
|
||||
{{# str }}buttonone, tool_brickfield{{/ str }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
@@ -179,6 +179,7 @@ class api {
|
||||
AND ra.roleid = :roleid
|
||||
AND ra.userid = :userid
|
||||
WHERE cm.cohortid ' . $cohortsql . '
|
||||
AND u.deleted = 0
|
||||
AND ra.id IS NULL';
|
||||
|
||||
$toadd = $DB->get_records_sql($sql, $params);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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/>.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tool_cohortroles;
|
||||
|
||||
use core\event\user_deleted;
|
||||
|
||||
/**
|
||||
* Plugin event observer callbacks
|
||||
*
|
||||
* @package tool_cohortroles
|
||||
* @copyright 2023 Paul Holden <paulh@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class observers {
|
||||
|
||||
/**
|
||||
* User deleted event, remove cohort role assignments specific to them
|
||||
*
|
||||
* @param user_deleted $event
|
||||
*/
|
||||
public static function user_deleted(user_deleted $event): void {
|
||||
$cohortroleassignments = cohort_role_assignment::get_records(['userid' => $event->objectid]);
|
||||
foreach ($cohortroleassignments as $cohortroleassignment) {
|
||||
$cohortroleassignment->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Plugin event observers
|
||||
*
|
||||
* @package tool_cohortroles
|
||||
* @copyright 2023 Paul Holden <paulh@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$observers = [
|
||||
[
|
||||
'eventname' => '\core\event\user_deleted',
|
||||
'callback' => '\tool_cohortroles\observers::user_deleted',
|
||||
],
|
||||
];
|
||||
@@ -50,5 +50,13 @@ function xmldb_tool_cohortroles_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v4.1.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2022112801) {
|
||||
// Delete any tool_cohortroles mappings for users who no longer exist.
|
||||
$DB->delete_records_select('tool_cohortroles', 'userid NOT IN (SELECT id FROM {user} WHERE deleted = 0)');
|
||||
|
||||
// Cohortroles savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2022112801, 'tool', 'cohortroles');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
$plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2022112801; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2022111800; // Requires this Moodle version.
|
||||
$plugin->component = 'tool_cohortroles'; // Full name of the plugin (used for diagnostics).
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
namespace tool_langimport;
|
||||
|
||||
use moodle_url;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
require_once($CFG->libdir.'/filelib.php');
|
||||
require_once($CFG->libdir.'/componentlib.class.php');
|
||||
@@ -59,6 +61,25 @@ class controller {
|
||||
$this->availablelangs = $this->installer->get_remote_list_of_languages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the specified url, outputting any required messages.
|
||||
*
|
||||
* @param moodle_url $url
|
||||
*/
|
||||
public function redirect(moodle_url $url): void {
|
||||
if ($this->info) {
|
||||
$info = implode('<br />', $this->info);
|
||||
\core\notification::success($info);
|
||||
}
|
||||
|
||||
if ($this->errors) {
|
||||
$info = implode('<br />', $this->errors);
|
||||
\core\notification::error($info);
|
||||
}
|
||||
|
||||
redirect($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install language packs provided
|
||||
*
|
||||
@@ -112,6 +133,18 @@ class controller {
|
||||
public function uninstall_language($lang) {
|
||||
global $CFG;
|
||||
|
||||
$lang = clean_param($lang, PARAM_LANG);
|
||||
if ($lang === '') {
|
||||
// Do not allow uninstallation of invalid languages.
|
||||
// Note: PARAM_LANG returns an empty string for invalid validation.
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($lang === 'en') {
|
||||
// Never allow removal of the default langauge.
|
||||
return false;
|
||||
}
|
||||
|
||||
$dest1 = $CFG->dataroot.'/lang/'.$lang;
|
||||
$dest2 = $CFG->dirroot.'/lang/'.$lang;
|
||||
$rm1 = false;
|
||||
|
||||
@@ -43,6 +43,13 @@ $uninstalllang = optional_param_array('uninstalllang', array(), PARAM_LANG)
|
||||
$confirmtounistall = optional_param('confirmtouninstall', '', PARAM_SAFEPATH); // uninstallation confirmation
|
||||
$purgecaches = optional_param('purgecaches', false, PARAM_BOOL); // explicit caches reset
|
||||
|
||||
// Filter the uninstall language list.
|
||||
// If the list contains a language which is not installed, it is replaced with an empty string.
|
||||
// When we try to uninstall an empty string, we uninstall every language.
|
||||
$uninstalllang = array_filter($uninstalllang, function($lang) {
|
||||
return !empty($lang);
|
||||
});
|
||||
|
||||
if ($purgecaches) {
|
||||
require_sesskey();
|
||||
get_string_manager()->reset_caches();
|
||||
@@ -69,11 +76,12 @@ if (($mode == INSTALLATION_OF_SELECTED_LANG) and confirm_sesskey() and !empty($p
|
||||
if (is_array($pack) && count($pack) > 1) {
|
||||
// Installing multiple languages can take a while - perform it asynchronously in the background.
|
||||
$controller->schedule_languagepacks_installation($pack);
|
||||
|
||||
$controller->redirect($PAGE->url);
|
||||
} else {
|
||||
// Single language pack to be installed synchronously. It should be reasonably quick and can be used for debugging, too.
|
||||
core_php_time_limit::raise();
|
||||
$controller->install_languagepacks($pack);
|
||||
$controller->redirect($PAGE->url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +110,7 @@ if ($mode == DELETION_OF_SELECTED_LANG and (!empty($uninstalllang) or !empty($co
|
||||
foreach ($uninstalllang as $ulang) {
|
||||
$controller->uninstall_language($ulang);
|
||||
}
|
||||
|
||||
$controller->redirect($PAGE->url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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/>.
|
||||
|
||||
namespace tool_langimport;
|
||||
|
||||
/**
|
||||
* Tests for \tool_langimport\locale class.
|
||||
*
|
||||
* @package tool_langimport
|
||||
* @category test
|
||||
* @coversDefaultClass \tool_langimport\controller
|
||||
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class controller_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Test uninstall of language with invalid values.
|
||||
*
|
||||
* @covers ::uninstall_lang
|
||||
* @dataProvider uninstall_lang_invalid_provider
|
||||
* @params string $lang
|
||||
*/
|
||||
public function test_uninstall_lang_invalid(string $lang): void {
|
||||
global $CFG;
|
||||
|
||||
$controller = new controller();
|
||||
$this->assertFalse($controller->uninstall_language($lang));
|
||||
$this->assertFileExists("{$CFG->dataroot}/lang");
|
||||
$this->assertFileExists("{$CFG->dirroot}/lang/en");
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for uninstall_lang tests with invalid values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function uninstall_lang_invalid_provider(): array {
|
||||
return [
|
||||
'Empty string' => [''],
|
||||
'Meaningless empty string' => [' '],
|
||||
'Default language' => ['en'],
|
||||
'Invalid language string' => ['swedish'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -14,19 +14,11 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* License manager helper class.
|
||||
*
|
||||
* @package tool_licensemanager
|
||||
* @copyright 2019 Tom Dickman <tomdickman@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_licensemanager;
|
||||
|
||||
use moodle_url;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* License manager helper class.
|
||||
@@ -48,12 +40,7 @@ class helper {
|
||||
* @return \moodle_url
|
||||
*/
|
||||
public static function get_licensemanager_url() : moodle_url {
|
||||
global $CFG;
|
||||
|
||||
$url = new moodle_url($CFG->wwwroot . self::MANAGER_PATH,
|
||||
['sesskey' => sesskey()]);
|
||||
|
||||
return $url;
|
||||
return new moodle_url(self::MANAGER_PATH);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,9 +77,8 @@ class helper {
|
||||
* @return \moodle_url
|
||||
*/
|
||||
public static function get_create_license_url() : moodle_url {
|
||||
$url = new moodle_url(self::MANAGER_PATH,
|
||||
['action' => manager::ACTION_CREATE, 'sesskey' => sesskey()]);
|
||||
|
||||
$url = self::get_licensemanager_url();
|
||||
$url->params(['action' => manager::ACTION_CREATE]);
|
||||
return $url;
|
||||
}
|
||||
|
||||
@@ -104,9 +90,8 @@ class helper {
|
||||
* @return \moodle_url
|
||||
*/
|
||||
public static function get_update_license_url(string $licenseshortname) : moodle_url {
|
||||
$url = new moodle_url(self::MANAGER_PATH,
|
||||
['action' => manager::ACTION_UPDATE, 'license' => $licenseshortname, 'sesskey' => sesskey()]);
|
||||
|
||||
$url = self::get_licensemanager_url();
|
||||
$url->params(['action' => manager::ACTION_UPDATE, 'license' => $licenseshortname]);
|
||||
return $url;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,22 +14,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* License manager.
|
||||
*
|
||||
* @package tool_licensemanager
|
||||
* @copyright 2019 Tom Dickman <tomdickman@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_licensemanager;
|
||||
|
||||
use tool_licensemanager\form\edit_license;
|
||||
use license_manager;
|
||||
use stdClass;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* License manager, main controller for tool_licensemanager.
|
||||
*
|
||||
@@ -95,18 +85,25 @@ class manager {
|
||||
}
|
||||
|
||||
$viewmanager = true;
|
||||
$redirect = helper::get_licensemanager_url();
|
||||
|
||||
switch ($action) {
|
||||
case self::ACTION_DISABLE:
|
||||
require_sesskey();
|
||||
license_manager::disable($license);
|
||||
redirect($redirect);
|
||||
break;
|
||||
|
||||
case self::ACTION_ENABLE:
|
||||
require_sesskey();
|
||||
license_manager::enable($license);
|
||||
redirect($redirect);
|
||||
break;
|
||||
|
||||
case self::ACTION_DELETE:
|
||||
require_sesskey();
|
||||
license_manager::delete($license);
|
||||
redirect($redirect);
|
||||
break;
|
||||
|
||||
case self::ACTION_CREATE:
|
||||
@@ -116,7 +113,9 @@ class manager {
|
||||
|
||||
case self::ACTION_MOVE_UP:
|
||||
case self::ACTION_MOVE_DOWN:
|
||||
require_sesskey();
|
||||
$this->change_license_order($action, $license);
|
||||
redirect($redirect);
|
||||
break;
|
||||
|
||||
case self::ACTION_VIEW_LICENSE_MANAGER:
|
||||
|
||||
@@ -28,15 +28,9 @@ require_once($CFG->libdir . '/licenselib.php');
|
||||
|
||||
require_admin();
|
||||
|
||||
$returnurl = \tool_licensemanager\helper::get_licensemanager_url();
|
||||
|
||||
$action = optional_param('action', '', PARAM_ALPHANUMEXT);
|
||||
$license = optional_param('license', '', PARAM_SAFEDIR);
|
||||
|
||||
if (!confirm_sesskey()) {
|
||||
redirect($returnurl);
|
||||
}
|
||||
|
||||
// Route via the manager.
|
||||
$licensemanager = new \tool_licensemanager\manager();
|
||||
$PAGE->set_context(context_system::instance());
|
||||
|
||||
@@ -128,3 +128,18 @@ Feature: Manage competency frameworks
|
||||
Then I should see "Competency framework updated"
|
||||
And I should see "Science Year-6 Edited"
|
||||
And I should see "sc-y-6"
|
||||
|
||||
Scenario: Duplicate a competency framework
|
||||
Given the following lp "frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| CF1 | CF1 |
|
||||
And the following lp "competencies" exist:
|
||||
| shortname | framework |
|
||||
| C1 | CF1 |
|
||||
And I navigate to "Competencies > Competency frameworks" in site administration
|
||||
# Duplicate the selected competency framework
|
||||
When I click on "Duplicate" of edit menu in the "CF1" row
|
||||
# Confirm that the selected framework was duplicated by clicking on the link
|
||||
And I click on "CF1 (copy) (CF1_1)" "link"
|
||||
# Confirm that the corresponding competency also exists in the duplicated framework
|
||||
Then I should see "C1"
|
||||
|
||||
@@ -14,17 +14,11 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Event documentation
|
||||
*
|
||||
* @package tool_monitor
|
||||
* @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_monitor;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
use core_component;
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* Class for returning event information.
|
||||
@@ -35,126 +29,6 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class eventlist {
|
||||
/**
|
||||
* Return all of the core event files.
|
||||
*
|
||||
* @return array Core events.
|
||||
*/
|
||||
protected static function get_core_eventlist() {
|
||||
global $CFG;
|
||||
|
||||
// Disable developer debugging as deprecated events will fire warnings.
|
||||
// Setup backup variables to restore the following settings back to what they were when we are finished.
|
||||
$debuglevel = $CFG->debug;
|
||||
$debugdisplay = $CFG->debugdisplay;
|
||||
$debugdeveloper = $CFG->debugdeveloper;
|
||||
$CFG->debug = 0;
|
||||
$CFG->debugdisplay = false;
|
||||
$CFG->debugdeveloper = false;
|
||||
|
||||
$eventinformation = array();
|
||||
$directory = $CFG->libdir . '/classes/event';
|
||||
$files = self::get_file_list($directory);
|
||||
|
||||
// Remove exceptional events that will cause problems being displayed.
|
||||
if (isset($files['unknown_logged'])) {
|
||||
unset($files['unknown_logged']);
|
||||
}
|
||||
foreach ($files as $file => $location) {
|
||||
$classname = '\\core\\event\\' . $file;
|
||||
// Check to see if this is actually a valid event.
|
||||
if (method_exists($classname, 'get_static_info')) {
|
||||
$ref = new \ReflectionClass($classname);
|
||||
// Ignore abstracts.
|
||||
if (!$ref->isAbstract() && $file != 'manager') {
|
||||
$eventinformation[$classname] = $classname::get_name_with_info();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Now enable developer debugging as event information has been retrieved.
|
||||
$CFG->debug = $debuglevel;
|
||||
$CFG->debugdisplay = $debugdisplay;
|
||||
$CFG->debugdeveloper = $debugdeveloper;
|
||||
return $eventinformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function returns an array of all events for the plugins of the system.
|
||||
*
|
||||
* @param bool $withoutcomponent Return an eventlist without associated components.
|
||||
*
|
||||
* @return array A list of events from all plug-ins.
|
||||
*/
|
||||
protected static function get_non_core_eventlist($withoutcomponent = false) {
|
||||
global $CFG;
|
||||
// Disable developer debugging as deprecated events will fire warnings.
|
||||
// Setup backup variables to restore the following settings back to what they were when we are finished.
|
||||
$debuglevel = $CFG->debug;
|
||||
$debugdisplay = $CFG->debugdisplay;
|
||||
$debugdeveloper = $CFG->debugdeveloper;
|
||||
$CFG->debug = 0;
|
||||
$CFG->debugdisplay = false;
|
||||
$CFG->debugdeveloper = false;
|
||||
|
||||
$noncorepluginlist = array();
|
||||
$plugintypes = \core_component::get_plugin_types();
|
||||
foreach ($plugintypes as $plugintype => $notused) {
|
||||
$pluginlist = \core_component::get_plugin_list($plugintype);
|
||||
foreach ($pluginlist as $plugin => $directory) {
|
||||
$plugindirectory = $directory . '/classes/event';
|
||||
foreach (self::get_file_list($plugindirectory) as $eventname => $notused) {
|
||||
$fullpluginname = $plugintype . '_' . $plugin;
|
||||
$plugineventname = '\\' . $fullpluginname . '\\event\\' . $eventname;
|
||||
// Check that this is actually an event.
|
||||
if (method_exists($plugineventname, 'get_static_info') && $fullpluginname !== 'tool_monitor') { // No selfie here.
|
||||
$ref = new \ReflectionClass($plugineventname);
|
||||
if (!$ref->isAbstract() && $fullpluginname !== 'logstore_legacy') {
|
||||
if ($withoutcomponent) {
|
||||
$noncorepluginlist[$plugineventname] = $plugineventname::get_name_with_info();
|
||||
} else {
|
||||
$noncorepluginlist[$fullpluginname][$plugineventname] = $plugineventname::get_name_with_info();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now enable developer debugging as event information has been retrieved.
|
||||
$CFG->debug = $debuglevel;
|
||||
$CFG->debugdisplay = $debugdisplay;
|
||||
$CFG->debugdeveloper = $debugdeveloper;
|
||||
|
||||
return $noncorepluginlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of files with a full directory path in a specified directory.
|
||||
*
|
||||
* @param string $directory location of files.
|
||||
* @return array full location of files from the specified directory.
|
||||
*/
|
||||
protected static function get_file_list($directory) {
|
||||
global $CFG;
|
||||
$directoryroot = $CFG->dirroot;
|
||||
$finalfiles = array();
|
||||
if (is_dir($directory)) {
|
||||
if ($handle = opendir($directory)) {
|
||||
$files = scandir($directory);
|
||||
foreach ($files as $file) {
|
||||
if ($file != '.' && $file != '..') {
|
||||
// Ignore the file if it is external to the system.
|
||||
if (strrpos($directory, $directoryroot) !== false) {
|
||||
$location = substr($directory, strlen($directoryroot));
|
||||
$name = substr($file, 0, -4);
|
||||
$finalfiles[$name] = $location . '/' . $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $finalfiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of events present in the system.
|
||||
@@ -164,13 +38,58 @@ class eventlist {
|
||||
* @return array list of events present in the system.
|
||||
*/
|
||||
public static function get_all_eventlist($withoutcomponent = false) {
|
||||
if ($withoutcomponent) {
|
||||
$return = array_merge(self::get_core_eventlist(), self::get_non_core_eventlist($withoutcomponent));
|
||||
array_multisort($return, SORT_NATURAL);
|
||||
} else {
|
||||
$return = array_merge(array('core' => self::get_core_eventlist()),
|
||||
self::get_non_core_eventlist($withoutcomponent = false));
|
||||
global $CFG;
|
||||
|
||||
// Disable developer debugging as deprecated events will fire warnings.
|
||||
// Setup backup variables to restore the following settings back to what they were when we are finished.
|
||||
$debuglevel = $CFG->debug;
|
||||
$debugdisplay = $CFG->debugdisplay;
|
||||
$debugdeveloper = $CFG->debugdeveloper;
|
||||
$CFG->debug = 0;
|
||||
$CFG->debugdisplay = false;
|
||||
$CFG->debugdeveloper = false;
|
||||
|
||||
// List of exceptional events that will cause problems if displayed.
|
||||
$eventsignore = [
|
||||
\core\event\unknown_logged::class,
|
||||
\logstore_legacy\event\legacy_logged::class,
|
||||
];
|
||||
|
||||
$return = [];
|
||||
|
||||
$events = core_component::get_component_classes_in_namespace(null, 'event');
|
||||
foreach (array_keys($events) as $event) {
|
||||
// We need to filter all classes that extend event base, or the base class itself.
|
||||
if (is_a($event, \core\event\base::class, true) && !in_array($event, $eventsignore)) {
|
||||
|
||||
$reflectionclass = new ReflectionClass($event);
|
||||
if ($reflectionclass->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We can't choose this component's own events.
|
||||
[$component] = explode('\\', $event);
|
||||
if ($component === 'tool_monitor') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($withoutcomponent) {
|
||||
$return["\\{$event}"] = $event::get_name_with_info();
|
||||
} else {
|
||||
$return[$component]["\\{$event}"] = $event::get_name_with_info();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now enable developer debugging as event information has been retrieved.
|
||||
$CFG->debug = $debuglevel;
|
||||
$CFG->debugdisplay = $debugdisplay;
|
||||
$CFG->debugdeveloper = $debugdeveloper;
|
||||
|
||||
if ($withoutcomponent) {
|
||||
array_multisort($return, SORT_NATURAL);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
||||
@@ -151,25 +151,25 @@ Feature: Basic OAuth2 functionality
|
||||
| Service base URL | https://dc.imsglobal.org/ |
|
||||
When I press "Save changes"
|
||||
Then I should see "Changes saved"
|
||||
And I should see "Open Badges"
|
||||
And "Allow services" "icon" should exist in the "Open Badges" "table_row"
|
||||
And "Do not allow login" "icon" should exist in the "Open Badges" "table_row"
|
||||
And "Service discovery successful" "icon" should exist in the "Open Badges" "table_row"
|
||||
And the "src" attribute of "table.admintable th img" "css_element" should contain "IMS-Global-Logo.png"
|
||||
And I click on "Configure endpoints" "link" in the "Open Badges" "table_row"
|
||||
And I should see "https://dc.imsglobal.org/.well-known/badgeconnect.json" in the "discovery_endpoint" "table_row"
|
||||
And I should see "authorization_endpoint"
|
||||
And I navigate to "Server > OAuth 2 services" in site administration
|
||||
And I click on "Configure user field mappings" "link" in the "Open Badges" "table_row"
|
||||
And I should not see "given_name"
|
||||
And I should not see "middle_name"
|
||||
And I navigate to "Server > OAuth 2 services" in site administration
|
||||
And I click on "Edit" "link" in the "Open Badges" "table_row"
|
||||
And I should see "IMS Global Reference Implementation"
|
||||
And I click on "Edit" "link" in the "IMS Global Reference Implementation" "table_row"
|
||||
And I set the following fields to these values:
|
||||
| Name | IMS Global |
|
||||
And I press "Save changes"
|
||||
And I should see "Changes saved"
|
||||
And I should see "IMS Global"
|
||||
And "Allow services" "icon" should exist in the "IMS Global" "table_row"
|
||||
And "Do not allow login" "icon" should exist in the "IMS Global" "table_row"
|
||||
And "Service discovery successful" "icon" should exist in the "IMS Global" "table_row"
|
||||
And the "src" attribute of "table.admintable th img" "css_element" should contain "IMS-Global-Logo.png"
|
||||
And I click on "Configure endpoints" "link" in the "IMS Global" "table_row"
|
||||
And I should see "https://dc.imsglobal.org/.well-known/badgeconnect.json" in the "discovery_endpoint" "table_row"
|
||||
And I should see "authorization_endpoint"
|
||||
And I navigate to "Server > OAuth 2 services" in site administration
|
||||
And I click on "Configure user field mappings" "link" in the "IMS Global" "table_row"
|
||||
And I should not see "given_name"
|
||||
And I should not see "middle_name"
|
||||
And I navigate to "Server > OAuth 2 services" in site administration
|
||||
And I click on "Delete" "link" in the "IMS Global" "table_row"
|
||||
And I should see "Are you sure you want to delete the identity issuer \"IMS Global\"?"
|
||||
And I press "Continue"
|
||||
|
||||
@@ -573,7 +573,7 @@ class acceptances_table extends \table_sql {
|
||||
if ($row->timemodified) {
|
||||
if ($this->is_downloading()) {
|
||||
// Use timestamp format readable for both machines and humans.
|
||||
return date_format_string($row->timemodified, '%Y-%m-%d %H:%M:%S %Z');
|
||||
return date_format_string((int) $row->timemodified, '%Y-%m-%d %H:%M:%S %Z');
|
||||
} else {
|
||||
// Use localised calendar format.
|
||||
return userdate($row->timemodified, get_string('strftimedatetime'));
|
||||
|
||||
@@ -29,6 +29,7 @@ require_once(__DIR__ . '/fixtures/test_target_course_level_shortname.php');
|
||||
* @package core_analytics
|
||||
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core_analytics\manager
|
||||
*/
|
||||
class manager_test extends \advanced_testcase {
|
||||
|
||||
@@ -491,25 +492,63 @@ class manager_test extends \advanced_testcase {
|
||||
// No potential context restrictions.
|
||||
$this->assertFalse(\core_analytics\manager::get_potential_context_restrictions([]));
|
||||
|
||||
$defaultcategory = \core_course_category::get_default();
|
||||
$defaultcategorycontext = $defaultcategory->get_context();
|
||||
|
||||
// Include the all context levels so the misc. category get included.
|
||||
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions());
|
||||
$this->assertEquals([
|
||||
$defaultcategorycontext->id => "Category: {$defaultcategory->name}",
|
||||
], manager::get_potential_context_restrictions());
|
||||
|
||||
$this->getDataGenerator()->create_course();
|
||||
$this->getDataGenerator()->create_category();
|
||||
$this->assertCount(3, \core_analytics\manager::get_potential_context_restrictions());
|
||||
$this->assertCount(3, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE, CONTEXT_COURSECAT]));
|
||||
$category = $this->getDataGenerator()->create_category(['name' => 'My category']);
|
||||
$categorycontext = $category->get_context();
|
||||
|
||||
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE]));
|
||||
$this->assertCount(2, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT]));
|
||||
$courseone = $this->getDataGenerator()->create_course(['fullname' => 'Course one', 'shortname' => 'CS1']);
|
||||
$courseonecontext = \context_course::instance($courseone->id);
|
||||
|
||||
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category'));
|
||||
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category 1'));
|
||||
$this->assertCount(
|
||||
2,
|
||||
\core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT],
|
||||
get_string('defaultcategoryname')
|
||||
));
|
||||
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course 1'));
|
||||
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course'));
|
||||
$coursetwo = $this->getDataGenerator()->create_course(['fullname' => 'Course two', 'shortname' => 'CS2']);
|
||||
$coursetwocontext = \context_course::instance($coursetwo->id);
|
||||
|
||||
// All context levels.
|
||||
$this->assertEqualsCanonicalizing([
|
||||
$defaultcategorycontext->id => "Category: {$defaultcategory->name}",
|
||||
$categorycontext->id => "Category: {$category->name}",
|
||||
$courseonecontext->id => "Course: {$courseone->shortname}",
|
||||
$coursetwocontext->id => "Course: {$coursetwo->shortname}",
|
||||
], manager::get_potential_context_restrictions());
|
||||
|
||||
// All category/course context levels.
|
||||
$this->assertEqualsCanonicalizing([
|
||||
$defaultcategorycontext->id => "Category: {$defaultcategory->name}",
|
||||
$categorycontext->id => "Category: {$category->name}",
|
||||
$courseonecontext->id => "Course: {$courseone->shortname}",
|
||||
$coursetwocontext->id => "Course: {$coursetwo->shortname}",
|
||||
], manager::get_potential_context_restrictions([CONTEXT_COURSECAT, CONTEXT_COURSE]));
|
||||
|
||||
// All category context levels.
|
||||
$this->assertEqualsCanonicalizing([
|
||||
$defaultcategorycontext->id => "Category: {$defaultcategory->name}",
|
||||
$categorycontext->id => "Category: {$category->name}",
|
||||
], manager::get_potential_context_restrictions([CONTEXT_COURSECAT]));
|
||||
|
||||
// Filtered category context levels.
|
||||
$this->assertEquals([
|
||||
$categorycontext->id => "Category: {$category->name}",
|
||||
], manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'My cat'));
|
||||
|
||||
$this->assertEmpty(manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'nothing'));
|
||||
|
||||
// All course context levels.
|
||||
$this->assertEqualsCanonicalizing([
|
||||
$courseonecontext->id => "Course: {$courseone->shortname}",
|
||||
$coursetwocontext->id => "Course: {$coursetwo->shortname}",
|
||||
], manager::get_potential_context_restrictions([CONTEXT_COURSE]));
|
||||
|
||||
// Filtered course context levels.
|
||||
$this->assertEquals([
|
||||
$courseonecontext->id => "Course: {$courseone->shortname}",
|
||||
], manager::get_potential_context_restrictions([CONTEXT_COURSE], 'one'));
|
||||
|
||||
$this->assertEmpty(manager::get_potential_context_restrictions([CONTEXT_COURSE], 'nothing'));
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -199,7 +199,6 @@ class auth_plugin_lti extends \auth_plugin_base {
|
||||
}
|
||||
}
|
||||
$user = $this->create_new_account($member, $iss);
|
||||
$this->update_user_account($user, $member, $iss);
|
||||
return \core_user::get_user($user->id);
|
||||
}
|
||||
}
|
||||
@@ -381,7 +380,11 @@ class auth_plugin_lti extends \auth_plugin_base {
|
||||
'lastname' => $userdata['family_name'] ?? $iss,
|
||||
'email' => $email
|
||||
];
|
||||
user_update_user($update);
|
||||
$userfieldstocompare = array_intersect_key((array) $user, $update);
|
||||
|
||||
if (!empty(array_diff($update, $userfieldstocompare))) {
|
||||
user_update_user($update); // Only update if there's a change.
|
||||
}
|
||||
|
||||
if (!empty($userdata['picture'])) {
|
||||
try {
|
||||
|
||||
@@ -249,12 +249,15 @@ class auth_test extends \advanced_testcase {
|
||||
$mockjwtdata = $this->get_mock_launchdata_for_user($launchdata['user'], $launchdata['migration_claim'] ?? []);
|
||||
|
||||
// Authenticate the platform user.
|
||||
$sink = $this->redirectEvents();
|
||||
$countusersbefore = $DB->count_records('user');
|
||||
$user = $auth->find_or_create_user_from_launch($mockjwtdata, true, $legacysecrets);
|
||||
if (!empty($expected['migration_debugging'])) {
|
||||
$this->assertDebuggingCalled();
|
||||
}
|
||||
$countusersafter = $DB->count_records('user');
|
||||
$events = $sink->get_events();
|
||||
$sink->close();
|
||||
|
||||
// Verify user count is correct. i.e. no user is created when migration claim is correctly processed or when
|
||||
// the user has authenticated with the tool before.
|
||||
@@ -295,15 +298,18 @@ class auth_test extends \advanced_testcase {
|
||||
$this->verify_user_profile_image_updated($user->id);
|
||||
}
|
||||
|
||||
// If migrated, verify the user account is reusing the legacy user account.
|
||||
if (!empty($expected['migrated']) && $expected['migrated']) {
|
||||
// If migrated, verify the user account is reusing the legacy user account.
|
||||
$legacyuserids = array_column($legacyusers, 'id');
|
||||
$this->assertContains($user->id, $legacyuserids);
|
||||
}
|
||||
|
||||
// If the user is authenticating a second time, confirm the same account is being returned.
|
||||
if (isset($firstauthuser)) {
|
||||
$this->assertInstanceOf(\core\event\user_updated::class, $events[0]);
|
||||
} else if (isset($firstauthuser)) {
|
||||
// If the user is authenticating a second time, confirm the same account is being returned.
|
||||
$this->assertEquals($firstauthuser->id, $user->id);
|
||||
$this->assertEmpty($events); // The user authenticated with the same data once before, so we don't expect an update.
|
||||
} else {
|
||||
// The user wasn't migrated and hasn't launched before, so we expect a user_created event.
|
||||
$this->assertInstanceOf(\core\event\user_created::class, $events[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,9 +825,12 @@ class auth_test extends \advanced_testcase {
|
||||
$mockmemberdata = $this->get_mock_member_data_for_user($memberdata['user'], $memberdata['legacy_user_id'] ?? '');
|
||||
|
||||
// Authenticate the platform user.
|
||||
$sink = $this->redirectEvents();
|
||||
$countusersbefore = $DB->count_records('user');
|
||||
$user = $auth->find_or_create_user_from_membership($mockmemberdata, $iss, $legacyconsumerkey ?? '');
|
||||
$countusersafter = $DB->count_records('user');
|
||||
$events = $sink->get_events();
|
||||
$sink->close();
|
||||
|
||||
// Verify user count is correct. i.e. no user is created when migration claim is correctly processed or when
|
||||
// the user has authenticated with the tool before.
|
||||
@@ -857,15 +866,18 @@ class auth_test extends \advanced_testcase {
|
||||
break;
|
||||
}
|
||||
|
||||
// If migrated, verify the user account is reusing the legacy user account.
|
||||
if (!empty($expected['migrated']) && $expected['migrated']) {
|
||||
// If migrated, verify the user account is reusing the legacy user account.
|
||||
$legacyuserids = array_column($legacyusers, 'id');
|
||||
$this->assertContains($user->id, $legacyuserids);
|
||||
}
|
||||
|
||||
// If the user is authenticating a second time, confirm the same account is being returned.
|
||||
if (isset($firstauthuser)) {
|
||||
$this->assertInstanceOf(\core\event\user_updated::class, $events[0]);
|
||||
} else if (isset($firstauthuser)) {
|
||||
// If the user is authenticating a second time, confirm the same account is being returned.
|
||||
$this->assertEquals($firstauthuser->id, $user->id);
|
||||
$this->assertEmpty($events); // The user authenticated with the same data once before, so we don't expect an update.
|
||||
} else {
|
||||
// The user wasn't migrated and hasn't launched before, so we expect a user_created event.
|
||||
$this->assertInstanceOf(\core\event\user_created::class, $events[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1090,7 +1102,23 @@ class auth_test extends \advanced_testcase {
|
||||
'PII' => self::PII_NONE,
|
||||
'migrated' => false
|
||||
]
|
||||
]
|
||||
],
|
||||
'Existing (linked) platform learner including PII, no legacy data, no consumer key bound, no legacy id' => [
|
||||
'legacy_data' => null,
|
||||
'launch_data' => [
|
||||
'has_authenticated_before' => true,
|
||||
'user' => $this->get_mock_users_with_ids(
|
||||
['1'],
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
|
||||
)[0],
|
||||
],
|
||||
'iss' => $this->issuer,
|
||||
'legacy_consumer_key' => null,
|
||||
'expected' => [
|
||||
'PII' => self::PII_ALL,
|
||||
'migrated' => false
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@ class backup_course_task extends backup_task {
|
||||
$content = self::encode_links_helper($content, 'GRADEREPORTINDEXBYID', '/grade/report/index.php?id=');
|
||||
$content = self::encode_links_helper($content, 'BADGESVIEWBYID', '/badges/view.php?type=2&id=');
|
||||
$content = self::encode_links_helper($content, 'USERINDEXVIEWBYID', '/user/index.php?id=');
|
||||
$content = self::encode_links_helper($content, 'PLUGINFILEBYCONTEXT', '/pluginfile.php/');
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
@@ -93,6 +93,8 @@ class base64_encode_final_element extends backup_final_element {
|
||||
* @param string $value Original value coming from backup step source, usually db.
|
||||
*/
|
||||
public function set_value($value) {
|
||||
// Avoid null being passed to base64_encode.
|
||||
$value = $value ?? '';
|
||||
parent::set_value(base64_encode($value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2383,28 +2383,40 @@ class backup_annotate_all_question_files extends backup_execution_step {
|
||||
AND bi.itemname = 'question_categoryfinal'", array($this->get_backupid()));
|
||||
// To know about qtype specific components/fileareas
|
||||
$components = backup_qtype_plugin::get_components_and_fileareas();
|
||||
$progress = $this->task->get_progress();
|
||||
$progress->start_progress($this->get_name());
|
||||
// Let's loop
|
||||
foreach($rs as $record) {
|
||||
// Backup all the file areas the are managed by the core question component.
|
||||
// That is, by the question_type base class. In particular, we don't want
|
||||
// to include files belonging to responses here.
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'questiontext', null);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'generalfeedback', null);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'answer', null);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'answerfeedback', null);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'hint', null);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'correctfeedback', null);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'partiallycorrectfeedback', null);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'incorrectfeedback', null);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'questiontext', null,
|
||||
$progress);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'generalfeedback', null,
|
||||
$progress);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'answer', null,
|
||||
$progress);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'answerfeedback', null,
|
||||
$progress);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'hint', null,
|
||||
$progress);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'correctfeedback', null,
|
||||
$progress);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question',
|
||||
'partiallycorrectfeedback', null, $progress);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'incorrectfeedback', null,
|
||||
$progress);
|
||||
|
||||
// For files belonging to question types, we make the leap of faith that
|
||||
// all the files belonging to the question type are part of the question definition,
|
||||
// so we can just backup all the files in bulk, without specifying each
|
||||
// file area name separately.
|
||||
foreach ($components as $component => $fileareas) {
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, $component, null, null);
|
||||
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, $component, null, null,
|
||||
$progress);
|
||||
}
|
||||
}
|
||||
$progress->end_progress();
|
||||
$rs->close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ class restore_course_task extends restore_task {
|
||||
$rules[] = new restore_decode_rule('GRADEREPORTINDEXBYID', '/grade/report/index.php?id=$1', 'course');
|
||||
$rules[] = new restore_decode_rule('BADGESVIEWBYID', '/badges/view.php?type=2&id=$1', 'course');
|
||||
$rules[] = new restore_decode_rule('USERINDEXVIEWBYID', '/user/index.php?id=$1', 'course');
|
||||
$rules[] = new restore_decode_rule('PLUGINFILEBYCONTEXT', '/pluginfile.php/$1', 'context');
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
@@ -119,18 +119,14 @@ class restore_gradebook_structure_step extends restore_structure_step {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Identify the backup we're dealing with.
|
||||
$backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
|
||||
$backupbuild = 0;
|
||||
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
|
||||
if (!empty($matches[1])) {
|
||||
$backupbuild = (int) $matches[1]; // The date of Moodle build at the time of the backup.
|
||||
}
|
||||
$restoretask = $this->get_task();
|
||||
|
||||
// On older versions the freeze value has to be converted.
|
||||
// We do this from here as it is happening right before the file is read.
|
||||
// This only targets the backup files that can contain the legacy freeze.
|
||||
if ($backupbuild > 20150618 && (version_compare($backuprelease, '3.0', '<') || $backupbuild < 20160527)) {
|
||||
if ($restoretask->backup_version_compare(20150618, '>')
|
||||
&& $restoretask->backup_release_compare('3.0', '<')
|
||||
|| $restoretask->backup_version_compare(20160527, '<')) {
|
||||
$this->rewrite_step_backup_file_for_legacy_freeze($fullpath);
|
||||
}
|
||||
|
||||
@@ -505,24 +501,25 @@ class restore_gradebook_structure_step extends restore_structure_step {
|
||||
protected function gradebook_calculation_freeze() {
|
||||
global $CFG;
|
||||
$gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
|
||||
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
|
||||
$backupbuild = (int)$matches[1];
|
||||
$backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
|
||||
$restoretask = $this->get_task();
|
||||
|
||||
// Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619).
|
||||
if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150619) {
|
||||
if (!$gradebookcalculationsfreeze && $restoretask->backup_version_compare(20141110, '>=')
|
||||
&& $restoretask->backup_version_compare(20150619, '<')) {
|
||||
require_once($CFG->libdir . '/db/upgradelib.php');
|
||||
upgrade_extra_credit_weightoverride($this->get_courseid());
|
||||
}
|
||||
// Calculated grade items need recalculating for backups made between 2.8 release (20141110) and the fix release (20150627).
|
||||
if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150627) {
|
||||
if (!$gradebookcalculationsfreeze && $restoretask->backup_version_compare(20141110, '>=')
|
||||
&& $restoretask->backup_version_compare(20150627, '<')) {
|
||||
require_once($CFG->libdir . '/db/upgradelib.php');
|
||||
upgrade_calculated_grade_items($this->get_courseid());
|
||||
}
|
||||
// Courses from before 3.1 (20160518) may have a letter boundary problem and should be checked for this issue.
|
||||
// Backups from before and including 2.9 could have a build number that is greater than 20160518 and should
|
||||
// be checked for this problem.
|
||||
if (!$gradebookcalculationsfreeze && ($backupbuild < 20160518 || version_compare($backuprelease, '2.9', '<='))) {
|
||||
if (!$gradebookcalculationsfreeze
|
||||
&& ($restoretask->backup_version_compare(20160518, '<') || $restoretask->backup_release_compare('2.9', '<='))) {
|
||||
require_once($CFG->libdir . '/db/upgradelib.php');
|
||||
upgrade_course_letter_boundary($this->get_courseid());
|
||||
}
|
||||
@@ -4810,13 +4807,8 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
protected function define_structure() {
|
||||
|
||||
// Check if the backup is a pre 4.0 one.
|
||||
$backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
|
||||
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
|
||||
$backupbuild = (int)$matches[1];
|
||||
$before40 = false;
|
||||
if (version_compare($backuprelease, '4.0', '<') || $backupbuild < 20220202) {
|
||||
$before40 = true;
|
||||
}
|
||||
$restoretask = $this->get_task();
|
||||
$before40 = $restoretask->backup_release_compare('4.0', '<') || $restoretask->backup_version_compare(20220202, '<');
|
||||
// Start creating the path, category should be the first one.
|
||||
$paths = [];
|
||||
$paths [] = new restore_path_element('question_category', '/question_categories/question_category');
|
||||
@@ -4898,13 +4890,8 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
|
||||
// Before 3.5, question categories could be created at top level.
|
||||
// From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
|
||||
$backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
|
||||
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
|
||||
$backupbuild = (int)$matches[1];
|
||||
$before35 = false;
|
||||
if (version_compare($backuprelease, '3.5', '<') || $backupbuild < 20180205) {
|
||||
$before35 = true;
|
||||
}
|
||||
$restoretask = $this->get_task();
|
||||
$before35 = $restoretask->backup_release_compare('3.5', '<') || $restoretask->backup_version_compare(20180205, '<');
|
||||
if (empty($mapping->info->parent) && $before35) {
|
||||
$top = question_get_top_category($data->contextid, true);
|
||||
$data->parent = $top->id;
|
||||
@@ -5052,14 +5039,8 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
$oldid = $data->id;
|
||||
|
||||
// Check if the backup is a pre 4.0 one.
|
||||
$backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
|
||||
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
|
||||
$backupbuild = (int)$matches[1];
|
||||
$before40 = false;
|
||||
if (version_compare($backuprelease, '4.0', '<') || $backupbuild < 20220202) {
|
||||
$before40 = true;
|
||||
}
|
||||
if ($before40) {
|
||||
$restoretask = $this->get_task();
|
||||
if ($restoretask->backup_release_compare('4.0', '<') || $restoretask->backup_version_compare(20220202, '<')) {
|
||||
// Check we have one mapping for this question.
|
||||
if (!$questionmapping = $this->get_mapping('question', $oldid)) {
|
||||
return; // No mapping = this question doesn't need to be created/mapped.
|
||||
@@ -5284,13 +5265,7 @@ class restore_move_module_questions_categories extends restore_execution_step {
|
||||
protected function define_execution() {
|
||||
global $DB;
|
||||
|
||||
$backuprelease = $this->task->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
|
||||
preg_match('/(\d{8})/', $this->task->get_info()->moodle_release, $matches);
|
||||
$backupbuild = (int)$matches[1];
|
||||
$after35 = false;
|
||||
if (version_compare($backuprelease, '3.5', '>=') && $backupbuild > 20180205) {
|
||||
$after35 = true;
|
||||
}
|
||||
$after35 = $this->task->backup_release_compare('3.5', '>=') && $this->task->backup_version_compare(20180205, '>');
|
||||
|
||||
$contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE);
|
||||
foreach ($contexts as $contextid => $contextlevel) {
|
||||
|
||||
@@ -578,16 +578,11 @@ abstract class restore_dbops {
|
||||
CONTEXT_SYSTEM => CONTEXT_COURSE,
|
||||
CONTEXT_COURSECAT => CONTEXT_COURSE);
|
||||
|
||||
/** @var restore_controller $rc */
|
||||
$rc = restore_controller_dbops::load_controller($restoreid);
|
||||
$restoreinfo = $rc->get_info();
|
||||
$plan = $rc->get_plan();
|
||||
$after35 = $plan->backup_release_compare('3.5', '>=') && $plan->backup_version_compare(20180205, '>');
|
||||
$rc->destroy(); // Always need to destroy.
|
||||
$backuprelease = $restoreinfo->backup_release; // The major version: 2.9, 3.0, 3.10...
|
||||
preg_match('/(\d{8})/', $restoreinfo->moodle_release, $matches);
|
||||
$backupbuild = (int)$matches[1];
|
||||
$after35 = false;
|
||||
if (version_compare($backuprelease, '3.5', '>=') && $backupbuild > 20180205) {
|
||||
$after35 = true;
|
||||
}
|
||||
|
||||
// For any contextlevel, follow this process logic:
|
||||
//
|
||||
|
||||
@@ -212,6 +212,30 @@ class restore_plan extends base_plan implements loggable {
|
||||
}
|
||||
$progress->end_progress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the provided moodle version with the one the backup was taken from.
|
||||
*
|
||||
* @param int $version Moodle version number (YYYYMMDD or YYYYMMDDXX)
|
||||
* @param string $operator Operator to compare the provided version to the backup version. {@see version_compare()}
|
||||
* @return bool True if the comparison passes.
|
||||
*/
|
||||
public function backup_version_compare(int $version, string $operator): bool {
|
||||
preg_match('/(\d{' . strlen($version) . '})/', $this->get_info()->moodle_version, $matches);
|
||||
$backupbuild = (int)$matches[1];
|
||||
return version_compare($backupbuild, $version, $operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the provided moodle release with the one the backup was taken from.
|
||||
*
|
||||
* @param string $release Moodle release (X.Y or X.Y.Z)
|
||||
* @param string $operator Operator to compare the provided release to the backup release. {@see version_compare()}
|
||||
* @return bool True if the comparison passes.
|
||||
*/
|
||||
public function backup_release_compare(string $release, string $operator): bool {
|
||||
return version_compare($this->get_info()->backup_release, $release, $operator);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -124,6 +124,28 @@ abstract class restore_task extends base_task {
|
||||
$this->after_restore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the provided moodle version with the one the backup was taken from.
|
||||
*
|
||||
* @param int $version Moodle version number (YYYYMMDD or YYYYMMDDXX)
|
||||
* @param string $operator Operator to compare the provided version to the backup version. {@see version_compare()}
|
||||
* @return bool True if the comparison passes.
|
||||
*/
|
||||
public function backup_version_compare(int $version, string $operator) {
|
||||
return $this->plan->backup_version_compare($version, $operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the provided moodle release with the one the backup was taken from.
|
||||
*
|
||||
* @param string $release Moodle release (X.Y or X.Y.Z)
|
||||
* @param string $operator Operator to compare the provided release to the backup release. {@see version_compare()}
|
||||
* @return bool True if the comparison passes.
|
||||
*/
|
||||
public function backup_release_compare(string $release, string $operator) {
|
||||
return $this->plan->backup_release_compare($release, $operator);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -786,6 +786,10 @@ class core_backup_renderer extends plugin_renderer_base {
|
||||
'extraclasses' => 'rcs-search mb-3 w-25',
|
||||
'inputname' => restore_course_search::$VAR_SEARCH,
|
||||
'searchstring' => get_string('searchcourses'),
|
||||
'buttonattributes' => [
|
||||
(object) ['key' => 'name', 'value' => 'searchcourses'],
|
||||
(object) ['key' => 'value', 'value' => 1],
|
||||
],
|
||||
'query' => $component->get_search(),
|
||||
];
|
||||
$output .= $this->output->render_from_template('core/search_input', $data);
|
||||
@@ -955,6 +959,10 @@ class core_backup_renderer extends plugin_renderer_base {
|
||||
'extraclasses' => 'rcs-search mb-3 w-25',
|
||||
'inputname' => restore_category_search::$VAR_SEARCH,
|
||||
'searchstring' => get_string('searchcoursecategories'),
|
||||
'buttonattributes' => [
|
||||
(object) ['key' => 'name', 'value' => 'searchcourses'],
|
||||
(object) ['key' => 'value', 'value' => 1],
|
||||
],
|
||||
'query' => $component->get_search(),
|
||||
];
|
||||
$output .= $this->output->render_from_template('core/search_input', $data);
|
||||
|
||||
@@ -262,7 +262,7 @@ class xml_writer {
|
||||
protected function xml_safe_utf8($content) {
|
||||
$content = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is', '', $content ?? ''); // clean CTRL chars.
|
||||
$content = preg_replace("/\r\n|\r/", "\n", $content); // Normalize line&return=>line
|
||||
return $content;
|
||||
return fix_utf8($content);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,6 +36,9 @@ use core_badges\backpack_api2p1_mapping;
|
||||
use core_badges\oauth2\client;
|
||||
use curl;
|
||||
use stdClass;
|
||||
use core\oauth2\issuer;
|
||||
use core\oauth2\endpoint;
|
||||
use core\oauth2\discovery\imsbadgeconnect;
|
||||
|
||||
/**
|
||||
* To process badges with backpack and control api request and this class using for Open Badge API v2.1 methods.
|
||||
@@ -61,8 +64,11 @@ class backpack_api2p1 {
|
||||
/** @var null version api of the backpack. */
|
||||
protected $backpackapiversion;
|
||||
|
||||
/** @var null api URL of the backpack. */
|
||||
protected $backpackapiurl = '';
|
||||
/** @var issuer The OAuth2 Issuer for this backpack */
|
||||
protected issuer $issuer;
|
||||
|
||||
/** @var endpoint The apiBase endpoint */
|
||||
protected endpoint $apibase;
|
||||
|
||||
/**
|
||||
* backpack_api2p1 constructor.
|
||||
@@ -75,7 +81,6 @@ class backpack_api2p1 {
|
||||
if (!empty($externalbackpack)) {
|
||||
$this->externalbackpack = $externalbackpack;
|
||||
$this->backpackapiversion = $externalbackpack->apiversion;
|
||||
$this->backpackapiurl = $externalbackpack->backpackapiurl;
|
||||
$this->get_clientid = $this->get_clientid($externalbackpack->oauth2_issuerid);
|
||||
|
||||
if (!($this->tokendata = $this->get_stored_token($externalbackpack->id))
|
||||
@@ -87,6 +92,44 @@ class backpack_api2p1 {
|
||||
$this->define_mappings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises or returns the OAuth2 issuer associated to this backpack.
|
||||
*
|
||||
* @return issuer
|
||||
*/
|
||||
protected function get_issuer(): issuer {
|
||||
if (!isset($this->issuer)) {
|
||||
$this->issuer = new \core\oauth2\issuer($this->externalbackpack->oauth2_issuerid);
|
||||
}
|
||||
return $this->issuer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the apiBase url associated to this backpack.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_api_base_url(): string {
|
||||
if (!isset($this->apibase)) {
|
||||
$apibase = endpoint::get_record([
|
||||
'issuerid' => $this->externalbackpack->oauth2_issuerid,
|
||||
'name' => 'apiBase',
|
||||
]);
|
||||
|
||||
if (empty($apibase)) {
|
||||
imsbadgeconnect::create_endpoints($this->get_issuer());
|
||||
$apibase = endpoint::get_record([
|
||||
'issuerid' => $this->externalbackpack->oauth2_issuerid,
|
||||
'name' => 'apiBase',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->apibase = $apibase;
|
||||
}
|
||||
|
||||
return $this->apibase->get('url');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Define the mappings supported by this usage and api version.
|
||||
@@ -157,7 +200,7 @@ class backpack_api2p1 {
|
||||
foreach ($this->mappings as $mapping) {
|
||||
if ($mapping->is_match($action)) {
|
||||
return $mapping->request(
|
||||
$this->backpackapiurl,
|
||||
$this->get_api_base_url(),
|
||||
$tokenkey,
|
||||
$postdata
|
||||
);
|
||||
@@ -194,6 +237,7 @@ class backpack_api2p1 {
|
||||
private function get_clientid($issuerid) {
|
||||
$issuer = \core\oauth2\api::get_issuer($issuerid);
|
||||
if (!empty($issuer)) {
|
||||
$this->issuer = $issuer;
|
||||
$this->clientid = $issuer->get('clientid');
|
||||
}
|
||||
}
|
||||
@@ -212,7 +256,7 @@ class backpack_api2p1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
$issuer = new \core\oauth2\issuer($this->externalbackpack->oauth2_issuerid);
|
||||
$issuer = $this->get_issuer();
|
||||
$client = new client($issuer, new moodle_url('/badges/mybadges.php'), '', $this->externalbackpack);
|
||||
if (!$client->is_logged_in()) {
|
||||
$redirecturl = new moodle_url('/badges/mybadges.php', ['error' => 'backpackexporterror']);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@core @core_badges @_file_upload
|
||||
@core @core_badges
|
||||
Feature: Backpack badges
|
||||
Test the settings to add/update a backpack for a site and user.
|
||||
I need to verify display backpack in the my profile
|
||||
@@ -37,36 +37,21 @@ Feature: Backpack badges
|
||||
|
||||
@javascript
|
||||
Scenario: Verify backback settings
|
||||
Given I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Badges settings" in site administration
|
||||
And I set the following fields to these values:
|
||||
| External backpack connection | 1 |
|
||||
And I press "Save changes"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
And I click on "Move up" "link" in the "https://dc.imsglobal.org" "table_row"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Test badge verify backpack |
|
||||
| Version | v1 |
|
||||
| Language | English |
|
||||
| Description | Test badge description |
|
||||
| Image author | http://author.example.com |
|
||||
| Image caption | Test caption image |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I set the field "type" to "Manual issue by role"
|
||||
And I set the field "Manager" to "1"
|
||||
And I press "Save"
|
||||
And I press "Enable access"
|
||||
And I press "Continue"
|
||||
And I select "Recipients (0)" from the "jump" singleselect
|
||||
And I press "Award badge"
|
||||
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
|
||||
And I press "Award badge"
|
||||
And I log out
|
||||
When I am on homepage
|
||||
And I log in as "student1"
|
||||
Given the following "core_badges > Badge" exists:
|
||||
| name | Test badge verify backpack |
|
||||
| version | 1 |
|
||||
| language | en |
|
||||
| description | Test badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
| imageauthorurl | http://author.example.com |
|
||||
| imagecaption | Test caption image |
|
||||
And the following "core_badges > Criteria" exists:
|
||||
| badge | Test badge verify backpack |
|
||||
| role | editingteacher |
|
||||
And the following "core_badges > Issued badge" exists:
|
||||
| badge | Test badge verify backpack |
|
||||
| user | student1 |
|
||||
When I log in as "student1"
|
||||
And I follow "Preferences" in the user menu
|
||||
And I follow "Backpack settings"
|
||||
Then I should see "https://dc.imsglobal.org"
|
||||
@@ -82,26 +67,20 @@ Feature: Backpack badges
|
||||
And I press "Save changes"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
And I click on "Move up" "link" in the "https://dc.imsglobal.org" "table_row"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Test badge verify backpack |
|
||||
| Version | v1 |
|
||||
| Language | English |
|
||||
| Description | Test badge description |
|
||||
| Image author | http://author.example.com |
|
||||
| Image caption | Test caption image |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I set the field "type" to "Manual issue by role"
|
||||
And I set the field "Manager" to "1"
|
||||
And I press "Save"
|
||||
And I press "Enable access"
|
||||
And I press "Continue"
|
||||
And I select "Recipients (0)" from the "jump" singleselect
|
||||
And I press "Award badge"
|
||||
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
|
||||
And I press "Award badge"
|
||||
And I log out
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Test badge verify backpack |
|
||||
| version | 1 |
|
||||
| language | en |
|
||||
| description | Test badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
| imageauthorurl | http://author.example.com |
|
||||
| imagecaption | Test caption image |
|
||||
And the following "core_badges > Criteria" exists:
|
||||
| badge | Test badge verify backpack |
|
||||
| role | editingteacher |
|
||||
And the following "core_badges > Issued badge" exists:
|
||||
| badge | Test badge verify backpack |
|
||||
| user | student1 |
|
||||
And the following "setup backpack connected" exist:
|
||||
| user | externalbackpack |
|
||||
| student1 | https://dc.imsglobal.org |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@core @core_badges @_file_upload @javascript
|
||||
@core @core_badges @javascript
|
||||
Feature: Test tertiary navigation as various users
|
||||
|
||||
Background:
|
||||
@@ -15,38 +15,32 @@ Feature: Test tertiary navigation as various users
|
||||
| teacher | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
| nonediting | C1 | teacher |
|
||||
# Create system badge and define a criterion.
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Badges > Add a new badge" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Testing course badge |
|
||||
| Version | 1.1 |
|
||||
| Language | Catalan |
|
||||
| Description | Testing course badge description |
|
||||
| Image author | http://author.example.com |
|
||||
| Image caption | Test caption image |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I set the field "type" to "Manual issue by role"
|
||||
And I expand all fieldsets
|
||||
And I set the field "Teacher" to "1"
|
||||
And I press "Save"
|
||||
And I am on site homepage
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Testing site badge |
|
||||
| Version | 1.1 |
|
||||
| Language | Catalan |
|
||||
| Description | Testing site badge description |
|
||||
| Image author | http://author.example.com |
|
||||
| Image caption | Test caption image |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I set the field "type" to "Manual issue by role"
|
||||
And I expand all fieldsets
|
||||
And I set the field "Teacher" to "1"
|
||||
And I press "Save"
|
||||
# Create a course badge in our test course.
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Testing course badge |
|
||||
| status | inactive |
|
||||
| type | 2 |
|
||||
| course | C1 |
|
||||
| version | 1.1 |
|
||||
| language | ca |
|
||||
| description | Testing course badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
| imageauthorurl | http://author.example.com |
|
||||
| imagecaption | Test caption image |
|
||||
# Create a site badge.
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Testing site badge |
|
||||
| status | inactive |
|
||||
| version | 1.1 |
|
||||
| language | ca |
|
||||
| description | Testing site badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
| imageauthorurl | http://author.example.com |
|
||||
| imagecaption | Test caption image |
|
||||
And the following "core_badges > Criterias" exist:
|
||||
| badge | role |
|
||||
| Testing course badge | editingteacher |
|
||||
| Testing site badge | editingteacher |
|
||||
|
||||
Scenario Outline: Check navigation as different users in a course context
|
||||
Given I log in as "<user>"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@mod @mod_quiz @core @core_badges @_file_upload @javascript
|
||||
@mod @mod_quiz @core @core_badges @javascript
|
||||
Feature: Award badges based on activity completion
|
||||
In order to ensure a student has learned the material before being marked complete
|
||||
As a teacher
|
||||
@@ -38,15 +38,13 @@ Feature: Award badges based on activity completion
|
||||
And user "student2" has attempted "Test quiz name" with responses:
|
||||
| slot | response |
|
||||
| 1 | False |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I change window size to "large"
|
||||
And I navigate to "Badges > Add a new badge" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Course Badge |
|
||||
| Description | Course badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Course Badge |
|
||||
| status | 0 |
|
||||
| type | 2 |
|
||||
| course | C1 |
|
||||
| description | Course badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
|
||||
Scenario: Student earns a badge using activity completion, but does not get passing grade
|
||||
Given I am on the "Course 1" course page logged in as teacher1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@core @core_badges @_file_upload
|
||||
@core @core_badges
|
||||
Feature: Award badges based on cohort
|
||||
In order to award badges to users based on their cohort membership
|
||||
As an admin
|
||||
@@ -18,13 +18,15 @@ Feature: Award badges based on cohort
|
||||
| user | cohort |
|
||||
| user1 | CH1 |
|
||||
| user2 | CH2 |
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Site Badge |
|
||||
| status | 0 |
|
||||
| description | Site badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Cohort membership"
|
||||
And I set the field "id_cohort_cohorts" to "One Cohort"
|
||||
And I press "Save"
|
||||
@@ -53,13 +55,15 @@ Feature: Award badges based on cohort
|
||||
| user1 | CH2 |
|
||||
| user2 | CH1 |
|
||||
| user2 | CH3 |
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Site Badge |
|
||||
| status | 0 |
|
||||
| description | Site badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Cohort membership"
|
||||
And I expand all fieldsets
|
||||
And I set the field "id_cohort_cohorts" to "One Cohort,Two Cohort"
|
||||
@@ -93,13 +97,15 @@ Feature: Award badges based on cohort
|
||||
| user2 | CH3 |
|
||||
| user3 | CH2 |
|
||||
| user3 | CH3 |
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Site Badge |
|
||||
| status | 0 |
|
||||
| description | Site badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Cohort membership"
|
||||
And I set the field "id_cohort_cohorts" to "One Cohort"
|
||||
And I press "Save"
|
||||
@@ -129,13 +135,15 @@ Feature: Award badges based on cohort
|
||||
| user | cohort |
|
||||
| user1 | CH1 |
|
||||
| user2 | CH2 |
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Site Badge |
|
||||
| status | 0 |
|
||||
| description | Site badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Cohort membership"
|
||||
And I set the field "id_cohort_cohorts" to "One Cohort"
|
||||
And I press "Save"
|
||||
@@ -180,13 +188,15 @@ Feature: Award badges based on cohort
|
||||
| user1 | CH1 |
|
||||
| user2 | CH2 |
|
||||
| user3 | CH2 |
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Site Badge |
|
||||
| status | 0 |
|
||||
| description | Site badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Cohort membership"
|
||||
And I set the field "id_cohort_cohorts" to "One Cohort"
|
||||
And I press "Save"
|
||||
@@ -237,13 +247,15 @@ Feature: Award badges based on cohort
|
||||
| user1 | CH2 |
|
||||
| user2 | CH2 |
|
||||
| user2 | CH2 |
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Site Badge |
|
||||
| status | 0 |
|
||||
| description | Site badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Cohort membership"
|
||||
And I set the field "id_cohort_cohorts" to "One Cohort"
|
||||
And I press "Save"
|
||||
@@ -294,13 +306,15 @@ Feature: Award badges based on cohort
|
||||
| user1 | CH2 |
|
||||
| user2 | CH1 |
|
||||
| user3 | CH2 |
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Site Badge |
|
||||
| status | 0 |
|
||||
| description | Site badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Cohort membership"
|
||||
And I expand all fieldsets
|
||||
And I set the field "id_cohort_cohorts" to "One Cohort,Two Cohort"
|
||||
@@ -352,25 +366,23 @@ Feature: Award badges based on cohort
|
||||
| user1 | CH1 |
|
||||
| user1 | CH2 |
|
||||
| user2 | CH2 |
|
||||
And the following "core_badges > Badges" exist:
|
||||
| name | status | description | image |
|
||||
| Site Badge 1 | 0 | Site badge description | badges/tests/behat/badge.png |
|
||||
| Site Badge 2 | 0 | Site badge description | badges/tests/behat/badge.png |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge 1 |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge 1" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Cohort membership"
|
||||
And I set the field "id_cohort_cohorts" to "One Cohort"
|
||||
And I press "Save"
|
||||
And I press "Enable access"
|
||||
When I press "Continue"
|
||||
And I should see "Recipients (1)"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge 2 |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge 2" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Cohort membership"
|
||||
And I set the field "id_cohort_cohorts" to "Two Cohort"
|
||||
And I press "Save"
|
||||
@@ -411,13 +423,14 @@ Feature: Award badges based on cohort
|
||||
| user1 | CH2 |
|
||||
| user2 | CH2 |
|
||||
| user2 | CH3 |
|
||||
And the following "core_badges > Badges" exist:
|
||||
| name | status | description | image |
|
||||
| Site Badge 1 | 0 | Site badge description | badges/tests/behat/badge.png |
|
||||
| Site Badge 2 | 0 | Site badge description | badges/tests/behat/badge.png |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge 1 |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge 1" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Cohort membership"
|
||||
And I expand all fieldsets
|
||||
And I set the field "id_cohort_cohorts" to "One Cohort,Two Cohort"
|
||||
@@ -426,12 +439,9 @@ Feature: Award badges based on cohort
|
||||
And I press "Enable access"
|
||||
When I press "Continue"
|
||||
And I should see "Recipients (1)"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge 2 |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge 2" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Cohort membership"
|
||||
And I expand all fieldsets
|
||||
And I set the field "id_cohort_cohorts" to "Three Cohort,Two Cohort"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@core @core_badges @_file_upload
|
||||
@core @core_badges
|
||||
Feature: Award badges based on competency completion
|
||||
In order to award badges to users based on competency completion
|
||||
As an admin
|
||||
@@ -25,6 +25,13 @@ Feature: Award badges based on competency completion
|
||||
|
||||
@javascript
|
||||
Scenario: Award badge for completing a competency in a course
|
||||
Given the following "core_badges > Badge" exists:
|
||||
| name | Course Badge |
|
||||
| status | 0 |
|
||||
| type | 2 |
|
||||
| course | C1 |
|
||||
| description | Course badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
# Add a competency to the course
|
||||
When I am on "Course 1" course homepage
|
||||
And I navigate to "Competencies > Add competencies to course" in current page administration
|
||||
@@ -39,12 +46,10 @@ Feature: Award badges based on competency completion
|
||||
# Add a badge to the course
|
||||
And I am on "Course 1" course homepage
|
||||
And I change window size to "large"
|
||||
And I navigate to "Badges > Add a new badge" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Course Badge |
|
||||
| Description | Course badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges" in current page administration
|
||||
And I press "Manage badges"
|
||||
And I follow "Course Badge"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
# Set the competency as a criteria for the badge
|
||||
And I set the field "type" to "Competencies"
|
||||
When I open the autocomplete suggestions list
|
||||
@@ -72,6 +77,11 @@ Feature: Award badges based on competency completion
|
||||
|
||||
@javascript
|
||||
Scenario: Award badge for completing a competency in the site
|
||||
Given the following "core_badges > Badge" exists:
|
||||
| name | Site Badge |
|
||||
| status | 0 |
|
||||
| description | Site badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
# Add a competency to the course
|
||||
When I am on "Course 1" course homepage
|
||||
And I navigate to "Competencies > Add competencies to course" in current page administration
|
||||
@@ -83,12 +93,9 @@ Feature: Award badges based on competency completion
|
||||
And I select "comp2" of the competency tree
|
||||
And I click on "Add" "button" in the "Competency picker" "dialogue"
|
||||
# Add a badge to the site
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
# Set the competency as a criteria for the badge
|
||||
And I set the field "type" to "Competencies"
|
||||
And I press "Add competency"
|
||||
@@ -123,6 +130,11 @@ Feature: Award badges based on competency completion
|
||||
|
||||
@javascript
|
||||
Scenario: Award badge for completing all competencies in the site
|
||||
Given the following "core_badges > Badge" exists:
|
||||
| name | Site Badge |
|
||||
| status | 0 |
|
||||
| description | Site badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
# Add a competency to the course
|
||||
When I am on "Course 1" course homepage
|
||||
And I navigate to "Competencies > Add competencies to course" in current page administration
|
||||
@@ -134,12 +146,9 @@ Feature: Award badges based on competency completion
|
||||
And I select "comp2" of the competency tree
|
||||
And I click on "Add" "button" in the "Competency picker" "dialogue"
|
||||
# Add a badge to the site
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
# Set the competency as a criteria for the badge
|
||||
And I set the field "type" to "Competencies"
|
||||
And I press "Add competency"
|
||||
|
||||
@@ -9,13 +9,15 @@ Feature: Award badges based on user profile field
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| user1 | First | User | first@example.com |
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Site Badge |
|
||||
| status | 0 |
|
||||
| description | Site badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge |
|
||||
| Description | Site badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I click on "Edit" "link" in the "Site Badge" "table_row"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Profile completion"
|
||||
And I set the field "id_field_picture" to "1"
|
||||
And I press "Save"
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<?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/>.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Badges test generator for Behat
|
||||
*
|
||||
* @package core_badges
|
||||
* @copyright 2023 Paul Holden <paulh@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_core_badges_generator extends behat_generator_base {
|
||||
|
||||
/**
|
||||
* Get a list of the entities that can be created for this component
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
protected function get_creatable_entities(): array {
|
||||
return [
|
||||
'Badges' => [
|
||||
'singular' => 'Badge',
|
||||
'datagenerator' => 'badge',
|
||||
'required' => [
|
||||
'name',
|
||||
],
|
||||
'switchids' => [
|
||||
'course' => 'courseid',
|
||||
],
|
||||
],
|
||||
'Criterias' => [
|
||||
'singular' => 'Criteria',
|
||||
'datagenerator' => 'criteria',
|
||||
'required' => [
|
||||
'badge',
|
||||
'role',
|
||||
],
|
||||
'switchids' => [
|
||||
'badge' => 'badgeid',
|
||||
'role' => 'roleid',
|
||||
],
|
||||
],
|
||||
'Issued badges' => [
|
||||
'singular' => 'Issued badge',
|
||||
'datagenerator' => 'issued_badge',
|
||||
'required' => [
|
||||
'badge',
|
||||
'user',
|
||||
],
|
||||
'switchids' => [
|
||||
'badge' => 'badgeid',
|
||||
'user' => 'userid',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up badge ID from given name
|
||||
*
|
||||
* @param string $name
|
||||
* @return int
|
||||
*/
|
||||
protected function get_badge_id(string $name): int {
|
||||
global $DB;
|
||||
|
||||
return (int) $DB->get_field('badge', 'id', ['name' => $name], MUST_EXIST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-process badge entity
|
||||
*
|
||||
* @param array $badge
|
||||
* @return array
|
||||
*/
|
||||
protected function preprocess_badge(array $badge): array {
|
||||
global $CFG;
|
||||
|
||||
require_once("{$CFG->libdir}/badgeslib.php");
|
||||
|
||||
// Allow text status' that correspond to badge constants.
|
||||
if (array_key_exists('status', $badge) && !is_numeric($badge['status'])) {
|
||||
$badge['status'] = constant('BADGE_STATUS_' . strtoupper($badge['status']));
|
||||
}
|
||||
|
||||
return $badge;
|
||||
}
|
||||
}
|
||||
@@ -35,12 +35,17 @@ class core_badges_generator extends component_generator_base {
|
||||
/**
|
||||
* Create badge
|
||||
*
|
||||
* TODO: MDL-73648 Use from Behat too
|
||||
*
|
||||
* @param array|stdClass $record
|
||||
* @return badge
|
||||
*/
|
||||
public function create_badge($record): badge {
|
||||
global $DB, $USER;
|
||||
global $CFG, $DB, $USER;
|
||||
|
||||
$record = (array) $record;
|
||||
|
||||
// Save badge image path for later.
|
||||
$badgeimage = $record['image'] ?? '';
|
||||
unset($record['image']);
|
||||
|
||||
$record = (object) array_merge([
|
||||
'name' => 'Test badge',
|
||||
@@ -67,10 +72,87 @@ class core_badges_generator extends component_generator_base {
|
||||
'imageauthoremail' => 'author@example.com',
|
||||
'imageauthorurl' => 'http://image.example.com/',
|
||||
'imagecaption' => 'Image caption'
|
||||
], (array) $record);
|
||||
], $record);
|
||||
|
||||
$record->id = $DB->insert_record('badge', $record);
|
||||
$badge = new badge($record->id);
|
||||
|
||||
return new badge($record->id);
|
||||
// Process badge image (if supplied).
|
||||
if ($badgeimage !== '') {
|
||||
$file = get_file_storage()->create_file_from_pathname([
|
||||
'contextid' => context_user::instance($USER->id)->id,
|
||||
'userid' => $USER->id,
|
||||
'component' => 'user',
|
||||
'filearea' => 'draft',
|
||||
'itemid' => file_get_unused_draft_itemid(),
|
||||
'filepath' => '/',
|
||||
'filename' => basename($badgeimage),
|
||||
], "{$CFG->dirroot}/$badgeimage");
|
||||
|
||||
// Copy image to temp file, as it'll be deleted by the following call.
|
||||
badges_process_badge_image($badge, $file->copy_content_to_temp());
|
||||
}
|
||||
|
||||
return $badge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create badge criteria
|
||||
*
|
||||
* Note that only manual criteria issues by role is currently supported
|
||||
*
|
||||
* @param array|stdClass $record
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function create_criteria($record): void {
|
||||
$record = (array) $record;
|
||||
|
||||
if (!array_key_exists('badgeid', $record)) {
|
||||
throw new coding_exception('Record must contain \'badgeid\' property');
|
||||
}
|
||||
if (!array_key_exists('roleid', $record)) {
|
||||
throw new coding_exception('Record must contain \'roleid\' property');
|
||||
}
|
||||
|
||||
$badge = new badge($record['badgeid']);
|
||||
|
||||
// Create the overall criteria.
|
||||
if (count($badge->criteria) === 0) {
|
||||
award_criteria::build([
|
||||
'badgeid' => $badge->id,
|
||||
'criteriatype' => BADGE_CRITERIA_TYPE_OVERALL,
|
||||
])->save([
|
||||
'agg' => BADGE_CRITERIA_AGGREGATION_ALL,
|
||||
]);
|
||||
}
|
||||
|
||||
// Create the manual criteria.
|
||||
award_criteria::build([
|
||||
'badgeid' => $badge->id,
|
||||
'criteriatype' => BADGE_CRITERIA_TYPE_MANUAL,
|
||||
])->save([
|
||||
'role_' . $record['roleid'] => $record['roleid'],
|
||||
'description' => $record['description'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create issued badge to a user
|
||||
*
|
||||
* @param array|stdClass $record
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function create_issued_badge($record): void {
|
||||
$record = (array) $record;
|
||||
|
||||
if (!array_key_exists('badgeid', $record)) {
|
||||
throw new coding_exception('Record must contain \'badgeid\' property');
|
||||
}
|
||||
if (!array_key_exists('userid', $record)) {
|
||||
throw new coding_exception('Record must contain \'userid\' property');
|
||||
}
|
||||
|
||||
$badge = new badge($record['badgeid']);
|
||||
$badge->issue($record['userid'], true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,29 +8,29 @@ Feature: Add the glossary random block when main feature is disabled
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | topics |
|
||||
And I am on the "C1" "course" page logged in as "admin"
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| glossary_random | Course | C1 | course-view-* | site-post |
|
||||
|
||||
Scenario: The glossary random block is displayed even when glossary module is disabled
|
||||
Given I turn editing mode on
|
||||
And I add the "Random glossary entry" block
|
||||
When I navigate to "Plugins > Activity modules > Manage activities" in site administration
|
||||
When I log in as "admin"
|
||||
And I navigate to "Plugins > Activity modules > Manage activities" in site administration
|
||||
And I click on "Hide" "icon" in the "Glossary" "table_row"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
Then I should see "Random glossary entry"
|
||||
Then "Random glossary entry" "block" should exist
|
||||
|
||||
Scenario: The glossary random block can be removed even when glossary module is disabled
|
||||
Given I turn editing mode on
|
||||
And I add the "Random glossary entry" block
|
||||
When I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I open the "Random glossary entry" blocks action menu
|
||||
And I click on "Delete Random glossary entry block" "link" in the "Random glossary entry" "block"
|
||||
And "Delete block?" "dialogue" should exist
|
||||
And I click on "Cancel" "button" in the "Delete block?" "dialogue"
|
||||
And I should see "Random glossary entry"
|
||||
And "Random glossary entry" "block" should exist
|
||||
When I navigate to "Plugins > Activity modules > Manage activities" in site administration
|
||||
And I click on "Hide" "icon" in the "Glossary" "table_row"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I open the "Random glossary entry" blocks action menu
|
||||
And I click on "Delete Random glossary entry block" "link" in the "Random glossary entry" "block"
|
||||
And "Delete block?" "dialogue" should exist
|
||||
And I click on "Delete" "button" in the "Delete block?" "dialogue"
|
||||
Then I should not see "Random glossary entry"
|
||||
Then "Random glossary entry" "block" should not exist
|
||||
|
||||
Vendored
+16
-6
@@ -39,7 +39,7 @@ require_once($CFG->dirroot.'/cache/locallib.php');
|
||||
* @copyright 2012 Sam Hemelryk
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class cache_disabled extends cache {
|
||||
class cache_disabled extends cache implements cache_loader_with_locking {
|
||||
|
||||
/**
|
||||
* Constructs the cache.
|
||||
@@ -204,20 +204,30 @@ class cache_disabled extends cache {
|
||||
/**
|
||||
* Pretend that we got a lock to avoid errors.
|
||||
*
|
||||
* @param string $key
|
||||
* @param int|string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function acquire_lock(string $key) : bool {
|
||||
public function acquire_lock($key) : bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretend that we released a lock to avoid errors.
|
||||
*
|
||||
* @param string $key
|
||||
* @return void
|
||||
* @param int|string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function release_lock(string $key) : bool {
|
||||
public function release_lock($key) : bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretend that we have a lock to avoid errors.
|
||||
*
|
||||
* @param int|string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function check_lock_state($key) : bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+49
-2
@@ -119,6 +119,15 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
*/
|
||||
protected $lastiobytes = 0;
|
||||
|
||||
/** @var int Maximum number of seconds to wait for a lock before giving up. */
|
||||
protected $lockwait = 60;
|
||||
|
||||
/** @var int Timeout before lock is automatically released (in case of crashes) */
|
||||
protected $locktimeout = 600;
|
||||
|
||||
/** @var ?array Array of current locks, or null if we haven't registered shutdown function */
|
||||
protected $currentlocks = null;
|
||||
|
||||
/**
|
||||
* Determines if the requirements for this type of store are met.
|
||||
*
|
||||
@@ -182,6 +191,12 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
}
|
||||
$password = !empty($configuration['password']) ? $configuration['password'] : '';
|
||||
$prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
|
||||
if (array_key_exists('lockwait', $configuration)) {
|
||||
$this->lockwait = (int)$configuration['lockwait'];
|
||||
}
|
||||
if (array_key_exists('locktimeout', $configuration)) {
|
||||
$this->locktimeout = (int)$configuration['locktimeout'];
|
||||
}
|
||||
$this->redis = $this->new_redis($configuration['server'], $prefix, $password);
|
||||
}
|
||||
|
||||
@@ -527,7 +542,38 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
* @return bool True if the lock was acquired, false if it was not.
|
||||
*/
|
||||
public function acquire_lock($key, $ownerid) {
|
||||
return $this->redis->setnx($key, $ownerid);
|
||||
$timelimit = time() + $this->lockwait;
|
||||
do {
|
||||
// If the key doesn't already exist, grab it and return true.
|
||||
if ($this->redis->setnx($key, $ownerid)) {
|
||||
// Ensure Redis deletes the key after a bit in case something goes wrong.
|
||||
$this->redis->expire($key, $this->locktimeout);
|
||||
// If we haven't got it already, better register a shutdown function.
|
||||
if ($this->currentlocks === null) {
|
||||
core_shutdown_manager::register_function([$this, 'shutdown_release_locks']);
|
||||
$this->currentlocks = [];
|
||||
}
|
||||
$this->currentlocks[$key] = $ownerid;
|
||||
return true;
|
||||
}
|
||||
// Wait 1 second then retry.
|
||||
sleep(1);
|
||||
} while (time() < $timelimit);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases any locks when the system shuts down, in case there is a crash or somebody forgets
|
||||
* to use 'try-finally'.
|
||||
*
|
||||
* Do not call this function manually (except from unit test).
|
||||
*/
|
||||
public function shutdown_release_locks() {
|
||||
foreach ($this->currentlocks as $key => $ownerid) {
|
||||
debugging('Automatically releasing Redis cache lock: ' . $key . ' (' . $ownerid .
|
||||
') - did somebody forget to call release_lock()?', DEBUG_DEVELOPER);
|
||||
$this->release_lock($key, $ownerid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -541,7 +587,7 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
*/
|
||||
public function check_lock_state($key, $ownerid) {
|
||||
$result = $this->redis->get($key);
|
||||
if ($result === $ownerid) {
|
||||
if ($result === (string)$ownerid) {
|
||||
return true;
|
||||
}
|
||||
if ($result === false) {
|
||||
@@ -586,6 +632,7 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
*/
|
||||
public function release_lock($key, $ownerid) {
|
||||
if ($this->check_lock_state($key, $ownerid)) {
|
||||
unset($this->currentlocks[$key]);
|
||||
return ($this->redis->del($key) !== false);
|
||||
}
|
||||
return false;
|
||||
|
||||
+52
@@ -141,6 +141,58 @@ class store_test extends \cachestore_tests {
|
||||
$this->assertTrue($store->release_lock('lock', '123'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the timeout features of locking.
|
||||
*/
|
||||
public function test_lock_timeouts(): void {
|
||||
$store = $this->create_cachestore_redis(['lockwait' => 2, 'locktimeout' => 4]);
|
||||
|
||||
// User 123 acquires lock.
|
||||
$this->assertTrue($store->acquire_lock('lock', '123'));
|
||||
$this->assertTrue($store->check_lock_state('lock', '123'));
|
||||
|
||||
// User 456 tries to acquire lock - should fail after about 2 seconds.
|
||||
$before = microtime(true);
|
||||
$this->assertFalse($store->acquire_lock('lock', '456'));
|
||||
$after = microtime(true);
|
||||
$this->assertEqualsWithDelta(2, $after - $before, 0.5);
|
||||
|
||||
// Wait another 2 seconds and then it should be able to get the lock because of timeout.
|
||||
sleep(2);
|
||||
$this->assertTrue($store->acquire_lock('lock', '456'));
|
||||
$this->assertTrue($store->check_lock_state('lock', '456'));
|
||||
|
||||
// The first user doesn't have the lock any more.
|
||||
$this->assertFalse($store->check_lock_state('lock', '123'));
|
||||
|
||||
// Releasing the lock from the first user does nothing.
|
||||
$this->assertFalse($store->release_lock('lock', '123'));
|
||||
$this->assertTrue($store->check_lock_state('lock', '456'));
|
||||
|
||||
$this->assertTrue($store->release_lock('lock', '456'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the shutdown function that is supposed to free any remaining locks.
|
||||
*/
|
||||
public function test_lock_shutdown(): void {
|
||||
$store = $this->create_cachestore_redis();
|
||||
try {
|
||||
$this->assertTrue($store->acquire_lock('a', '123'));
|
||||
$this->assertTrue($store->acquire_lock('b', '123'));
|
||||
$this->assertTrue($store->acquire_lock('c', '123'));
|
||||
$this->assertTrue($store->check_lock_state('a', '123'));
|
||||
$this->assertTrue($store->check_lock_state('b', '123'));
|
||||
$this->assertTrue($store->check_lock_state('c', '123'));
|
||||
} finally {
|
||||
$store->shutdown_release_locks();
|
||||
$this->assertDebuggingCalledCount(3);
|
||||
}
|
||||
$this->assertNull($store->check_lock_state('a', '123'));
|
||||
$this->assertNull($store->check_lock_state('b', '123'));
|
||||
$this->assertNull($store->check_lock_state('c', '123'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the get_last_io_bytes function when not using compression (just returns unknown).
|
||||
*/
|
||||
|
||||
Vendored
+9
@@ -1426,6 +1426,15 @@ class cache_test extends \advanced_testcase {
|
||||
$this->assertFalse($cache->set_versioned('v', 1, 'data'));
|
||||
$this->assertFalse($cache->delete('test'));
|
||||
$this->assertTrue($cache->purge());
|
||||
// Checking a lock should always report that we have one.
|
||||
// Acquiring or releasing a lock should always report success.
|
||||
$this->assertTrue($cache->check_lock_state('test'));
|
||||
$this->assertTrue($cache->acquire_lock('test'));
|
||||
$this->assertTrue($cache->acquire_lock('test'));
|
||||
$this->assertTrue($cache->check_lock_state('test'));
|
||||
$this->assertTrue($cache->release_lock('test'));
|
||||
$this->assertTrue($cache->release_lock('test'));
|
||||
$this->assertTrue($cache->check_lock_state('test'));
|
||||
|
||||
// Test a session cache.
|
||||
$cache = cache::make_from_params(cache_store::MODE_SESSION, 'phpunit', 'disable');
|
||||
|
||||
+1
-1
@@ -6,6 +6,6 @@ define("core_calendar/manage_subscriptions",["exports","core_calendar/selectors"
|
||||
* @copyright 2021 Huong Nguyen <huongnv13@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since 4.0
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,CalendarSelectors=_interopRequireWildcard(CalendarSelectors),CalendarRepository=_interopRequireWildcard(CalendarRepository),Modal=_interopRequireWildcard(Modal),ModalEvents=_interopRequireWildcard(ModalEvents),_prefetch=(obj=_prefetch)&&obj.__esModule?obj:{default:obj};const getSubscriptionName=element=>element.closest("tr").dataset.subname,registerEventListeners=()=>{document.addEventListener("click",(e=>{const deleteAction=e.target.closest(CalendarSelectors.actions.deleteSubscription);if(deleteAction){e.preventDefault();((element,messageCode)=>{const subscriptionName=getSubscriptionName(element);return Modal.create({type:Modal.types.SAVE_CANCEL,title:(0,_str.get_string)("confirmation","admin"),body:(0,_str.get_string)(messageCode,"calendar",subscriptionName),buttons:{save:(0,_str.get_string)("yes")}}).then((modal=>(modal.getRoot().on(ModalEvents.hidden,(()=>{element.focus()})),modal.show(),modal)))})(deleteAction,"confirmsubscriptiondelete").then((modal=>(modal.getRoot().on(ModalEvents.save,(()=>{const subscriptionId=parseInt(deleteAction.closest("tr").dataset.subid);CalendarRepository.deleteSubscription(subscriptionId).then((data=>{const response=(async(element,data)=>{const subscriptionName=getSubscriptionName(element),message=data.status?await(0,_str.get_string)("subscriptionremoved","calendar",subscriptionName):data.warnings[0].message,type=data.status?"info":"error";return(0,_notification.addNotification)({message:message,type:type})})(deleteAction,data);return response.then((()=>{const subscriptionRow=(subscriptionId=>document.querySelector('tr[data-subid="'.concat(subscriptionId,'"]')))(subscriptionId);return subscriptionRow.remove()}))})).catch(_notification.displayException)})),modal))).catch(_notification.displayException)}})),document.addEventListener(_events.eventTypes.elementUpdated,(e=>{"core_calendar"==e.target.getAttribute("data-component")&&(0,_notification.fetchNotifications)()}))};_exports.init=()=>{_prefetch.default.prefetchStrings("moodle",["yes"]),_prefetch.default.prefetchStrings("core_admin",["confirmation"]),_prefetch.default.prefetchStrings("core_calendar",["confirmsubscriptiondelete","subscriptionremoved"]),registerEventListeners()}}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,CalendarSelectors=_interopRequireWildcard(CalendarSelectors),CalendarRepository=_interopRequireWildcard(CalendarRepository),Modal=_interopRequireWildcard(Modal),ModalEvents=_interopRequireWildcard(ModalEvents),_prefetch=(obj=_prefetch)&&obj.__esModule?obj:{default:obj};const getSubscriptionName=element=>element.closest("tr").dataset.subname,registerEventListeners=()=>{document.addEventListener("click",(e=>{const deleteAction=e.target.closest(CalendarSelectors.actions.deleteSubscription);if(deleteAction){e.preventDefault();((element,messageCode)=>{const subscriptionName=getSubscriptionName(element);return Modal.create({type:Modal.types.SAVE_CANCEL,title:(0,_str.get_string)("confirmation","admin"),body:(0,_str.get_string)(messageCode,"calendar",subscriptionName),buttons:{save:(0,_str.get_string)("yes")}}).then((modal=>(modal.getRoot().on(ModalEvents.hidden,(()=>{element.focus()})),modal.show(),modal)))})(deleteAction,"confirmsubscriptiondelete").then((modal=>(modal.getRoot().on(ModalEvents.save,(()=>{const subscriptionId=parseInt(deleteAction.closest("tr").dataset.subid);CalendarRepository.deleteSubscription(subscriptionId).then((data=>{const response=(async(element,data)=>{const subscriptionName=getSubscriptionName(element),message=data.status?await(0,_str.get_string)("subscriptionremoved","calendar",subscriptionName):data.warnings[0].message,type=data.status?"info":"error";return(0,_notification.addNotification)({message:message,type:type})})(deleteAction,data);return response.then((()=>{const subscriptionRow=(subscriptionId=>document.querySelector('tr[data-subid="'.concat(subscriptionId,'"]')))(subscriptionId);return subscriptionRow.remove()}))})).catch(_notification.exception)})),modal))).catch(_notification.exception)}})),document.addEventListener(_events.eventTypes.elementUpdated,(e=>{"core_calendar"==e.target.getAttribute("data-component")&&(0,_notification.fetchNotifications)()}))};_exports.init=()=>{_prefetch.default.prefetchStrings("moodle",["yes"]),_prefetch.default.prefetchStrings("core_admin",["confirmation"]),_prefetch.default.prefetchStrings("core_calendar",["confirmsubscriptiondelete","subscriptionremoved"]),registerEventListeners()}}));
|
||||
|
||||
//# sourceMappingURL=manage_subscriptions.min.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -26,7 +26,7 @@ import * as CalendarSelectors from 'core_calendar/selectors';
|
||||
import * as CalendarRepository from 'core_calendar/repository';
|
||||
import * as Modal from 'core/modal_factory';
|
||||
import * as ModalEvents from 'core/modal_events';
|
||||
import {displayException, addNotification, fetchNotifications} from 'core/notification';
|
||||
import {exception as displayException, addNotification, fetchNotifications} from 'core/notification';
|
||||
import Prefetch from 'core/prefetch';
|
||||
import {get_string as getString} from 'core/str';
|
||||
import {eventTypes} from 'core/local/inplace_editable/events';
|
||||
|
||||
@@ -126,7 +126,9 @@ class progress_test extends \advanced_testcase {
|
||||
* @covers \core_completion\progress::get_course_progress_percentage.
|
||||
*/
|
||||
public function test_course_progress_percentage_completion_state() {
|
||||
global $DB;
|
||||
global $DB, $CFG;
|
||||
|
||||
require_once("{$CFG->dirroot}/completion/criteria/completion_criteria_activity.php");
|
||||
|
||||
// Add a course that supports completion.
|
||||
$course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -65,8 +65,10 @@ const registerListenerEvents = (contentBank) => {
|
||||
});
|
||||
|
||||
const heading = fileArea.querySelector(selectors.elements.heading);
|
||||
heading.removeAttribute('role');
|
||||
heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.removeAttribute('role'));
|
||||
if (heading) {
|
||||
heading.removeAttribute('role');
|
||||
heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.removeAttribute('role'));
|
||||
}
|
||||
}
|
||||
viewGrid.classList.add('active');
|
||||
viewList.classList.remove('active');
|
||||
@@ -87,8 +89,10 @@ const registerListenerEvents = (contentBank) => {
|
||||
});
|
||||
|
||||
const heading = fileArea.querySelector(selectors.elements.heading);
|
||||
heading.setAttribute('role', 'row');
|
||||
heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.setAttribute('role', 'columnheader'));
|
||||
if (heading) {
|
||||
heading.setAttribute('role', 'row');
|
||||
heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.setAttribute('role', 'columnheader'));
|
||||
}
|
||||
}
|
||||
viewList.classList.add('active');
|
||||
viewGrid.classList.remove('active');
|
||||
|
||||
@@ -132,8 +132,9 @@ class bankcontent implements renderable, templatable {
|
||||
}
|
||||
$options = [];
|
||||
foreach ($this->allowedcategories as $allowedcategory) {
|
||||
$options[$allowedcategory->ctxid] = format_string($allowedcategory->name, true,
|
||||
context_coursecat::instance($allowedcategory->ctxinstance));
|
||||
$options[$allowedcategory->ctxid] = format_string($allowedcategory->name, true, [
|
||||
'context' => context_coursecat::instance($allowedcategory->ctxinstance),
|
||||
]);
|
||||
}
|
||||
if (!empty($options)) {
|
||||
$allowedcontexts['categories'] = [get_string('coursecategories') => $options];
|
||||
|
||||
@@ -900,7 +900,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
|
||||
// Trigger a purge for all caches listening for changes to category enrolment.
|
||||
cache_helper::purge_by_event('changesincategoryenrolment');
|
||||
|
||||
if (!$CFG->coursecontact || !in_array($roleid, explode(',', $CFG->coursecontact))) {
|
||||
if (empty($CFG->coursecontact) || !in_array($roleid, explode(',', $CFG->coursecontact))) {
|
||||
// The role is not one of course contact roles.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
}
|
||||
}
|
||||
}}
|
||||
<div id="{{uniqid}}-course-format">
|
||||
<div id="course-format-{{uniqid}}">
|
||||
<h2 class="accesshide">{{{title}}}</h2>
|
||||
{{{completionhelp}}}
|
||||
<ul class="{{format}}" data-for="course_sectionlist">
|
||||
@@ -202,6 +202,6 @@
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['core_courseformat/local/content'], function(component) {
|
||||
component.init('{{uniqid}}-course-format', {}, {{sectionreturn}});
|
||||
component.init('course-format-{{uniqid}}', {}, {{sectionreturn}});
|
||||
});
|
||||
{{/js}}
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
{{/activityinfo}}
|
||||
{{#altcontent}}
|
||||
<div class="activity-altcontent {{#hasname}}course-description-item small{{/hasname}}{{^hasname}}contentwithoutlink{{/hasname}} d-flex">
|
||||
<div class="flex-fill description-inner">
|
||||
<div class="flex-fill description-inner text-break">
|
||||
{{{altcontent}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
{{^accessvisible}}dimmed{{/accessvisible}}
|
||||
{{#url}} d-flex {{/url}} {{^url}} d-flex-noedit {{/url}}
|
||||
{{#indent}} indented {{/indent}}"
|
||||
id="{{uniqid}}-course-index-cm-{{id}}"
|
||||
id="course-index-cm-{{id}}"
|
||||
data-for="cm"
|
||||
data-id="{{id}}"
|
||||
role="treeitem"
|
||||
@@ -68,6 +68,6 @@
|
||||
</li>
|
||||
{{#js}}
|
||||
require(['core_courseformat/local/courseindex/cm'], function(component) {
|
||||
component.init('{{uniqid}}-course-index-cm-{{id}}');
|
||||
component.init('course-index-cm-{{id}}');
|
||||
});
|
||||
{{/js}}
|
||||
|
||||
@@ -99,13 +99,13 @@
|
||||
}
|
||||
|
||||
}}
|
||||
<div id="{{uniqid}}-course-index" class="courseindex {{#editmode}} editing {{/editmode}}" role="tree">
|
||||
<div id="course-index" class="courseindex {{#editmode}} editing {{/editmode}}" role="tree">
|
||||
{{#sections}}
|
||||
{{> core_courseformat/local/courseindex/section }}
|
||||
{{/sections}}
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['core_courseformat/local/courseindex/courseindex'], function(component) {
|
||||
component.init('{{uniqid}}-course-index');
|
||||
component.init('course-index');
|
||||
});
|
||||
{{/js}}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<div data-region="loading-placeholder-content" aria-hidden="true" id="{{uniqid}}-course-index-placeholder">
|
||||
<div data-region="loading-placeholder-content" aria-hidden="true" id="course-index-placeholder">
|
||||
<ul class="media-list">
|
||||
<li class="media">
|
||||
<div class="media-body col-md-6 p-0 d-flex align-items-center">
|
||||
@@ -52,6 +52,6 @@
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['core_courseformat/local/courseindex/placeholder'], function(component) {
|
||||
component.init('{{uniqid}}-course-index-placeholder');
|
||||
component.init('course-index-placeholder');
|
||||
});
|
||||
{{/js}}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
}}
|
||||
<div
|
||||
class="courseindex-section {{#current}}current{{/current}}"
|
||||
id="{{uniqid}}-course-index-section-{{id}}"
|
||||
id="course-index-section-{{id}}"
|
||||
data-for="section"
|
||||
data-id="{{id}}"
|
||||
data-number="{{number}}"
|
||||
@@ -119,6 +119,6 @@
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['core_courseformat/local/courseindex/section'], function(component) {
|
||||
component.init('{{uniqid}}-course-index-section-{{id}}');
|
||||
component.init('course-index-section-{{id}}');
|
||||
});
|
||||
{{/js}}
|
||||
|
||||
+1
-1
@@ -1218,7 +1218,7 @@ class core_course_renderer extends plugin_renderer_base {
|
||||
html_writer::tag('span', $file->get_filename(), ['class' => 'fp-filename']);
|
||||
$contentfiles .= html_writer::tag('span',
|
||||
html_writer::link($url, $filename),
|
||||
['class' => 'coursefile fp-filename-icon']);
|
||||
['class' => 'coursefile fp-filename-icon text-break']);
|
||||
}
|
||||
}
|
||||
return $contentimages . $contentfiles;
|
||||
|
||||
@@ -25,15 +25,21 @@
|
||||
"name": "assign",
|
||||
"title": "Assignment",
|
||||
"link": "http://yourmoodle/modedit.php?id=x&itemtype=y",
|
||||
"icon": "<img class='icon' src='http://urltooptionicon'>",
|
||||
"icon": "<img class='icon' src='http://urltooptionicon' alt='Icon'>",
|
||||
"help": "This is a description of the assignment activity",
|
||||
"archetype": 0,
|
||||
"componentname": "mod_assign",
|
||||
"favourite": 1
|
||||
}
|
||||
}}
|
||||
<div class="optionsummary" tabindex="-1" data-region="chooser-option-summary-container" aria-labelledby="optionsummary_label" aria-describedby="optionsumary_desc">
|
||||
<div class="content text-left mb-5 p-2 px-sm-5 py-sm-4" data-region="chooser-option-summary-content-container">
|
||||
<div
|
||||
class="optionsummary d-flex flex-column rounded-bottom"
|
||||
tabindex="-1"
|
||||
data-region="chooser-option-summary-container"
|
||||
aria-labelledby="optionsummary_label-{{uniqid}}"
|
||||
aria-describedby="optionsumary_desc-{{uniqid}}"
|
||||
>
|
||||
<div class="content flex-grow-1 text-left p-2 px-sm-5 py-sm-4" data-region="chooser-option-summary-content-container">
|
||||
<div class="heading mb-4">
|
||||
<h5 id="optionsummary_label-{{uniqid}}" data-region="summary-header" tabindex="0">
|
||||
{{{icon}}}
|
||||
@@ -45,7 +51,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{{^showFooter}}
|
||||
<div class="fixed-bottom position-absolute py-3 px-4 border-top modal-footer">
|
||||
<div class="modal-footer">
|
||||
{{>core_course/local/activitychooser/footer_partial}}
|
||||
</div>
|
||||
{{/showFooter}}
|
||||
|
||||
@@ -162,10 +162,18 @@ class user_repository {
|
||||
"to user '{$ltiuser->userid}' and can't be associated with another user '{$userrecord->id}'.");
|
||||
}
|
||||
|
||||
$userrecord->timemodified = $timenow;
|
||||
$ltiuserrecord->timemodified = $timenow;
|
||||
\user_update_user($userrecord);
|
||||
// Only update the Moodle user record if something has changed.
|
||||
$rawuser = \core_user::get_user($userrecord->id);
|
||||
$userfieldstocompare = array_intersect_key(
|
||||
(array) $rawuser,
|
||||
(array) $userrecord
|
||||
);
|
||||
if (!empty(array_diff((array) $userrecord, $userfieldstocompare))) {
|
||||
\user_update_user($userrecord);
|
||||
}
|
||||
unset($userrecord->id);
|
||||
|
||||
$ltiuserrecord->timemodified = $timenow;
|
||||
$DB->update_record($this->ltiuserstable, $ltiuserrecord);
|
||||
} else {
|
||||
// Validate uniqueness of the lti user, in the case of a stale object coming in to be saved.
|
||||
@@ -173,8 +181,17 @@ class user_repository {
|
||||
throw new \coding_exception("Cannot create duplicate LTI user '{$user->get_localid()}' for resource " .
|
||||
"'{$user->get_resourceid()}'.");
|
||||
}
|
||||
|
||||
// Only update the Moodle user record if something has changed.
|
||||
$userid = $userrecord->id;
|
||||
\user_update_user($userrecord);
|
||||
$rawuser = \core_user::get_user($userid);
|
||||
$userfieldstocompare = array_intersect_key(
|
||||
(array) $rawuser,
|
||||
(array) $userrecord
|
||||
);
|
||||
if (!empty(array_diff((array) $userrecord, $userfieldstocompare))) {
|
||||
\user_update_user($userrecord);
|
||||
}
|
||||
unset($userrecord->id);
|
||||
|
||||
// Create the lti_user record, holding details that have a lifespan equal to that of the enrolment instance.
|
||||
@@ -218,7 +235,8 @@ class user_repository {
|
||||
FROM {{$this->ltiuserstable}} lu
|
||||
JOIN {user} u
|
||||
ON (u.id = lu.userid)
|
||||
WHERE lu.id = :id";
|
||||
WHERE lu.id = :id
|
||||
AND lu.ltideploymentid IS NOT NULL";
|
||||
|
||||
$record = $DB->get_record_sql($sql, ['id' => $id], MUST_EXIST);
|
||||
return $this->user_from_record($record);
|
||||
@@ -245,7 +263,8 @@ class user_repository {
|
||||
JOIN {user} u
|
||||
ON (u.id = lu.userid)
|
||||
WHERE lu.userid = :userid
|
||||
AND lu.toolid = :resourceid";
|
||||
AND lu.toolid = :resourceid
|
||||
AND lu.ltideploymentid IS NOT NULL";
|
||||
|
||||
$params = ['userid' => $userid, 'resourceid' => $resourceid];
|
||||
$record = $DB->get_record_sql($sql, $params, MUST_EXIST);
|
||||
@@ -270,6 +289,7 @@ class user_repository {
|
||||
JOIN {user} u
|
||||
ON (u.id = lu.userid)
|
||||
WHERE lu.toolid = :resourceid
|
||||
AND lu.ltideploymentid IS NOT NULL
|
||||
ORDER BY lu.lastaccess DESC";
|
||||
|
||||
$records = $DB->get_records_sql($sql, ['resourceid' => $resourceid]);
|
||||
|
||||
@@ -148,6 +148,11 @@ class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {
|
||||
// Sign using the key
|
||||
$ok = openssl_sign($base_string, $signature, $privatekeyid);
|
||||
|
||||
// Avoid passing null values to base64_encode.
|
||||
if (!$ok) {
|
||||
throw new OAuthException("OpenSSL unable to sign data");
|
||||
}
|
||||
|
||||
// TODO: Remove this block once PHP 8.0 becomes required.
|
||||
if (PHP_MAJOR_VERSION < 8) {
|
||||
// Release the key resource
|
||||
|
||||
@@ -6,3 +6,4 @@ In future releases we should look into using a supported library.
|
||||
2022-01-05 - MDL-73502 - Removed get_magic_quotes_gpc() use, was returning false since ages ago.
|
||||
2022-01-20 - MDL-73523 - Conditional openssl_free_key() use, deprecated by PHP 8.0
|
||||
2022-03-05 - MDL-73520 - replace deprecated php_errormsg with error_get_last(), deprecated by PHP 8.0
|
||||
2023-05-03 - MDL-77840 - Throw exception on openssl_sign to avoid null reaching base64_encode, deprecated by PHP 8.1
|
||||
|
||||
@@ -54,6 +54,7 @@ $string['enrolenddate'] = 'End date';
|
||||
$string['enrolenddate_help'] = 'If enabled, users can access until this date only.';
|
||||
$string['enrolenddateerror'] = 'Enrolment end date cannot be earlier than start date';
|
||||
$string['enrolisdisabled'] = 'The \'Publish as LTI tool\' plugin is disabled.';
|
||||
$string['enrolltiversionincorrect'] = 'The resource is not set up for use over legacy LTI (versions 1.1/2.0). Please contact the administrator of this tool.';
|
||||
$string['enrolperiod'] = 'Enrolment duration';
|
||||
$string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user enrols themselves from the remote system. If disabled, the enrolment duration will be unlimited.';
|
||||
$string['enrolmentfinished'] = 'Enrolment finished.';
|
||||
|
||||
@@ -42,7 +42,7 @@ use enrol_lti\local\ltiadvantage\repository\user_repository;
|
||||
use enrol_lti\local\ltiadvantage\service\application_registration_service;
|
||||
|
||||
require_once(__DIR__."/../../config.php");
|
||||
global $OUTPUT, $PAGE, $CFG;
|
||||
global $OUTPUT, $PAGE, $CFG, $SITE;
|
||||
require_once($CFG->libdir . '/filelib.php');
|
||||
|
||||
$PAGE->set_context(context_system::instance());
|
||||
@@ -113,9 +113,9 @@ $regrequest = (object) [
|
||||
$CFG->wwwroot . '/enrol/lti/launch_deeplink.php',
|
||||
],
|
||||
// TODO: Consider whether to support client_name#ja syntax for multi language support - see MDL-73109.
|
||||
'client_name' => get_string('moodle', 'enrol_lti'),
|
||||
'client_name' => format_string($SITE->fullname, true, ['context' => context_system::instance()]),
|
||||
'jwks_uri' => $CFG->wwwroot . '/enrol/lti/jwks.php',
|
||||
'logo_uri' => $OUTPUT->image_url('moodlelogo')->out(false),
|
||||
'logo_uri' => $OUTPUT->get_compact_logo_url() ? $OUTPUT->get_compact_logo_url()->out(false) : '',
|
||||
'token_endpoint_auth_method' => 'private_key_jwt',
|
||||
'scope' => implode(" ", $scopes),
|
||||
'https://purl.imsglobal.org/spec/lti-tool-configuration' => [
|
||||
|
||||
@@ -31,9 +31,10 @@ class user_repository_test extends \advanced_testcase {
|
||||
* Helper to generate a new user instance.
|
||||
*
|
||||
* @param int $mockresourceid used to spoof a published resource, to which this user is associated.
|
||||
* @param array $userfields user information like city, timezone which would normally come from the tool configuration.
|
||||
* @return user a user instance
|
||||
*/
|
||||
protected function generate_user(int $mockresourceid = 1): user {
|
||||
protected function generate_user(int $mockresourceid = 1, array $userfields = []): user {
|
||||
global $CFG;
|
||||
$registration = application_registration::create(
|
||||
'Test',
|
||||
@@ -63,16 +64,35 @@ class user_repository_test extends \advanced_testcase {
|
||||
$savedcontext->get_id());
|
||||
$savedresourcelink = $resourcelinkrepo->save($resourcelink);
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
// Create a user using the DB defaults to simulate what would have occurred during an auth_lti user auth.
|
||||
$user = $this->getDataGenerator()->create_user([
|
||||
'city' => '',
|
||||
'country' => '',
|
||||
'institution' => '',
|
||||
'timezone' => '99',
|
||||
'maildisplay' => 2,
|
||||
'lang' => 'en'
|
||||
]);
|
||||
|
||||
$userdefaultvalues = [
|
||||
'lang' => $CFG->lang,
|
||||
'city' => '',
|
||||
'country' => '',
|
||||
'institution' => '',
|
||||
'timezone' => '99',
|
||||
'maildisplay' => 2
|
||||
];
|
||||
if (empty($userfields)) {
|
||||
// If userfields is omitted, assume the tool default configuration values (as if 'User default values' are unchanged).
|
||||
$userfields = $userdefaultvalues;
|
||||
} else {
|
||||
// If they have been provided, merge and override the defaults.
|
||||
$userfields = array_merge($userdefaultvalues, $userfields);
|
||||
}
|
||||
$ltiuser = $savedresourcelink->add_user(
|
||||
$user->id,
|
||||
'source-id-123',
|
||||
$CFG->lang,
|
||||
'Perth',
|
||||
'AU',
|
||||
'An Example Institution',
|
||||
'99',
|
||||
2,
|
||||
...array_values($userfields)
|
||||
);
|
||||
|
||||
$ltiuser->set_lastgrade(67.33333333);
|
||||
@@ -141,19 +161,45 @@ class user_repository_test extends \advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests adding a user to the store.
|
||||
* Tests adding a user to the store, assuming that the user has been created using the default 'user default values'.
|
||||
*
|
||||
* @covers ::save
|
||||
*/
|
||||
public function test_save_new() {
|
||||
public function test_save_new_unchanged_user_defaults() {
|
||||
$this->resetAfterTest();
|
||||
$user = $this->generate_user();
|
||||
$userrepo = new user_repository();
|
||||
$sink = $this->redirectEvents();
|
||||
$saveduser = $userrepo->save($user);
|
||||
$events = $sink->get_events();
|
||||
$sink->close();
|
||||
|
||||
$this->assertIsInt($saveduser->get_id());
|
||||
$this->assert_same_user_values($user, $saveduser, true);
|
||||
$this->assert_user_db_values($saveduser);
|
||||
// No change to underlying user: city, etc. take on default values matching those of the existing user record.
|
||||
$this->assertEmpty($events);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests adding a user to the store, assuming that the user has been created using modified 'user default values'.
|
||||
*
|
||||
* @covers ::save
|
||||
*/
|
||||
public function test_save_new_changed_user_defaults() {
|
||||
$this->resetAfterTest();
|
||||
$user = $this->generate_user(1, ['city' => 'Perth']);
|
||||
$userrepo = new user_repository();
|
||||
$sink = $this->redirectEvents();
|
||||
$saveduser = $userrepo->save($user);
|
||||
$events = $sink->get_events();
|
||||
$sink->close();
|
||||
|
||||
$this->assertIsInt($saveduser->get_id());
|
||||
$this->assert_same_user_values($user, $saveduser, true);
|
||||
$this->assert_user_db_values($saveduser);
|
||||
// The underlying user record will change: city ('Perth') differs from that of the existing user ('').
|
||||
$this->assertInstanceOf(\core\event\user_updated::class, $events[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,16 +211,25 @@ class user_repository_test extends \advanced_testcase {
|
||||
$this->resetAfterTest();
|
||||
$user = $this->generate_user();
|
||||
$userrepo = new user_repository();
|
||||
$sink = $this->redirectEvents();
|
||||
$saveduser = $userrepo->save($user);
|
||||
$events = $sink->get_events();
|
||||
$sink->close();
|
||||
$this->assertEmpty($events); // No event for the first save, since the underlying user record is unchanged.
|
||||
|
||||
$saveduser->set_city('New City');
|
||||
$saveduser->set_country('NZ');
|
||||
$saveduser->set_lastgrade(99.99999999);
|
||||
$sink = $this->redirectEvents();
|
||||
$saveduser2 = $userrepo->save($saveduser);
|
||||
$events = $sink->get_events();
|
||||
$sink->close();
|
||||
|
||||
$this->assertEquals($saveduser->get_id(), $saveduser2->get_id());
|
||||
$this->assert_same_user_values($saveduser, $saveduser2, true);
|
||||
$this->assert_user_db_values($saveduser2);
|
||||
// The underlying user record will change now, since city and country have changed.
|
||||
$this->assertInstanceOf(\core\event\user_updated::class, $events[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -455,4 +510,43 @@ class user_repository_test extends \advanced_testcase {
|
||||
$this->assertEquals($saveduser->get_localid(), $saveduser2->get_localid());
|
||||
$this->assertNotEquals($saveduser->get_id(), $saveduser2->get_id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test confirming that any associated legacy lti user records are not returned by the repository.
|
||||
*
|
||||
* This test ensures that any enrolment methods (resources) updated in-place from legacy LTI to 1.3 only return LTI 1.3 users.
|
||||
*
|
||||
* @covers ::find
|
||||
* @covers ::find_single_user_by_resource
|
||||
* @covers ::find_by_resource
|
||||
*/
|
||||
public function test_find_filters_legacy_lti_users(): void {
|
||||
$this->resetAfterTest();
|
||||
global $DB;
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$resource = $this->getDataGenerator()->create_lti_tool((object)['courseid' => $course->id]);
|
||||
$ltiuserdata = [
|
||||
'userid' => $user->id,
|
||||
'toolid' => $resource->id,
|
||||
'sourceid' => '1001',
|
||||
];
|
||||
$ltiuserid = $DB->insert_record('enrol_lti_users', $ltiuserdata);
|
||||
$userrepo = new user_repository();
|
||||
|
||||
$this->assertNull($userrepo->find($ltiuserid));
|
||||
$this->assertNull($userrepo->find_single_user_by_resource($user->id, $resource->id));
|
||||
$this->assertEmpty($userrepo->find_by_resource($resource->id));
|
||||
|
||||
// Set deploymentid, indicating the user originated from an LTI 1.3 launch and should now be returned.
|
||||
$ltiuserdata['id'] = $ltiuserid;
|
||||
$ltiuserdata['ltideploymentid'] = '234';
|
||||
$DB->update_record('enrol_lti_users', $ltiuserdata);
|
||||
|
||||
$this->assertInstanceOf(user::class, $userrepo->find($ltiuserid));
|
||||
$this->assertInstanceOf(user::class, $userrepo->find_single_user_by_resource($user->id, $resource->id));
|
||||
$ltiusers = $userrepo->find_by_resource($resource->id);
|
||||
$this->assertCount(1, $ltiusers);
|
||||
$this->assertInstanceOf(user::class, reset($ltiusers));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,12 @@ if ($tool->status != ENROL_INSTANCE_ENABLED) {
|
||||
exit();
|
||||
}
|
||||
|
||||
// Check if the enrolment instance has been upgraded to a newer LTI version.
|
||||
if ($tool->ltiversion != 'LTI-1p0/LTI-2p0') {
|
||||
throw new \moodle_exception('enrolltiversionincorrect', 'enrol_lti');
|
||||
exit();
|
||||
}
|
||||
|
||||
$consumerkey = required_param('oauth_consumer_key', PARAM_TEXT);
|
||||
$ltiversion = optional_param('lti_version', null, PARAM_TEXT);
|
||||
$messagetype = required_param('lti_message_type', PARAM_TEXT);
|
||||
|
||||
@@ -72,6 +72,7 @@ Feature: Render H5P content using filters
|
||||
And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
|
||||
And I wait until the page is ready
|
||||
When I click on "Save and display" "button"
|
||||
And I should see "PageName1" in the "page-header" "region"
|
||||
# Switch to iframe created by filter
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
# Switch to iframe created by embed.php page
|
||||
@@ -107,6 +108,7 @@ Feature: Render H5P content using filters
|
||||
And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
|
||||
And I wait until the page is ready
|
||||
When I click on "Save and display" "button"
|
||||
And I should see "PageName1" in the "page-header" "region"
|
||||
# Switch to iframe created by filter
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
Then I should see "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload new libraries."
|
||||
@@ -132,6 +134,7 @@ Feature: Render H5P content using filters
|
||||
And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
|
||||
And I wait until the page is ready
|
||||
And I click on "Save and display" "button"
|
||||
And I should see "PageName1" in the "page-header" "region"
|
||||
# Switch to iframe created by filter
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
# Libraries don't exist, so an error should be displayed.
|
||||
@@ -155,6 +158,7 @@ Feature: Render H5P content using filters
|
||||
And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
|
||||
And I wait until the page is ready
|
||||
And I click on "Save and display" "button"
|
||||
And I should see "PageName2" in the "page-header" "region"
|
||||
# Switch to iframe created by filter
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
# Switch to iframe created by embed.php page
|
||||
@@ -194,6 +198,7 @@ Feature: Render H5P content using filters
|
||||
And I click on "Select this file" "button"
|
||||
And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
|
||||
And I click on "Save and display" "button"
|
||||
And I should see "PageName1" in the "page-header" "region"
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
# Library is disabled, so an error should be displayed.
|
||||
Then I should see "This file can't be displayed because its content type is disabled."
|
||||
|
||||
+2
-27
@@ -16,11 +16,8 @@
|
||||
* Other platforms could/should be added
|
||||
*/
|
||||
public function __construct() {
|
||||
global $CFG;
|
||||
|
||||
// construct directory structure
|
||||
$this->temp_dir = $CFG->tempdir . "/latex";
|
||||
make_temp_directory('latex');
|
||||
// Construct directory structure.
|
||||
$this->temp_dir = make_request_directory();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,26 +157,4 @@
|
||||
|
||||
return $img;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete files created in temporary area
|
||||
* Don't forget to copy the final gif/png before calling this
|
||||
* @param string $filename file base (no extension)
|
||||
*/
|
||||
function clean_up( $filename ) {
|
||||
global $CFG;
|
||||
|
||||
unlink( "{$this->temp_dir}/$filename.tex" );
|
||||
unlink( "{$this->temp_dir}/$filename.dvi" );
|
||||
unlink( "{$this->temp_dir}/$filename.ps" );
|
||||
$convertformat = get_config('filter_tex', 'convertformat');
|
||||
unlink( "{$this->temp_dir}/$filename.{$convertformat}" );
|
||||
unlink( "{$this->temp_dir}/$filename.aux" );
|
||||
unlink( "{$this->temp_dir}/$filename.log" );
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ function filter_tex_sanitize_formula(string $texexp): string {
|
||||
'\afterassignment', '\expandafter', '\noexpand', '\special',
|
||||
'\let', '\futurelet', '\else', '\fi', '\chardef', '\makeatletter', '\afterground',
|
||||
'\noexpand', '\line', '\mathcode', '\item', '\section', '\mbox', '\declarerobustcommand',
|
||||
'\ExplSyntaxOn',
|
||||
];
|
||||
|
||||
$allowlist = ['inputenc'];
|
||||
|
||||
@@ -50,8 +50,6 @@ define('NO_MOODLE_COOKIES', true); // Because it interferes with caching
|
||||
$lateximage = $latex->render($texexp, $image, 12, $density, $background);
|
||||
if ($lateximage) {
|
||||
copy($lateximage, $pathname);
|
||||
$latex->clean_up($md5);
|
||||
|
||||
} else {
|
||||
// failing that, use mimetex
|
||||
$texexp = $texcache->rawtext;
|
||||
|
||||
+1
-1
@@ -5,6 +5,6 @@ define("core_grades/searchwidget/basewidget",["exports","core/utils","core/templ
|
||||
* @module core_grades/searchwidget/basewidget
|
||||
* @copyright 2022 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showLoader=_exports.registerListenerEvents=_exports.promisesAndResolvers=_exports.init=void 0,Templates=_interopRequireWildcard(Templates),Selectors=_interopRequireWildcard(Selectors),_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};_exports.init=async function(widgetContentContainer,bodyPromise,data,searchFunc){let unsearchableContent=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null;bodyPromise.then((async bodyContent=>{if(widgetContentContainer.innerHTML=bodyContent,unsearchableContent){widgetContentContainer.querySelector(Selectors.regions.unsearchableContent).innerHTML+=unsearchableContent}const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults);await showLoader(searchResultsContainer),await renderSearchResults(searchResultsContainer,data),registerListenerEvents(widgetContentContainer,data,searchFunc)})).catch(_notification.default.exception)};const registerListenerEvents=(widgetContentContainer,data,searchFunc)=>{const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults),searchInput=widgetContentContainer.querySelector(Selectors.actions.search);searchInput.focus();const clearSearchButton=widgetContentContainer.querySelector(Selectors.actions.clearSearch);searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{searchInput.value.length>0?clearSearchButton.classList.remove("d-none"):clearSearchButton.classList.add("d-none"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}),300)),clearSearchButton.addEventListener("click",(async e=>{e.stopPropagation(),searchInput.value="",searchInput.focus(),clearSearchButton.classList.add("d-none"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}))};_exports.registerListenerEvents=registerListenerEvents;const showLoader=async container=>{const{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/loading",{});Templates.replaceNodeContents(container,html,js)};_exports.showLoader=showLoader;const debounceCallee=(searchValue,data,searchFunction)=>searchValue.length>0?searchFunction(data,searchValue):data,renderSearchResults=async(searchResultsContainer,searchResultsData)=>{const templateData={searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/searchresults",templateData);await Templates.replaceNodeContents(searchResultsContainer,html,js)};_exports.promisesAndResolvers=()=>{let bodyPromiseResolver;const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve}));return{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}}}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showLoader=_exports.registerListenerEvents=_exports.promisesAndResolvers=_exports.init=void 0,Templates=_interopRequireWildcard(Templates),Selectors=_interopRequireWildcard(Selectors),_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};_exports.init=async function(widgetContentContainer,bodyPromise,data,searchFunc){let unsearchableContent=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null;bodyPromise.then((async bodyContent=>{if(widgetContentContainer.innerHTML=bodyContent,unsearchableContent){widgetContentContainer.querySelector(Selectors.regions.unsearchableContent).innerHTML+=unsearchableContent}const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults);await showLoader(searchResultsContainer),await renderSearchResults(searchResultsContainer,data),registerListenerEvents(widgetContentContainer,data,searchFunc)})).catch(_notification.default.exception)};const registerListenerEvents=(widgetContentContainer,data,searchFunc)=>{const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults),searchInput=widgetContentContainer.querySelector(Selectors.actions.search);if(!searchInput)return;searchInput.focus();const clearSearchButton=widgetContentContainer.querySelector(Selectors.actions.clearSearch);searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{searchInput.value.length>0?clearSearchButton.classList.remove("d-none"):clearSearchButton.classList.add("d-none"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}),300)),clearSearchButton.addEventListener("click",(async e=>{e.stopPropagation(),searchInput.value="",searchInput.focus(),clearSearchButton.classList.add("d-none"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}))};_exports.registerListenerEvents=registerListenerEvents;const showLoader=async container=>{const{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/loading",{});Templates.replaceNodeContents(container,html,js)};_exports.showLoader=showLoader;const debounceCallee=(searchValue,data,searchFunction)=>searchValue.length>0?searchFunction(data,searchValue):data,renderSearchResults=async(searchResultsContainer,searchResultsData)=>{const templateData={searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/searchresults",templateData);await Templates.replaceNodeContents(searchResultsContainer,html,js)};_exports.promisesAndResolvers=()=>{let bodyPromiseResolver;const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve}));return{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}}}));
|
||||
|
||||
//# sourceMappingURL=basewidget.min.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -68,6 +68,12 @@ export const init = async(widgetContentContainer, bodyPromise, data, searchFunc,
|
||||
export const registerListenerEvents = (widgetContentContainer, data, searchFunc) => {
|
||||
const searchResultsContainer = widgetContentContainer.querySelector(Selectors.regions.searchResults);
|
||||
const searchInput = widgetContentContainer.querySelector(Selectors.actions.search);
|
||||
|
||||
if (!searchInput) {
|
||||
// Too late. The widget is already closed and its content is empty.
|
||||
return;
|
||||
}
|
||||
|
||||
// We want to focus on the first known user interable element within the dropdown.
|
||||
searchInput.focus();
|
||||
const clearSearchButton = widgetContentContainer.querySelector(Selectors.actions.clearSearch);
|
||||
|
||||
+1
-2
@@ -80,7 +80,7 @@ class singleview extends core_course_external {
|
||||
$coursecontext = context_course::instance($params['courseid']);
|
||||
parent::validate_context($coursecontext);
|
||||
|
||||
$gtree = new grade_tree($params['courseid'], false, false, null, !$CFG->enableoutcomes);
|
||||
$gtree = new grade_tree($params['courseid'], false, true, null, !$CFG->enableoutcomes);
|
||||
$gradeableitems = $gtree->get_items();
|
||||
|
||||
$gradeitems = array_map(function ($gradeitem) use ($PAGE, $USER, $params) {
|
||||
@@ -98,7 +98,6 @@ class singleview extends core_course_external {
|
||||
|
||||
return $item;
|
||||
}, $gradeableitems);
|
||||
sort($gradeitems);
|
||||
|
||||
return [
|
||||
'gradeitems' => $gradeitems,
|
||||
|
||||
@@ -157,7 +157,8 @@ class grade_items extends base {
|
||||
$html = \html_writer::start_div('page-context-header' . $dimmed);
|
||||
// Image data.
|
||||
$html .= \html_writer::div($imagedata, 'page-header-image mr-2');
|
||||
$prefix = \html_writer::div($row->itemmodule, 'text-muted text-uppercase small line-height-3');
|
||||
$prefix = \html_writer::div(get_string('pluginname', "mod_{$row->itemmodule}"),
|
||||
'text-muted text-uppercase small line-height-3');
|
||||
$name = $prefix . \html_writer::link($url, format_string($cm->name, true));
|
||||
$html .= \html_writer::tag('div', $name, ['class' => 'page-header-headings']);
|
||||
} else {
|
||||
|
||||
+11
-7
@@ -187,7 +187,8 @@ class core_group_external extends external_api {
|
||||
}
|
||||
require_capability('moodle/course:managegroups', $context);
|
||||
|
||||
list($group->description, $group->descriptionformat) =
|
||||
$group->name = external_format_string($group->name, $context);
|
||||
[$group->description, $group->descriptionformat] =
|
||||
external_format_text($group->description, $group->descriptionformat,
|
||||
$context->id, 'group', 'description', $group->id);
|
||||
|
||||
@@ -209,7 +210,7 @@ class core_group_external extends external_api {
|
||||
array(
|
||||
'id' => new external_value(PARAM_INT, 'group record id'),
|
||||
'courseid' => new external_value(PARAM_INT, 'id of course'),
|
||||
'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
|
||||
'name' => new external_value(PARAM_TEXT, 'group name'),
|
||||
'description' => new external_value(PARAM_RAW, 'group description text'),
|
||||
'descriptionformat' => new external_format_value('description'),
|
||||
'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'),
|
||||
@@ -260,7 +261,8 @@ class core_group_external extends external_api {
|
||||
|
||||
$groups = array();
|
||||
foreach ($gs as $group) {
|
||||
list($group->description, $group->descriptionformat) =
|
||||
$group->name = external_format_string($group->name, $context);
|
||||
[$group->description, $group->descriptionformat] =
|
||||
external_format_text($group->description, $group->descriptionformat,
|
||||
$context->id, 'group', 'description', $group->id);
|
||||
$groups[] = (array)$group;
|
||||
@@ -281,7 +283,7 @@ class core_group_external extends external_api {
|
||||
array(
|
||||
'id' => new external_value(PARAM_INT, 'group record id'),
|
||||
'courseid' => new external_value(PARAM_INT, 'id of course'),
|
||||
'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
|
||||
'name' => new external_value(PARAM_TEXT, 'group name'),
|
||||
'description' => new external_value(PARAM_RAW, 'group description text'),
|
||||
'descriptionformat' => new external_format_value('description'),
|
||||
'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'),
|
||||
@@ -1264,7 +1266,8 @@ class core_group_external extends external_api {
|
||||
'g.id, g.name, g.description, g.descriptionformat, g.idnumber');
|
||||
|
||||
foreach ($groups as $group) {
|
||||
list($group->description, $group->descriptionformat) =
|
||||
$group->name = external_format_string($group->name, $course->context);
|
||||
[$group->description, $group->descriptionformat] =
|
||||
external_format_text($group->description, $group->descriptionformat,
|
||||
$course->context->id, 'group', 'description', $group->id);
|
||||
$group->courseid = $course->id;
|
||||
@@ -1303,7 +1306,7 @@ class core_group_external extends external_api {
|
||||
return new external_single_structure(
|
||||
array(
|
||||
'id' => new external_value(PARAM_INT, 'group record id'),
|
||||
'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
|
||||
'name' => new external_value(PARAM_TEXT, 'group name'),
|
||||
'description' => new external_value(PARAM_RAW, 'group description text'),
|
||||
'descriptionformat' => new external_format_value('description'),
|
||||
'idnumber' => new external_value(PARAM_RAW, 'id number'),
|
||||
@@ -1388,7 +1391,8 @@ class core_group_external extends external_api {
|
||||
$groups = groups_get_activity_allowed_groups($cm, $user->id);
|
||||
|
||||
foreach ($groups as $group) {
|
||||
list($group->description, $group->descriptionformat) =
|
||||
$group->name = external_format_string($group->name, $coursecontext);
|
||||
[$group->description, $group->descriptionformat] =
|
||||
external_format_text($group->description, $group->descriptionformat,
|
||||
$coursecontext->id, 'group', 'description', $group->id);
|
||||
$group->courseid = $cm->course;
|
||||
|
||||
+4
-3
@@ -95,7 +95,7 @@ switch ($action) {
|
||||
|
||||
foreach($groupmemberroles as $roleid=>$roledata) {
|
||||
$shortroledata = new stdClass();
|
||||
$shortroledata->name = $roledata->name;
|
||||
$shortroledata->name = html_entity_decode($roledata->name, ENT_QUOTES, 'UTF-8');
|
||||
$shortroledata->users = array();
|
||||
foreach($roledata->users as $member) {
|
||||
$shortmember = new stdClass();
|
||||
@@ -183,7 +183,7 @@ if ($groups) {
|
||||
foreach ($groups as $group) {
|
||||
$selected = false;
|
||||
$usercount = $DB->count_records('groups_members', array('groupid' => $group->id));
|
||||
$groupname = format_string($group->name) . ' (' . $usercount . ')';
|
||||
$groupname = format_string($group->name, true, ['context' => $context, 'escape' => false]) . ' (' . $usercount . ')';
|
||||
if (in_array($group->id, $groupids)) {
|
||||
$selected = true;
|
||||
if ($singlegroup) {
|
||||
@@ -234,8 +234,9 @@ if ($singlegroup) {
|
||||
|
||||
$users[] = $shortmember;
|
||||
}
|
||||
|
||||
$members[] = (object)[
|
||||
'role' => s($roledata->name),
|
||||
'role' => html_entity_decode($roledata->name, ENT_QUOTES, 'UTF-8'),
|
||||
'rolemembers' => $users
|
||||
];
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
{{#members}}
|
||||
<optgroup label="{{role}}">
|
||||
{{#rolemembers}}
|
||||
<option value="{{value}}" title="{{{text}}}">{{{text}}}</option>
|
||||
<option value="{{value}}" title="{{text}}">{{{text}}}</option>
|
||||
{{/rolemembers}}
|
||||
</optgroup>
|
||||
{{/members}}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
This files describes API changes in /group/*,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 4.1.4 ===
|
||||
* The following external methods now return group names correctly formatted:
|
||||
- `core_group_get_groups`
|
||||
- `core_group_get_course_groups`
|
||||
- `core_group_get_course_user_groups`
|
||||
- `core_group_get_activity_allowed_groups`
|
||||
|
||||
=== 3.11 ===
|
||||
|
||||
* The groups do not support 'hidepicture' any more, and so the column 'hidepicture'
|
||||
|
||||
@@ -32,4 +32,5 @@ defined('MOODLE_INTERNAL') || die();
|
||||
$string['language'] = 'भाषा';
|
||||
$string['moodlelogo'] = 'Moodle लोगो';
|
||||
$string['next'] = 'अगला';
|
||||
$string['previous'] = 'पिछला';
|
||||
$string['reload'] = 'सीमा से अधिक लादना';
|
||||
|
||||
@@ -29,5 +29,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['parentlanguage'] = '';
|
||||
$string['thisdirection'] = 'ltr';
|
||||
$string['thislanguage'] = 'Монгол';
|
||||
|
||||
@@ -33,9 +33,9 @@ $string['admindirname'] = 'Director Admin';
|
||||
$string['availablelangs'] = 'Pachete de limbă disponibile';
|
||||
$string['chooselanguagehead'] = 'Selectare limbă';
|
||||
$string['chooselanguagesub'] = 'Vă rugăm selectaţi limba pentru interfaţa de instalare, limba selectată va fi folosită EXCLUSIV în cadrul procedurii de instalare. Ulterior veţi putea selecta limba în care doriţi să fie afişată interfaţa.';
|
||||
$string['clialreadyconfigured'] = 'Fișierul de configurare';
|
||||
$string['clialreadyconfigured'] = 'Fișierul de configurare config.php există deja. Te rugăm să utilizezi admin/cli/install_database.php pentru a instala Moodle pe acest site.';
|
||||
$string['clialreadyinstalled'] = 'Fișierul de configurare config.php există deja. Vă rugăm să folosiți dmin/cli/install_database.php to pentru a upgrada Moodle pentru acest site.';
|
||||
$string['cliinstallheader'] = 'Program command line installation Moodle {$a}';
|
||||
$string['cliinstallheader'] = 'Program command line installation {$a} Moodle';
|
||||
$string['clitablesexist'] = 'Tabele de baze de date deja prezente; Instalarea CLI nu poate continua.';
|
||||
$string['databasehost'] = 'Gazdă baza de date';
|
||||
$string['databasename'] = 'Nume baza de date';
|
||||
|
||||
+5
-5
@@ -383,7 +383,7 @@ $string['configstripalltitletags'] = 'Uncheck this setting to allow HTML tags in
|
||||
$string['configsupportavailability'] = 'Determines who has access to contact site support from the footer.';
|
||||
$string['configsupportemail'] = 'If SMTP is configured on this site and a support page is not set, this email address will receive messages submitted through the support form. If sending fails, the email address will be displayed to logged-in users.';
|
||||
$string['configsupportname'] = 'The name of the person or other entity providing support via the support form or support page.';
|
||||
$string['configsupportpage'] = 'Enter the URL of a support page or leave empty to link to a contact form.';
|
||||
$string['configsupportpage'] = 'Enter the URL of a support page or leave empty to link to a contact form. The link is displayed in the site footer.';
|
||||
$string['configtempdatafoldercleanup'] = 'Remove temporary data files from the data folder that are older than the selected time.';
|
||||
$string['configthemedesignermode'] = 'Normally all theme images and style sheets are cached in browsers and on the server for a very long time, for performance. If you are designing themes or developing code then you probably want to turn this mode on so that you are not served cached versions. Warning: this will make your site slower for all users! Alternatively, you can also reset the theme caches manually from the Theme selection page.';
|
||||
$string['configthemelist'] = 'Leave this blank to allow any valid theme to be used. If you want to shorten the theme menu, you can specify a comma-separated list of names here (Don\'t use spaces!).
|
||||
@@ -606,7 +606,7 @@ $string['environmentrequireversion'] = 'version {$a->needed} is required and you
|
||||
$string['environmentsettingok'] = 'recommended setting detected';
|
||||
$string['environmentshouldfixsetting'] = 'PHP setting should be changed.';
|
||||
$string['environmentxmlerror'] = 'Error reading environment data ({$a->error_code})';
|
||||
$string['environmentmariadbwrongdbtype'] = 'Wrong <code>$CFG->dbtype</code>: you need to change it in your <code>config.php</code> file, from \'<code>mysql</code>\' to \'<code>mariadb</code>\'.';
|
||||
$string['environmentmariadbwrongdbtype'] = 'Wrong $CFG->dbtype. You need to change it in your config.php file from \'mysqli\' to \'mariadb\'.';
|
||||
$string['errordeletingconfig'] = 'An error occurred while deleting the configuration records for plugin \'{$a}\'.';
|
||||
$string['errorsetting'] = 'Could not save setting:';
|
||||
$string['errorwithsettings'] = 'Some settings were not changed due to an error.';
|
||||
@@ -724,9 +724,8 @@ $string['ipblocker'] = 'IP blocker';
|
||||
$string['ipblockersyntax'] = 'Put every entry on one line. Valid entries are either full IP address (such as <b>192.168.10.1</b>) which matches a single host; or partial address (such as <b>192.168</b>) which matches any address starting with those numbers; or CIDR notation (such as <b>231.54.211.0/20</b>); or a range of IP addresses (such as <b>231.3.56.10-20</b>) where the range applies to the last part of the address. Text domain names (like \'example.com\') are not supported. Blank lines, and text following a "#" character are ignored.';
|
||||
$string['iplookup'] = 'IP address lookup';
|
||||
$string['iplookupgeoplugin'] = 'The <a href="https://www.geoplugin.com">geoPlugin</a> service is currently being used to look up geographical information. For more accurate results we recommend installing a local copy of the MaxMind GeoLite database.';
|
||||
$string['iplookupinfo'] = 'By default Moodle uses the free online NetGeo (The Internet Geographic Database) server to lookup location of IP addresses, unfortunately this database is not maintained anymore and may return <em>wildly incorrect</em> data.
|
||||
It is recommended to install local copy of free GeoLite2 City database from MaxMind.<br />
|
||||
IP address location is displayed on simple map or using Google Maps. Please note that you need to have a Google account and apply for free Google Maps API key to enable interactive maps.';
|
||||
$string['iplookupinfo'] = 'The free online NetGeo server is used to look up IP address locations. The database is not maintained any more though, and may return incorrect data. Installing a local copy of the free GeoLite2 City database from MaxMind is recommended.<br />
|
||||
If a Google Maps API V3 key is provided, IP address location is displayed using Google Maps. You need a Google account and to apply for a Google Maps API key. If no Google Maps API V3 key is provided, IP address location is displayed using OpenStreetMap.';
|
||||
$string['iplookupmaxmindnote'] = 'This product includes GeoLite2 data created by MaxMind, available from <a href="https://www.maxmind.com">https://www.maxmind.com</a>.';
|
||||
$string['iscustomadminwarnings'] = 'It has been detected that your site is using a custom admin directory. This feature is no longer supported and will be removed after Moodle 4.2.';
|
||||
$string['ishttpswarning'] = 'It has been detected that your site is not secured using HTTPS. It is strongly recommended to migrate your site to HTTPS for increased security and improved integration with other systems.';
|
||||
@@ -1449,6 +1448,7 @@ $string['unsupportedphpversion73'] = 'PHP version 7.3 and higher are not support
|
||||
$string['unsupportedphpversion74'] = 'PHP version 7.4 and higher are not supported.';
|
||||
$string['unsupportedphpversion80'] = 'PHP version 8.0 and higher are not supported.';
|
||||
$string['unsupportedphpversion81'] = 'PHP version 8.1 and higher are not supported.';
|
||||
$string['unsupportedphpversion82'] = 'PHP version 8.2 and higher are not supported.';
|
||||
$string['unsuspenduser'] = 'Activate user account';
|
||||
$string['updateaccounts'] = 'Update existing accounts';
|
||||
$string['updatecomponent'] = 'Update component';
|
||||
|
||||
@@ -488,6 +488,8 @@ $string['onlyadmins'] = 'Only administrators can do that';
|
||||
$string['onlyeditingteachers'] = 'Only editing teachers can do that';
|
||||
$string['onlyeditown'] = 'You can only edit your own information';
|
||||
$string['orderidnotfound'] = 'Order ID {$a} not found';
|
||||
$string['opensslsignerror'] = 'OpenSSL unable to sign data';
|
||||
$string['opensslsealerror'] = 'OpenSSL unable to seal data';
|
||||
$string['pagenotexisttitle'] = '404 Error: File not found';
|
||||
$string['pagenotexist'] = '<p>An unusual error occurred trying to view a page that does not exist:</p>{$a}';
|
||||
$string['pathdoesnotstartslash'] = 'No valid arguments supplied, path does not start with slash!';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user