Compare commits
381 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2fe75a270 | |||
| 6b632fef09 | |||
| bc3e140d8b | |||
| b53f12d8c9 | |||
| f9c700eb9f | |||
| 0d3495e6b2 | |||
| 72eac30148 | |||
| e19f451723 | |||
| db0073291b | |||
| 984e3fc510 | |||
| 698142b1f5 | |||
| 8669b0ebb7 | |||
| 2204ea8afe | |||
| 142e12bee7 | |||
| 2bdebbf6e7 | |||
| f47ec5d0a9 | |||
| 52da2d6eaa | |||
| 78b0f6e204 | |||
| 7030fa5972 | |||
| 2bd53efc1b | |||
| 9ccb3a2af9 | |||
| 3354f46fd6 | |||
| 9fb1e3d300 | |||
| e4b11bf852 | |||
| 28cf3f9119 | |||
| 1a885a1756 | |||
| 753035e090 | |||
| cb98541563 | |||
| 2729301721 | |||
| 54a2d7df22 | |||
| 59f97a627f | |||
| 362c67f84d | |||
| cbe5ec5a51 | |||
| 201eff4d98 | |||
| 22e52ebb55 | |||
| 493d3c218e | |||
| 97ff875c83 | |||
| 8a5601722d | |||
| 68aa19ba73 | |||
| c8d12c3217 | |||
| f9d08701fb | |||
| 324e1fbb78 | |||
| b58a4e8da4 | |||
| e7c0ddb942 | |||
| e9d63ac29c | |||
| fce440eeed | |||
| 5fae2fc7a1 | |||
| 913eb31830 | |||
| 68fbcaf303 | |||
| 20403e0e43 | |||
| 2740e06920 | |||
| a84b51046a | |||
| a19092e71c | |||
| b41a748a98 | |||
| cd96cb300e | |||
| e3c031023f | |||
| c5b4da7257 | |||
| 7edc33a8ce | |||
| 61e9318339 | |||
| f254b2b88f | |||
| 264fe69743 | |||
| 80e2f8473e | |||
| b9b5f4b489 | |||
| dc871f251e | |||
| f972485484 | |||
| 1f9488ccb5 | |||
| 1d8512bc0e | |||
| b751583087 | |||
| 94b897a176 | |||
| 4c6b771c2f | |||
| b8122e1de3 | |||
| a9eae7edf4 | |||
| a7a9150b62 | |||
| c283ed04e0 | |||
| f65e706662 | |||
| 49e1731fbe | |||
| cd320b32b5 | |||
| 8dea153c6b | |||
| ae9fea0b47 | |||
| dc9258574d | |||
| 5b94aaed06 | |||
| a099c8d921 | |||
| 36a3c41b8b | |||
| d7208f8dd0 | |||
| df9c7f755d | |||
| a76583296b | |||
| b56affcfe6 | |||
| 4799f8b7c4 | |||
| 62758b8224 | |||
| cf6aeae427 | |||
| 8ea5b30a5d | |||
| 270f52bf55 | |||
| 1affa30299 | |||
| f61b2edde0 | |||
| d1b2674eea | |||
| 849f392f3b | |||
| 2c73122c60 | |||
| 3b6104d6dc | |||
| be2eb84be9 | |||
| 711b1a5ae9 | |||
| df136f7535 | |||
| 6c996b1570 | |||
| f01179355c | |||
| 414102ed74 | |||
| 24cd545eb8 | |||
| bcf349ed45 | |||
| ae221af121 | |||
| 4f87b28436 | |||
| b3a66b1484 | |||
| 52b5aa5a18 | |||
| de6a44d7d0 | |||
| 21941c6c2a | |||
| 54da1506a1 | |||
| 7f3a1dff28 | |||
| 818182879a | |||
| e995073997 | |||
| bc47266a65 | |||
| 799e988f80 | |||
| 9bae0ce6a7 | |||
| d6a633d7a8 | |||
| 16dff14e56 | |||
| c0d26fff0d | |||
| 3698f34fc3 | |||
| e52ec9edd4 | |||
| 63b4929729 | |||
| 3afbffe6a0 | |||
| 1dd574efd8 | |||
| a426320c54 | |||
| 3dea860e64 | |||
| 69f3e0753e | |||
| 690bad040a | |||
| 22c30c160e | |||
| b28b6066b4 | |||
| c4e993d546 | |||
| 5b7d2a1992 | |||
| 23846c5450 | |||
| b6d65c77cf | |||
| f77ec5e7fd | |||
| 9031cf789d | |||
| 010aa5ee16 | |||
| 2014c08636 | |||
| f5531458aa | |||
| a6e2d950e7 | |||
| dddc4cc410 | |||
| 2e16fca0f1 | |||
| 30d1832f45 | |||
| 24c44b38be | |||
| d848c604dd | |||
| 9a4034f3bf | |||
| 4c1d448b5b | |||
| f92e83fe36 | |||
| d500b5c62d | |||
| 65fad5f93f | |||
| 7ea8dc7112 | |||
| 276476233d | |||
| a565c5d93d | |||
| a72e1f1e0d | |||
| 2d86d87012 | |||
| 27ea958dfe | |||
| 2410df1fd2 | |||
| ccb1a3b39f | |||
| ce2058c4c2 | |||
| b7fb146cca | |||
| 2695a394fe | |||
| 05e9ae07cd | |||
| c14603ecd1 | |||
| 5d2d5bc542 | |||
| 3d92acac8a | |||
| ac115326f9 | |||
| cb94a54412 | |||
| 0c2e30809a | |||
| a6c9132e73 | |||
| 323276969f | |||
| 18a99edcb9 | |||
| 9d6fff9671 | |||
| c4dad5b5e6 | |||
| 9c4204c96b | |||
| 15da5ba48e | |||
| c7e65064b1 | |||
| 32702452e7 | |||
| 2e1151bbb3 | |||
| 2e7cc5e17c | |||
| 513ac0c086 | |||
| 9f7752d6bc | |||
| 79071cdd87 | |||
| 69962f36d7 | |||
| 76ef9c5d1a | |||
| b67b742e40 | |||
| eece2df651 | |||
| 61ef211525 | |||
| 535fcef31e | |||
| 817757c088 | |||
| 054d5c0f04 | |||
| adcd50bea0 | |||
| 3018a35249 | |||
| 1d217301ad | |||
| 4dc6c7a79d | |||
| 6d6a39e34d | |||
| 1b169c82be | |||
| 91376cdb69 | |||
| 3f5477f8a0 | |||
| 2166e3e681 | |||
| 9e3b15f31e | |||
| dfe187a59d | |||
| 45725c2408 | |||
| 8aea2ef66b | |||
| f065061f08 | |||
| 92df285f49 | |||
| c72839d6d0 | |||
| f33b8bb576 | |||
| 7ccf8f2fe4 | |||
| 0997a89e39 | |||
| f63aed03b8 | |||
| cf3215cff4 | |||
| d26512cb63 | |||
| b4ccc75a03 | |||
| 49f994fb03 | |||
| ebd1eb3726 | |||
| 88d21b999c | |||
| 946cbedf54 | |||
| 6ec958e639 | |||
| 737682c750 | |||
| 138e5b9996 | |||
| f3f7f28015 | |||
| 2bada93f17 | |||
| a5d64155ad | |||
| e511a7fdc4 | |||
| 767c610be1 | |||
| c0f4d9e170 | |||
| e4c7028c20 | |||
| 375ec1c034 | |||
| 1c3b9af49a | |||
| d4d2ef9119 | |||
| fdcc9383b1 | |||
| d1d57dc12d | |||
| 27a6bf394d | |||
| 5e4aab248f | |||
| c32142cf80 | |||
| ea90581cb1 | |||
| 505366bed8 | |||
| a6a5130a65 | |||
| a53238a07b | |||
| e085678bf2 | |||
| ad3ac443a9 | |||
| 81da134c2c | |||
| dc4a36c3b1 | |||
| 946120eeee | |||
| 82d35db47a | |||
| 83c3d04702 | |||
| 16736110d8 | |||
| 22f1a27aa3 | |||
| 30e6bf75dd | |||
| 9c4a0b8490 | |||
| da03cf8972 | |||
| 0b3e3d7c54 | |||
| 1b8ced636c | |||
| 2c73fbfc3d | |||
| 5e3c934691 | |||
| df3efafbcf | |||
| e2f2de4885 | |||
| 5279159083 | |||
| 8392586599 | |||
| 26a0581c1b | |||
| 6037a3dde0 | |||
| 471d1a4b6d | |||
| 3db93ec2a2 | |||
| ff6ac0b2fd | |||
| 57d5be04f8 | |||
| c9d3c97e74 | |||
| a772325afc | |||
| 4c1c1aa6c6 | |||
| 05e79651b6 | |||
| a8c569c368 | |||
| 12afd60ba2 | |||
| c4fb2a2141 | |||
| 97d63b6272 | |||
| 39944aee16 | |||
| 3284f1fe7e | |||
| a96ad13616 | |||
| 3c872b34c6 | |||
| 4699142c9b | |||
| 613e7f8a88 | |||
| b240173b66 | |||
| 74730a2d63 | |||
| 0273fc7731 | |||
| eecb28b386 | |||
| 88f81cf8d1 | |||
| fe177d5dec | |||
| d80644a986 | |||
| bfe2779cea | |||
| 39916b12fd | |||
| b861c1dec7 | |||
| 808a335c90 | |||
| 3b55f2847a | |||
| a2e4730a35 | |||
| 7c2b6ed44f | |||
| 3c4d30bcf4 | |||
| c34c6a75df | |||
| 0464ae85ca | |||
| 3211454a2a | |||
| 06bebd6820 | |||
| 3f0cc3d712 | |||
| f71bfc90b1 | |||
| ae29d57085 | |||
| a2cf272e00 | |||
| 4736877c43 | |||
| aa6c15c975 | |||
| d7d8a654f3 | |||
| 2794943d3c | |||
| 8c62673dc5 | |||
| f7a80e3f2b | |||
| 773e7cd612 | |||
| 8f23a60ba8 | |||
| ce059497ac | |||
| 6c712c9caf | |||
| 6cfed9eebb | |||
| 2370d0c629 | |||
| 7cae4c7570 | |||
| 86f6671fe6 | |||
| 91a40ccce5 | |||
| 20c93ebb6f | |||
| 62ee8fd5c8 | |||
| 35c6838f14 | |||
| 596e7c7b0c | |||
| 1720bef450 | |||
| c197d3f14b | |||
| 42934afbed | |||
| a97de58aa7 | |||
| 47d87f7243 | |||
| 56f849dcc6 | |||
| 3784dcc912 | |||
| 2ad3d1a7f1 | |||
| 3265a02f6c | |||
| 6bc25f23fd | |||
| 42631c3e34 | |||
| 4e75827d72 | |||
| 6b06a36646 | |||
| bbf96cb595 | |||
| 918d45dfbd | |||
| 35b29cf219 | |||
| 6f5266c717 | |||
| 841fd94499 | |||
| 3c8abccaaa | |||
| 23cc13e8ef | |||
| b665e5aa33 | |||
| 5fc3b83497 | |||
| 56fdffa68e | |||
| 2b442c2fb7 | |||
| 7215c71486 | |||
| 9b3a8f7bd1 | |||
| 51b2acfae3 | |||
| 5dbf93200a | |||
| 4b987e5f7a | |||
| 337704e555 | |||
| 9f01cf6122 | |||
| 8872202049 | |||
| 855b9e504b | |||
| f253fa7f7e | |||
| 8ee6f49384 | |||
| f4f6534b40 | |||
| 1d70790168 | |||
| 17a5e73a9b | |||
| 5987bed350 | |||
| b6d0dec01e | |||
| c0d519b426 | |||
| 05c66fcebd | |||
| 5c545ca2ff | |||
| 4037bdd825 | |||
| c211e62465 | |||
| ff27bc6c5a | |||
| bab8ea6608 | |||
| 4501c37045 | |||
| b8322f7dd1 | |||
| a2221d6d8d | |||
| f702707ef7 | |||
| 71bb7f306e | |||
| 616935475b | |||
| 8d6cbcb248 | |||
| 31b1a9601f | |||
| b70b06bc61 | |||
| f8811d5661 |
+1
-1
@@ -89,4 +89,4 @@ theme/boost/amd/src/bootstrap/toast.js
|
||||
theme/boost/amd/src/bootstrap/tooltip.js
|
||||
theme/boost/amd/src/bootstrap/util.js
|
||||
theme/boost/amd/src/tether.js
|
||||
theme/boost/scss/fontawesome/
|
||||
theme/boost/scss/fontawesome/
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
name: Core
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- master
|
||||
- MOODLE_[0-9]+_STABLE
|
||||
tags-ignore:
|
||||
- v[0-9]+.[0-9]+.[0-9]+*
|
||||
|
||||
env:
|
||||
php: 7.4
|
||||
|
||||
@@ -44,3 +44,4 @@ composer.phar
|
||||
/lib/yuilib/*/*/*-coverage.js
|
||||
atlassian-ide-plugin.xml
|
||||
/node_modules/
|
||||
moodle-plugin-ci.phar
|
||||
|
||||
+1
-1
@@ -90,4 +90,4 @@ theme/boost/amd/src/bootstrap/toast.js
|
||||
theme/boost/amd/src/bootstrap/tooltip.js
|
||||
theme/boost/amd/src/bootstrap/util.js
|
||||
theme/boost/amd/src/tether.js
|
||||
theme/boost/scss/fontawesome/
|
||||
theme/boost/scss/fontawesome/
|
||||
|
||||
+7
-1
@@ -10,7 +10,7 @@ language: php
|
||||
|
||||
os: linux
|
||||
|
||||
dist: xenial
|
||||
dist: bionic
|
||||
|
||||
services:
|
||||
- mysql
|
||||
@@ -19,6 +19,12 @@ services:
|
||||
addons:
|
||||
postgresql: "9.6"
|
||||
|
||||
branches:
|
||||
except:
|
||||
- master
|
||||
- /MOODLE_[0-9]+_STABLE/
|
||||
- /^v[0-9]+\.[0-9]+\.[0-9]+.*/
|
||||
|
||||
jobs:
|
||||
# Enable fast finish.
|
||||
# This will fail the build if a single job fails (except those in allow_failures).
|
||||
|
||||
+2
-2
@@ -424,7 +424,7 @@ module.exports = function(grunt) {
|
||||
'*/**/yui/src/*/meta/',
|
||||
'*/**/build/',
|
||||
].concat(thirdPartyPaths);
|
||||
grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
|
||||
grunt.file.write('.eslintignore', eslintIgnores.join('\n') + '\n');
|
||||
|
||||
// Generate .stylelintignore.
|
||||
const stylelintIgnores = [
|
||||
@@ -433,7 +433,7 @@ module.exports = function(grunt) {
|
||||
'theme/boost/style/moodle.css',
|
||||
'theme/classic/style/moodle.css',
|
||||
].concat(thirdPartyPaths);
|
||||
grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
|
||||
grunt.file.write('.stylelintignore', stylelintIgnores.join('\n') + '\n');
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ MOODLE TRADEMARKS POLICY
|
||||
========================
|
||||
|
||||
The word "Moodle" is trademarked in many countries around the world. The word
|
||||
was originally an acronym: Modular Object-Oriented Dynamic Learning
|
||||
was originally coined as an acronym: Modular Object-Oriented Dynamic Learning
|
||||
Environment. The trademark is owned by Martin Dougiamas, Founder and Director
|
||||
of Moodle.
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Fix orphaned calendar events that were broken by MDL-67494.
|
||||
*
|
||||
* This script will look for all the calendar events which userids
|
||||
* where broken by a wrong upgrade step, affecting to Moodle 3.9.5
|
||||
* and up.
|
||||
*
|
||||
* It performs checks to both:
|
||||
* a) Detect if the site was affected (ran the wrong upgrade step).
|
||||
* b) Look for orphaned calendar events, categorising them as:
|
||||
* - standard: site / category / course / group / user events
|
||||
* - subscription: events created via subscriptions.
|
||||
* - action: normal action events, created to show common important dates.
|
||||
* - override: user and group override events, particular, that some activities support.
|
||||
* - custom: other events, not being any of the above, common or particular.
|
||||
* By specifying it (--fix) try to recover as many broken events (missing userid) as
|
||||
* possible. Standard, subscription, action, override events in core are fully supported but
|
||||
* override or custom events should be fixed by each plugin as far as there isn't any standard
|
||||
* API (plugin-wise) to launch a rebuild of the calendar events.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2021 onwards Simey Lameze <simey@moodle.com>
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define('CLI_SCRIPT', true);
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->libdir . "/clilib.php");
|
||||
require_once($CFG->libdir . '/db/upgradelib.php');
|
||||
|
||||
// Supported options.
|
||||
$long = ['fix' => false, 'help' => false];
|
||||
$short = ['f' => 'fix', 'h' => 'help'];
|
||||
|
||||
// CLI options.
|
||||
[$options, $unrecognized] = cli_get_params($long, $short);
|
||||
|
||||
if ($unrecognized) {
|
||||
$unrecognized = implode("\n ", $unrecognized);
|
||||
cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
|
||||
}
|
||||
|
||||
if ($options['help']) {
|
||||
$help = <<<EOT
|
||||
Fix orphaned calendar events.
|
||||
|
||||
This script detects calendar events that have had their
|
||||
userid lost. By default it will perform various checks
|
||||
and report them, showing the site status in an easy way.
|
||||
|
||||
Also, optionally (--fix), it wil try to recover as many
|
||||
lost userids as possible from different sources. Note that
|
||||
this script aims to process well-know events in core,
|
||||
leaving custom events in 3rd part plugins mostly unmodified
|
||||
because there isn't any consistent way to regenerate them.
|
||||
|
||||
For more details: https://tracker.moodle.org/browse/MDL-71156
|
||||
|
||||
Options:
|
||||
-h, --help Print out this help.
|
||||
-f, --fix Fix the orphaned calendar events in the DB.
|
||||
If not specified only check and report problems to STDERR.
|
||||
|
||||
Usage:
|
||||
- Only report: \$ sudo -u www-data /usr/bin/php admin/cli/fix_orphaned_calendar_events.php
|
||||
- Report and fix: \$ sudo -u www-data /usr/bin/php admin/cli/fix_orphaned_calendar_events.php -f
|
||||
EOT;
|
||||
|
||||
cli_writeln($help);
|
||||
die;
|
||||
}
|
||||
|
||||
// Check various usual pre-requisites.
|
||||
if (empty($CFG->version)) {
|
||||
cli_error('Database is not yet installed.');
|
||||
}
|
||||
|
||||
$admin = get_admin();
|
||||
if (!$admin) {
|
||||
cli_error('Error: No admin account was found.');
|
||||
}
|
||||
|
||||
if (moodle_needs_upgrading()) {
|
||||
cli_error('Moodle upgrade pending, script execution suspended.');
|
||||
}
|
||||
|
||||
// Do everything as admin by default.
|
||||
\core\session\manager::set_user($admin);
|
||||
|
||||
// Report current site status.
|
||||
cli_heading('Checking the site status');
|
||||
$needsfix = upgrade_calendar_site_status();
|
||||
|
||||
// Report current calendar events status.
|
||||
cli_heading('Checking the calendar events status');
|
||||
$info = upgrade_calendar_events_status();
|
||||
$hasbadevents = $info['total']->bad > 0 || $info['total']->bad != $info['other']->bad;
|
||||
$needsfix = $needsfix || $hasbadevents;
|
||||
|
||||
// If, selected, fix as many calendar events as possible.
|
||||
if ($options['fix']) {
|
||||
|
||||
// If the report has told us that the fix was not needed... ask for confirmation!
|
||||
if (!$needsfix) {
|
||||
cli_writeln("This site DOES NOT NEED to run the calendar events fix.");
|
||||
$input = cli_input('Are you completely sure that you want to run the fix? (y/N)', 'N', ['y', 'Y', 'n', 'N']);
|
||||
if (strtolower($input) != 'y') {
|
||||
exit(0);
|
||||
}
|
||||
cli_writeln("");
|
||||
}
|
||||
cli_heading('Fixing as many as possible calendar events');
|
||||
upgrade_calendar_events_fix_remaining($info);
|
||||
// Report current (after fix) calendar events status.
|
||||
cli_heading('Checking the calendar events status (after fix)');
|
||||
upgrade_calendar_events_status();
|
||||
} else if ($needsfix) {
|
||||
// Fix option was not provided but problem events have been found. Notify the user and provide info how to fix these events.
|
||||
cli_writeln("This site NEEDS to run the calendar events fix!");
|
||||
cli_writeln("To fix the calendar events, re-run this script with the --fix option.");
|
||||
}
|
||||
@@ -119,7 +119,7 @@ if (!empty($options['collation'])) {
|
||||
if ($dbcollation->value !== $collation || $dbcharset->value !== $charset) {
|
||||
// Try to convert the DB.
|
||||
echo "Converting database to '$collation' for $CFG->wwwroot:\n";
|
||||
$sql = "ALTER DATABASE $CFG->dbname DEFAULT CHARACTER SET $charset DEFAULT COLLATE = $collation";
|
||||
$sql = "ALTER DATABASE `$CFG->dbname` DEFAULT CHARACTER SET $charset DEFAULT COLLATE = $collation";
|
||||
try {
|
||||
$DB->change_database_structure($sql);
|
||||
} catch (exception $e) {
|
||||
@@ -146,7 +146,7 @@ if (!empty($options['collation'])) {
|
||||
|
||||
} else {
|
||||
try {
|
||||
$DB->change_database_structure("ALTER TABLE $table->name CONVERT TO CHARACTER SET $charset COLLATE $collation");
|
||||
$DB->change_database_structure("ALTER TABLE `$table->name` CONVERT TO CHARACTER SET $charset COLLATE $collation");
|
||||
echo "CONVERTED\n";
|
||||
$converted++;
|
||||
} catch (ddl_exception $e) {
|
||||
@@ -163,7 +163,7 @@ if (!empty($options['collation'])) {
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "SHOW FULL COLUMNS FROM $table->name WHERE collation IS NOT NULL";
|
||||
$sql = "SHOW FULL COLUMNS FROM `$table->name` WHERE collation IS NOT NULL";
|
||||
$rs2 = $DB->get_recordset_sql($sql);
|
||||
foreach ($rs2 as $column) {
|
||||
$column = (object)array_change_key_case((array)$column, CASE_LOWER);
|
||||
@@ -181,7 +181,7 @@ if (!empty($options['collation'])) {
|
||||
$notnull = ($column->null === 'NO') ? 'NOT NULL' : 'NULL';
|
||||
$default = (!is_null($column->default) and $column->default !== '') ? "DEFAULT '$column->default'" : '';
|
||||
// primary, unique and inc are not supported for texts
|
||||
$sql = "ALTER TABLE $table->name
|
||||
$sql = "ALTER TABLE `$table->name`
|
||||
MODIFY COLUMN $column->field $column->type
|
||||
CHARACTER SET $charset
|
||||
COLLATE $collation $notnull $default";
|
||||
@@ -192,11 +192,11 @@ if (!empty($options['collation'])) {
|
||||
$default = !is_null($column->default) ? "DEFAULT '$column->default'" : '';
|
||||
|
||||
if ($rowformat != '') {
|
||||
$sql = "ALTER TABLE $table->name $rowformat";
|
||||
$sql = "ALTER TABLE `$table->name` $rowformat";
|
||||
$DB->change_database_structure($sql);
|
||||
}
|
||||
|
||||
$sql = "ALTER TABLE $table->name
|
||||
$sql = "ALTER TABLE `$table->name`
|
||||
MODIFY COLUMN $column->field $column->type
|
||||
CHARACTER SET $charset
|
||||
COLLATE $collation $notnull $default";
|
||||
@@ -315,8 +315,8 @@ function mysql_set_row_format($tablename, $charset, $collation, $engine) {
|
||||
if ($rs->row_format == 'Compact' || $rs->row_format == 'Redundant') {
|
||||
$rowformat = $DB->get_row_format_sql($engine, $collation);
|
||||
// Try to convert to compressed format and then try updating the collation again.
|
||||
$DB->change_database_structure("ALTER TABLE $tablename $rowformat");
|
||||
$DB->change_database_structure("ALTER TABLE $tablename CONVERT TO CHARACTER SET $charset COLLATE $collation");
|
||||
$DB->change_database_structure("ALTER TABLE `$tablename` $rowformat");
|
||||
$DB->change_database_structure("ALTER TABLE `$tablename` CONVERT TO CHARACTER SET $charset COLLATE $collation");
|
||||
} else {
|
||||
// Row format may not be the problem. Can not diagnose problem. Send fail reply.
|
||||
return false;
|
||||
|
||||
@@ -158,7 +158,7 @@ if (!empty($options['info'])) {
|
||||
}
|
||||
|
||||
foreach ($fixtables as $table) {
|
||||
$DB->change_database_structure("ALTER TABLE {$prefix}$table ROW_FORMAT=Compressed");
|
||||
$DB->change_database_structure("ALTER TABLE `{$prefix}$table` ROW_FORMAT=Compressed");
|
||||
echo str_pad($prefix . $table, 32, ' ', STR_PAD_RIGHT) . " ... Compressed\n";
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ if (!empty($options['info'])) {
|
||||
echo "SET GLOBAL innodb_file_per_table=1;\n";
|
||||
echo "SET GLOBAL innodb_file_format=Barracuda;\n";
|
||||
foreach ($fixtables as $table) {
|
||||
echo "ALTER TABLE {$prefix}$table ROW_FORMAT=Compressed;\n";
|
||||
echo "ALTER TABLE `{$prefix}$table` ROW_FORMAT=Compressed;\n";
|
||||
}
|
||||
echo "\n";
|
||||
exit(0);
|
||||
|
||||
+188
-1
@@ -3447,7 +3447,7 @@
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="optional"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -3527,6 +3527,193 @@
|
||||
<ON_CHECK message="sixtyfourbitswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.0" requires="3.6">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="10.2.29" />
|
||||
<VENDOR name="mysql" version="5.7" />
|
||||
<VENDOR name="postgres" version="10" />
|
||||
<VENDOR name="mssql" version="14.0" />
|
||||
<VENDOR name="oracle" version="11.2" />
|
||||
</DATABASE>
|
||||
<PHP version="7.3.0" level="required">
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="pcreunicodewarning" />
|
||||
</FEEDBACK>
|
||||
</PCREUNICODE>
|
||||
<PHP_EXTENSIONS>
|
||||
<PHP_EXTENSION name="iconv" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="iconvrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="mbstring" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="mbstringrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="curl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="curlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="openssl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="opensslrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="tokenizer" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlrpc" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="xmlrpcrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="ctype" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ctyperequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zip" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ziprequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zlib" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="gd" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="gdrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="simplexml" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="simplexmlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="spl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="splrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="pcre" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="dom" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xml" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlreader" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="intl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="intlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="json" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="optional"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="settingmemorylimit" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="file_uploads" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingfileuploads" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="opcache.enable" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="opcacherecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
</PHP_SETTINGS>
|
||||
<CUSTOM_CHECKS>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbstorageengine" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="quizattemptsupgradedmessage" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="slashargumentswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unsupporteddbtablerowformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unoconvwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="libcurlwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfileformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfilepertable" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddblargeprefix" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="ishttpswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="incompleteunicodesupport" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="sixtyfourbitswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
@@ -45,7 +45,7 @@ list($options, $unrecognised) = cli_get_params(
|
||||
array(
|
||||
'stop-on-failure' => 0,
|
||||
'verbose' => false,
|
||||
'replace' => false,
|
||||
'replace' => '',
|
||||
'help' => false,
|
||||
'tags' => '',
|
||||
'profile' => '',
|
||||
@@ -70,7 +70,7 @@ $help = "
|
||||
Behat utilities to run behat tests in parallel
|
||||
|
||||
Usage:
|
||||
php run.php [--BEHAT_OPTION=\"value\"] [--feature=\"value\"] [--replace] [--fromrun=value --torun=value] [--help]
|
||||
php run.php [--BEHAT_OPTION=\"value\"] [--feature=\"value\"] [--replace=\"{run}\"] [--fromrun=value --torun=value] [--help]
|
||||
|
||||
Options:
|
||||
--BEHAT_OPTION Any combination of behat option specified in http://behat.readthedocs.org/en/v2.5/guides/6.cli.html
|
||||
@@ -144,9 +144,10 @@ $extraopts = $unrecognised;
|
||||
if ($options['profile']) {
|
||||
$profile = $options['profile'];
|
||||
|
||||
// If profile passed is not set, then exit.
|
||||
// If profile passed is not set, then exit (note we skip if the 'replace' option is found within the 'profile' value).
|
||||
if (!isset($CFG->behat_config[$profile]) && !isset($CFG->behat_profiles[$profile]) &&
|
||||
!(isset($options['replace']) && (strpos($options['profile'], $options['replace']) >= 0 ))) {
|
||||
!($options['replace'] && (strpos($profile, (string) $options['replace']) !== false))) {
|
||||
|
||||
echo "Invalid profile passed: " . $profile . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -21,5 +21,5 @@ Feature: Transform date time string arguments
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Test assignment name"
|
||||
And I should see "##yesterday##l, j F Y##"
|
||||
And I should see "##yesterday##%A, %d %B %Y##"
|
||||
And I log out
|
||||
|
||||
@@ -48,7 +48,7 @@ Feature: Verify that all form fields values can be get and set
|
||||
And the "Unenrol users" select box should not contain "Baker"
|
||||
And the "Unenrol users" select box should not contain "President, Baker"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I follow "Test this one"
|
||||
And I am on the "Test this one" "wiki activity" page
|
||||
And I press "Create page"
|
||||
# Text (textarea & editor) & Select (multi-select) - Checking "I set the following fields to these values".
|
||||
When I set the following fields to these values:
|
||||
@@ -61,8 +61,7 @@ Feature: Verify that all form fields values can be get and set
|
||||
# Checkbox - Checking "I set the field" and "The field matches value" ticked.
|
||||
And I set the field "Force format" to "1"
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I am on the "Test this one" "wiki activity" page
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I expand all fieldsets
|
||||
And the field "Force format" matches value "1"
|
||||
@@ -70,8 +69,7 @@ Feature: Verify that all form fields values can be get and set
|
||||
# Checkbox - Checking "I set the field" and "The field matches value" unticked.
|
||||
And I set the field "Force format" to ""
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I am on the "Test this one" "wiki activity" page
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I expand all fieldsets
|
||||
And the field "Force format" matches value ""
|
||||
@@ -80,8 +78,7 @@ Feature: Verify that all form fields values can be get and set
|
||||
And I set the following fields to these values:
|
||||
| Force format | 1 |
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I am on the "Test this one" "wiki activity" page
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I expand all fieldsets
|
||||
And the following fields match these values:
|
||||
@@ -92,8 +89,7 @@ Feature: Verify that all form fields values can be get and set
|
||||
And I set the following fields to these values:
|
||||
| Force format | |
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I am on the "Test this one" "wiki activity" page
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I expand all fieldsets
|
||||
And the following fields match these values:
|
||||
@@ -115,20 +111,21 @@ Feature: Verify that all form fields values can be get and set
|
||||
| Wiki name | Test this one baby |
|
||||
| Default format | HTML |
|
||||
And I press "Cancel"
|
||||
And I am on "Course 1" course homepage
|
||||
# Radio - Checking "I set the field" and "the field matches value".
|
||||
And I add a "Choice" to section "1" and I fill the form with:
|
||||
| Choice name | Test choice name |
|
||||
| Description | Test choice description |
|
||||
| Allow choice to be updated | Yes |
|
||||
| Option 1 | one |
|
||||
| Option 2 | two |
|
||||
| Option 3 | three |
|
||||
And I follow "Test choice name"
|
||||
And I set the field "one" to "1"
|
||||
And the following "activity" exists:
|
||||
| activity | choice |
|
||||
| course | C1 |
|
||||
| idnumber | choice1 |
|
||||
| intro | Test choice description |
|
||||
| name | Test choice name |
|
||||
| option | Option 1, Option 2, Option 3 |
|
||||
| section | 1 |
|
||||
| allowupdate | 1 |
|
||||
And I am on the "Test choice name" "choice activity" page
|
||||
And I set the field "Option 1" to "1"
|
||||
And I press "Save my choice"
|
||||
And the field "one" matches value "1"
|
||||
And the field "two" matches value ""
|
||||
And the field "Option 1" matches value "1"
|
||||
And the field "Option 2" matches value ""
|
||||
# Check if field xpath set/match works.
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
@@ -174,31 +171,23 @@ Feature: Verify that all form fields values can be get and set
|
||||
And I press "Cancel"
|
||||
# Advanced checkbox requires real browser to allow uncheck to work. MDL-58681. MDL-55386.
|
||||
# Advanced checkbox - Checking "I set the field" and "The field matches value" ticked.
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Test this one"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test choice name" "choice activity editing" page
|
||||
And I set the field "Display description on course page" to "1"
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test choice name" "choice activity editing" page
|
||||
And the field "Display description on course page" matches value "1"
|
||||
And the field "Display description on course page" does not match value ""
|
||||
# Advanced checkbox - Checking "I set the field" and "The field matches value" unticked.
|
||||
And I set the field "Display description on course page" to ""
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test choice name" "choice activity editing" page
|
||||
And the field "Display description on course page" matches value ""
|
||||
And the field "Display description on course page" does not match value "1"
|
||||
# Advanced checkbox - Checking "I set the following fields to these values:" and "The following fields match these values" ticked.
|
||||
And I set the following fields to these values:
|
||||
| Display description on course page | 1 |
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test choice name" "choice activity editing" page
|
||||
And the following fields match these values:
|
||||
| Display description on course page | 1 |
|
||||
And the following fields do not match these values:
|
||||
@@ -207,9 +196,7 @@ Feature: Verify that all form fields values can be get and set
|
||||
And I set the following fields to these values:
|
||||
| Display description on course page | |
|
||||
And I press "Save and return to course"
|
||||
And I should see "Test this one"
|
||||
And I follow "Test this one"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test choice name" "choice activity editing" page
|
||||
And the following fields match these values:
|
||||
| Display description on course page | |
|
||||
And the following fields do not match these values:
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
@tool @tool_behat
|
||||
Feature: Use core page resolvers for the I am on the page steps
|
||||
In order to write tests correctly
|
||||
As a developer
|
||||
I need to have steps which take me straight to a page
|
||||
|
||||
Scenario Outline: When I am on an instance
|
||||
Given the following "categories" exist:
|
||||
| name | category | idnumber |
|
||||
| Cat 1 | 0 | CAT1 |
|
||||
And the following "course" exists:
|
||||
| fullname | Economics 101 |
|
||||
| shortname | ECON101 |
|
||||
| idnumber | 2021-econ101 |
|
||||
And the following "activity" exists:
|
||||
| course | ECON101 |
|
||||
| activity | forum |
|
||||
| name | Fundamentals of Economics |
|
||||
| idnumber | fundamentalsofeconomics |
|
||||
And I log in as "admin"
|
||||
When I am on the <identifier> <type> page
|
||||
Then I should see "<shouldsee>"
|
||||
|
||||
Examples:
|
||||
| description | identifier | type | shouldsee |
|
||||
| Course Category idnumber | CAT1 | category | Add a new course |
|
||||
| Course Category name | "Cat 1" | Category | Add a new course |
|
||||
| Course Full name | "Economics 101" | course | Fundamentals of Economics |
|
||||
| Course Short name | ECON101 | COURSE | Fundamentals of Economics |
|
||||
| Course idnumber | "2021-econ101" | Course | Fundamentals of Economics |
|
||||
| Forum idnumber | fundamentalsofeconomics | Activity | Add a new discussion |
|
||||
| Generic activity editing | fundamentalsofeconomics | "Activity editing" | Updating: Forum |
|
||||
| Forum name | "Fundamentals of Economics" | "Forum activity" | Add a new discussion |
|
||||
| Forum name editing | "Fundamentals of Economics" | "Forum activity editing" | Updating: Forum |
|
||||
| Forum name permissions | "Fundamentals of Economics" | "Forum activity permissions" | Permissions in Forum: Fun |
|
||||
| Forum name roles | "Fundamentals of Economics" | "Forum activity roles" | Assign roles in Forum: Fun |
|
||||
|
||||
Scenario Outline: When I am on an instance logged in as
|
||||
Given the following "categories" exist:
|
||||
| name | category | idnumber |
|
||||
| Cat 1 | 0 | CAT1 |
|
||||
And the following "course" exists:
|
||||
| fullname | Economics 101 |
|
||||
| shortname | ECON101 |
|
||||
| idnumber | 2021-econ101 |
|
||||
And the following "activity" exists:
|
||||
| course | ECON101 |
|
||||
| activity | forum |
|
||||
| name | Fundamentals of Economics |
|
||||
| idnumber | fundamentalsofeconomics |
|
||||
When I am on the <identifier> <type> page logged in as admin
|
||||
Then I should see "<shouldsee>"
|
||||
|
||||
Examples:
|
||||
| description | identifier | type | shouldsee |
|
||||
| Course Category idnumber | CAT1 | category | Add a new course |
|
||||
| Course Category name | "Cat 1" | Category | Add a new course |
|
||||
| Course Full name | "Economics 101" | course | Fundamentals of Economics |
|
||||
| Course Short name | ECON101 | COURSE | Fundamentals of Economics |
|
||||
| Course idnumber | "2021-econ101" | Course | Fundamentals of Economics |
|
||||
| Forum idnumber | fundamentalsofeconomics | Activity | Add a new discussion |
|
||||
| Generic activity editing | fundamentalsofeconomics | "Activity editing" | Updating: Forum |
|
||||
| Forum name | "Fundamentals of Economics" | "Forum activity" | Add a new discussion |
|
||||
| Forum name editing | "Fundamentals of Economics" | "Forum activity editing" | Updating: Forum |
|
||||
| Forum name permissions | "Fundamentals of Economics" | "Forum activity permissions" | Permissions in Forum: Fun |
|
||||
| Forum name roles | "Fundamentals of Economics" | "Forum activity roles" | Assign roles in Forum: Fun |
|
||||
|
||||
Scenario Outline: When I am on a named page
|
||||
Given I log in as "admin"
|
||||
When I am on the <identifier> page
|
||||
Then I should see "<shouldsee>"
|
||||
|
||||
Examples:
|
||||
| description | identifier | shouldsee |
|
||||
| Admin page | "Admin notifications" | Check for available updates |
|
||||
| Home page | Homepage | Recently accessed courses |
|
||||
|
||||
Scenario Outline: When I am on a named page logged in as
|
||||
When I am on the <identifier> page logged in as admin
|
||||
Then I should see "<shouldsee>"
|
||||
|
||||
Examples:
|
||||
| description | identifier | shouldsee |
|
||||
| Admin page | "Admin notifications" | Check for available updates |
|
||||
| Home page | Homepage | Recently accessed courses |
|
||||
@@ -189,7 +189,12 @@ class cohort_role_assignments_table extends table_sql {
|
||||
FROM {tool_cohortroles} uca
|
||||
JOIN {user} u ON u.id = uca.userid
|
||||
JOIN {cohort} c ON c.id = uca.cohortid";
|
||||
$params = array();
|
||||
|
||||
// Check if any additional filtering is required.
|
||||
[$sqlwhere, $params] = $this->get_sql_where();
|
||||
if ($sqlwhere) {
|
||||
$sql .= " WHERE {$sqlwhere}";
|
||||
}
|
||||
|
||||
// Add order by if needed.
|
||||
if (!$count && $sqlsort = $this->get_sql_sort()) {
|
||||
|
||||
@@ -86,8 +86,8 @@ if ($removeid) {
|
||||
|
||||
$title = get_string('existingcohortroles', 'tool_cohortroles');
|
||||
echo $output->heading($title);
|
||||
$url = new moodle_url('/admin/tool/cohortroles/index.php');
|
||||
$table = new tool_cohortroles\output\cohort_role_assignments_table(uniqid(), $url);
|
||||
|
||||
$table = new tool_cohortroles\output\cohort_role_assignments_table('cohort-role-assignments', $pageurl);
|
||||
echo $table->out(50, true);
|
||||
|
||||
echo $output->spacer();
|
||||
|
||||
@@ -37,67 +37,68 @@
|
||||
This template does not have an example context because it includes ajax functionality.
|
||||
}}
|
||||
<div data-region="user-competency-full-info" data-node="user-competency" data-competencyid="{{usercompetency.competencyid}}" data-userid="{{usercompetency.userid}}" data-region-id="{{uniqid}}">
|
||||
<div data-region="competency-summary">
|
||||
{{#competency}}
|
||||
{{> tool_lp/competency_summary }}
|
||||
{{/competency}}
|
||||
<dl>
|
||||
{{#usercompetency}}
|
||||
<dt>{{#str}}reviewstatus, tool_lp{{/str}}</dt>
|
||||
<dd data-region="user-competency-status">{{statusname}}
|
||||
<div data-region="competency-summary">
|
||||
{{#competency}}
|
||||
{{> tool_lp/competency_summary }}
|
||||
{{/competency}}
|
||||
<dl>
|
||||
{{#usercompetency}}
|
||||
<dt>{{#str}}reviewstatus, tool_lp{{/str}}</dt>
|
||||
<dd data-region="user-competency-status">{{statusname}}
|
||||
|
||||
{{#isstatusinreview}}
|
||||
- {{reviewer.fullname}}
|
||||
{{/isstatusinreview}}
|
||||
{{#isstatusinreview}}
|
||||
- {{reviewer.fullname}}
|
||||
{{/isstatusinreview}}
|
||||
|
||||
{{#isrequestreviewallowed}}<button class="btn btn-secondary" data-action="request-review">{{#str}}requestreview, tool_lp{{/str}}</button>{{/isrequestreviewallowed}}
|
||||
{{#iscancelreviewrequestallowed}}<button class="btn btn-secondary" data-action="cancel-review-request">{{#str}}cancelreviewrequest, tool_lp{{/str}}</button>{{/iscancelreviewrequestallowed}}
|
||||
{{#isstartreviewallowed}}<button class="btn btn-secondary" data-action="start-review">{{#str}}startreview, tool_lp{{/str}}</button>{{/isstartreviewallowed}}
|
||||
{{#isstopreviewallowed}}<button class="btn btn-secondary" data-action="stop-review">{{#str}}stopreview, tool_lp{{/str}}</button>{{/isstopreviewallowed}}
|
||||
</dd>
|
||||
<dt>{{#str}}proficient, tool_lp{{/str}}</dt>
|
||||
<dd>
|
||||
<span class="badge {{#proficiency}}badge-success{{/proficiency}}{{^proficiency}}badge-danger{{/proficiency}} float-left">
|
||||
{{proficiencyname}}
|
||||
</span>
|
||||
</dd>
|
||||
<dt>{{#str}}rating, tool_lp{{/str}}</dt>
|
||||
<dd>{{gradename}}
|
||||
{{#cangrade}}
|
||||
<button class="btn btn-secondary" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
|
||||
{{/cangrade}}
|
||||
</dd>
|
||||
{{#js}}
|
||||
require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info', 'tool_lp/user_competency_workflow'], function($, mod, info, UserCompWorkflow) {
|
||||
{{#isrequestreviewallowed}}<button class="btn btn-secondary" data-action="request-review">{{#str}}requestreview, tool_lp{{/str}}</button>{{/isrequestreviewallowed}}
|
||||
{{#iscancelreviewrequestallowed}}<button class="btn btn-secondary" data-action="cancel-review-request">{{#str}}cancelreviewrequest, tool_lp{{/str}}</button>{{/iscancelreviewrequestallowed}}
|
||||
{{#isstartreviewallowed}}<button class="btn btn-secondary" data-action="start-review">{{#str}}startreview, tool_lp{{/str}}</button>{{/isstartreviewallowed}}
|
||||
{{#isstopreviewallowed}}<button class="btn btn-secondary" data-action="stop-review">{{#str}}stopreview, tool_lp{{/str}}</button>{{/isstopreviewallowed}}
|
||||
</dd>
|
||||
<dt>{{#str}}proficient, tool_lp{{/str}}</dt>
|
||||
<dd>
|
||||
<span class="badge {{#proficiency}}badge-success{{/proficiency}}{{^proficiency}}badge-danger{{/proficiency}} float-left">
|
||||
{{proficiencyname}}
|
||||
</span>
|
||||
</dd>
|
||||
<dt>{{#str}}rating, tool_lp{{/str}}</dt>
|
||||
<dd>{{gradename}}
|
||||
{{#cangrade}}
|
||||
<button class="btn btn-secondary" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
|
||||
{{/cangrade}}
|
||||
</dd>
|
||||
{{#js}}
|
||||
require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info', 'tool_lp/user_competency_workflow'], function($, mod, info, UserCompWorkflow) {
|
||||
|
||||
var competencyElement = $('[data-region-id="{{uniqid}}"]');
|
||||
var infoReloader = new info(competencyElement, '{{competency.competency.id}}', '{{user.id}}');
|
||||
var competencyElement = $('[data-region-id="{{uniqid}}"]');
|
||||
var infoReloader = new info(competencyElement, '{{competency.competency.id}}', '{{user.id}}');
|
||||
|
||||
var ucw = new UserCompWorkflow();
|
||||
ucw.registerEvents('[data-region="user-competency-status"]');
|
||||
ucw.on('status-changed', infoReloader.reload.bind(infoReloader));
|
||||
ucw.on('error-occured', infoReloader.reload.bind(infoReloader));
|
||||
var ucw = new UserCompWorkflow();
|
||||
ucw.registerEvents('[data-region="user-competency-status"]');
|
||||
ucw.on('status-changed', infoReloader.reload.bind(infoReloader));
|
||||
ucw.on('error-occured', infoReloader.reload.bind(infoReloader));
|
||||
|
||||
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
|
||||
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
|
||||
});
|
||||
{{/js}}
|
||||
{{/usercompetency}}
|
||||
</dl>
|
||||
{{#commentarea}}
|
||||
{{#canpostorhascomments}}
|
||||
{{>tool_lp/comment_area}}
|
||||
{{/canpostorhascomments}}
|
||||
{{/commentarea}}
|
||||
<dl data-region="evidence-listing">
|
||||
<dt>{{#str}}evidence, tool_lp{{/str}}</dt>
|
||||
<dd>
|
||||
{{#evidence}}
|
||||
{{> tool_lp/evidence_summary }}
|
||||
{{/evidence}}
|
||||
{{^evidence}}
|
||||
<p>{{#str}}noevidence, tool_lp{{/str}}</p>
|
||||
{{/evidence}}
|
||||
</dd>
|
||||
</dl>
|
||||
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
|
||||
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
|
||||
});
|
||||
{{/js}}
|
||||
{{/usercompetency}}
|
||||
</dl>
|
||||
{{#commentarea}}
|
||||
{{#canpostorhascomments}}
|
||||
{{>tool_lp/comment_area}}
|
||||
{{/canpostorhascomments}}
|
||||
{{/commentarea}}
|
||||
<dl data-region="evidence-listing">
|
||||
<dt>{{#str}}evidence, tool_lp{{/str}}</dt>
|
||||
<dd>
|
||||
{{#evidence}}
|
||||
{{> tool_lp/evidence_summary }}
|
||||
{{/evidence}}
|
||||
{{^evidence}}
|
||||
<p>{{#str}}noevidence, tool_lp{{/str}}</p>
|
||||
{{/evidence}}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,9 +24,7 @@ Feature: See the competencies for an activity on the course competencies page.
|
||||
| activity | name | intro | course | idnumber | completion | completionview |
|
||||
| page | PageName1 | PageDesc1 | C1 | PAGE1 | 1 | 1 |
|
||||
| page | PageName2 | PageDesc2 | C1 | PAGE2 | 1 | 1 |
|
||||
And I log in as "admin"
|
||||
And I am on site homepage
|
||||
And I follow "Course 1"
|
||||
And I am on the "Course 1" course page logged in as admin
|
||||
And I follow "Competencies"
|
||||
And I press "Add competencies to course"
|
||||
And "Competency picker" "dialogue" should be visible
|
||||
@@ -36,9 +34,7 @@ Feature: See the competencies for an activity on the course competencies page.
|
||||
And "Competency picker" "dialogue" should be visible
|
||||
And I select "Test-Comp2" of the competency tree
|
||||
And I click on "Add" "button" in the "Competency picker" "dialogue"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the PageName1 "page activity editing" page
|
||||
And I follow "Expand all"
|
||||
And I set the field "Course competencies" to "Test-Comp1"
|
||||
And I press "Save and return to course"
|
||||
@@ -46,10 +42,8 @@ Feature: See the competencies for an activity on the course competencies page.
|
||||
|
||||
@javascript
|
||||
Scenario: Go to the competency course competencies page.
|
||||
When I log in as "student1"
|
||||
And I am on site homepage
|
||||
And I follow "Course 1"
|
||||
And I follow "Competencies"
|
||||
Given I am on the "Course 1" course page logged in as student1
|
||||
When I follow "Competencies"
|
||||
Then I should see "Test-Comp1"
|
||||
And I should see "Test-Comp2"
|
||||
And I set the field "Filter competencies by resource or activity" to "PageName1"
|
||||
@@ -64,12 +58,5 @@ Feature: See the competencies for an activity on the course competencies page.
|
||||
|
||||
@javascript
|
||||
Scenario: None course competencies page.
|
||||
When I log in as "student1"
|
||||
And I am on site homepage
|
||||
And I follow "Course 1"
|
||||
And I follow "PageName1"
|
||||
Then I should see "Test page content"
|
||||
And I am on site homepage
|
||||
And I follow "Course 1"
|
||||
And I follow "PageName1"
|
||||
When I am on the PageName1 "page activity" page logged in as student1
|
||||
Then I should see "Test page content"
|
||||
|
||||
@@ -42,9 +42,7 @@ Feature: Backup user data
|
||||
| Feedback for the response 'True'. | So you think it is true |
|
||||
| Feedback for the response 'False'. | So you think it is false |
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Quiz 1"
|
||||
When I am on the "Quiz 1" "quiz activity" page logged in as student1
|
||||
And I press "Attempt quiz now"
|
||||
And I click on "True" "radio" in the "First question" "question"
|
||||
And I click on "False" "radio" in the "Second question" "question"
|
||||
|
||||
@@ -132,12 +132,12 @@ Feature: Upload users
|
||||
# Boost check.
|
||||
And I log in as "jonest"
|
||||
And I am on "Maths" course homepage
|
||||
And "Turn editing on" "button" should not exist
|
||||
And "Calendar" "link" should be visible
|
||||
And I log out
|
||||
# Classic check.
|
||||
And I log in as "reznor"
|
||||
And I am on "Maths" course homepage
|
||||
And "Turn editing on" "button" should exist
|
||||
And "Calendar" "link" should not be visible
|
||||
|
||||
@javascript
|
||||
Scenario: Upload users setting their user theme when allowuserthemes is false
|
||||
|
||||
@@ -438,17 +438,7 @@ class manager {
|
||||
$filename = 'tour_export_' . $tour->get_id() . '_' . time() . '.json';
|
||||
|
||||
// Force download.
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
|
||||
header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
|
||||
header('Expires: ' . gmdate('D, d M Y H:i:s', 0) . 'GMT');
|
||||
header('Pragma: no-cache');
|
||||
header('Accept-Ranges: none');
|
||||
header('Content-disposition: attachment; filename=' . $filename);
|
||||
header('Content-length: ' . strlen($exportstring));
|
||||
header('Content-type: text/calendar; charset=utf-8');
|
||||
|
||||
echo $exportstring;
|
||||
die;
|
||||
send_file($exportstring, $filename, 0, 0, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,7 +68,9 @@ if ($dataformat) {
|
||||
$downloadusers = new ArrayObject($SESSION->bulk_users);
|
||||
$iterator = $downloadusers->getIterator();
|
||||
|
||||
\core\dataformat::download_data($filename, $dataformat, $fields, $iterator, function($userid) use ($extrafields, $fields) {
|
||||
\core\dataformat::download_data($filename, $dataformat, $fields, $iterator, function($userid, $supportshtml)
|
||||
use ($extrafields, $fields) {
|
||||
|
||||
global $DB;
|
||||
|
||||
if (!$user = $DB->get_record('user', array('id' => $userid))) {
|
||||
@@ -86,6 +88,8 @@ if ($dataformat) {
|
||||
// We only take the text.
|
||||
if (is_array($user->$field)) {
|
||||
$userprofiledata[$field] = reset($user->$field);
|
||||
} else if ($supportshtml) {
|
||||
$userprofiledata[$field] = s($user->$field);
|
||||
} else {
|
||||
$userprofiledata[$field] = $user->$field;
|
||||
}
|
||||
|
||||
@@ -178,6 +178,33 @@ class prediction {
|
||||
\core\event\prediction_action_started::create($eventdata)->trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the executed actions.
|
||||
*
|
||||
* Actions could be filtered by actionname.
|
||||
*
|
||||
* @param array $actionnamefilter Limit the results obtained to this list of action names.
|
||||
* @param int $userid the user id. Current user by default.
|
||||
* @return array of actions.
|
||||
*/
|
||||
public function get_executed_actions(array $actionnamefilter = null, int $userid = 0): array {
|
||||
global $USER, $DB;
|
||||
|
||||
$conditions[] = "predictionid = :predictionid";
|
||||
$params['predictionid'] = $this->get_prediction_data()->id;
|
||||
if (!$userid) {
|
||||
$userid = $USER->id;
|
||||
}
|
||||
$conditions[] = "userid = :userid";
|
||||
$params['userid'] = $userid;
|
||||
if ($actionnamefilter) {
|
||||
list($actionsql, $actionparams) = $DB->get_in_or_equal($actionnamefilter, SQL_PARAMS_NAMED);
|
||||
$conditions[] = "actionname $actionsql";
|
||||
$params = $params + $actionparams;
|
||||
}
|
||||
return $DB->get_records_select('analytics_prediction_actions', implode(' AND ', $conditions), $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* format_calculations
|
||||
*
|
||||
|
||||
@@ -112,6 +112,86 @@ class analytics_prediction_actions_testcase extends advanced_testcase {
|
||||
$this->assertEquals(2, $DB->count_records('analytics_prediction_actions'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_get_executed_actions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function execute_actions_provider(): array {
|
||||
return [
|
||||
'Empty actions with no filter' => [
|
||||
[],
|
||||
[],
|
||||
0
|
||||
],
|
||||
'Empty actions with filter' => [
|
||||
[],
|
||||
[\core_analytics\prediction::ACTION_FIXED],
|
||||
0
|
||||
],
|
||||
'Multiple actions with no filter' => [
|
||||
[
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
|
||||
],
|
||||
[],
|
||||
3
|
||||
],
|
||||
'Multiple actions applying filter' => [
|
||||
[
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
|
||||
],
|
||||
[\core_analytics\prediction::ACTION_FIXED],
|
||||
2
|
||||
],
|
||||
'Multiple actions not applying filter' => [
|
||||
[
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
|
||||
],
|
||||
[\core_analytics\prediction::ACTION_NOT_APPLICABLE],
|
||||
0
|
||||
],
|
||||
'Multiple actions with multiple filter' => [
|
||||
[
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_FIXED,
|
||||
\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
|
||||
],
|
||||
[\core_analytics\prediction::ACTION_FIXED, \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED],
|
||||
3
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for get_executed_actions() function.
|
||||
*
|
||||
* @dataProvider execute_actions_provider
|
||||
* @param array $actionstoexecute An array of actions to execute
|
||||
* @param array $actionnamefilter Actions to filter
|
||||
* @param int $returned Number of actions returned
|
||||
*
|
||||
* @covers \core_analytics\prediction::get_executed_actions
|
||||
*/
|
||||
public function test_get_executed_actions(array $actionstoexecute, array $actionnamefilter, int $returned) {
|
||||
|
||||
$this->setUser($this->teacher2);
|
||||
list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
|
||||
$prediction = reset($predictions);
|
||||
$target = $this->model->get_target();
|
||||
foreach($actionstoexecute as $action) {
|
||||
$prediction->action_executed($action, $target);
|
||||
}
|
||||
|
||||
$filteredactions = $prediction->get_executed_actions($actionnamefilter);
|
||||
$this->assertCount($returned, $filteredactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* test_get_predictions
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
This files describes API changes in analytics sub system,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 3.9.7 ===
|
||||
|
||||
* A new function get_executed_actions() has been added to \core_analytics\prediction class
|
||||
to get all (or filtered by action name) executed actions of a prediction
|
||||
|
||||
=== 3.8 ===
|
||||
|
||||
* "Time-splitting method" have been replaced by "Analysis interval" for the language strings that are
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Description of phpCAS 1.3.8 library import
|
||||
|
||||
* downloaded from http://downloads.jasig.org/cas-clients/php/current/
|
||||
* downloaded source code from https://github.com/apereo/phpCAS/releases
|
||||
@@ -4,7 +4,7 @@
|
||||
<location>CAS</location>
|
||||
<name>CAS</name>
|
||||
<license>Apache</license>
|
||||
<version>1.3.7</version>
|
||||
<version>1.3.8</version>
|
||||
<licenseversion>2.0</licenseversion>
|
||||
</library>
|
||||
</libraries>
|
||||
|
||||
@@ -45,7 +45,7 @@ $string['auth_dbname'] = 'Name of the database itself. Leave empty if using an O
|
||||
$string['auth_dbname_key'] = 'DB name';
|
||||
$string['auth_dbpass'] = 'Password matching the above username';
|
||||
$string['auth_dbpass_key'] = 'Password';
|
||||
$string['auth_dbpasstype'] = '<p>Specify the format that the password field is using.</p> <p>Use \'internal\' if you want the external database to manage usernames and email addresses, but Moodle to manage passwords. If you use \'internal\', you <i>must</i> provide a populated email address field in the external database, and you must execute both admin/cron.php and auth/db/cli/sync_users.php regularly. Moodle will send an email to new users with a temporary password.</p>';
|
||||
$string['auth_dbpasstype'] = '<p>Specify the format that the password field is using.</p> <p>Use \'internal\' if you want the external database to manage usernames and email addresses, but Moodle to manage passwords. If you use \'internal\', you must provide a populated email address field in the external database, and you must enable the \auth_db\task\sync_users scheduled task. Moodle will send an email to new users with a temporary password.</p>';
|
||||
$string['auth_dbpasstype_key'] = 'Password format';
|
||||
$string['auth_dbreviveduser'] = 'Revived user {$a->name} id {$a->id}';
|
||||
$string['auth_dbrevivedusererror'] = 'Error reviving user {$a}';
|
||||
|
||||
@@ -75,7 +75,7 @@ $string['auth_ldap_passwdexpire_settings'] = 'LDAP password expiry settings';
|
||||
$string['auth_ldap_preventpassindb'] = 'Select yes to prevent passwords from being stored in Moodle\'s DB.';
|
||||
$string['auth_ldap_preventpassindb_key'] = 'Prevent password caching';
|
||||
$string['auth_ldap_rolecontext'] = '{$a->localname} context';
|
||||
$string['auth_ldap_rolecontext_help'] = 'LDAP context used to select for <i>{$a->localname}</i> mapping. Separate multiple groups with \';\'. Usually something like "cn={$a->shortname},ou=staff,o=myorg".';
|
||||
$string['auth_ldap_rolecontext_help'] = 'LDAP context used to select for <i>{$a->localname}</i> mapping. Separate multiple groups with \';\'. Usually something like "cn={$a->shortname},ou=first-ou-with-role-groups,o=myorg; cn={$a->shortname},ou=second-ou-with-role-groups,o=myorg".';
|
||||
$string['auth_ldap_search_sub'] = 'Search users from subcontexts.';
|
||||
$string['auth_ldap_search_sub_key'] = 'Search subcontexts';
|
||||
$string['auth_ldap_server_settings'] = 'LDAP server settings';
|
||||
|
||||
+1
-3
@@ -716,9 +716,7 @@ class auth_plugin_mnet extends auth_plugin_base {
|
||||
|
||||
foreach($superArray as $subArray) {
|
||||
$subArray = array_values($subArray);
|
||||
$instring = "('".implode("', '",$subArray)."')";
|
||||
$query = "select id, session_id, username from {mnet_session} where username in $instring";
|
||||
$results = $DB->get_records_sql($query);
|
||||
$results = $DB->get_records_list('mnet_session', 'username', $subArray, '', 'id, session_id, username');
|
||||
|
||||
if ($results == false) {
|
||||
// We seem to have a username that breaks our query:
|
||||
|
||||
@@ -113,11 +113,22 @@ class helper {
|
||||
*/
|
||||
private static function unserializesession($serializedstring) {
|
||||
$variables = array();
|
||||
$a = preg_split("/(\w+)\|/", $serializedstring, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
|
||||
$counta = count($a);
|
||||
for ($i = 0; $i < $counta; $i = $i + 2) {
|
||||
$variables[$a[$i]] = unserialize($a[$i + 1]);
|
||||
|
||||
$index = 0;
|
||||
|
||||
// Find next delimiter after current index. It's key being the characters between those points.
|
||||
while ($delimiterpos = strpos($serializedstring, '|', $index)) {
|
||||
$key = substr($serializedstring, $index, $delimiterpos - $index);
|
||||
|
||||
// Start unserializing immediately after the delimiter. PHP will read as much valid data as possible.
|
||||
$value = unserialize(substr($serializedstring, $delimiterpos + 1),
|
||||
['allowed_classes' => ['stdClass']]);
|
||||
$variables[$key] = $value;
|
||||
|
||||
// Advance index beyond the length of the previously captured serialized value.
|
||||
$index = $delimiterpos + 1 + strlen(serialize($value));
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,9 +290,9 @@ abstract class info {
|
||||
*/
|
||||
protected function warn_about_invalid_availability(\coding_exception $e) {
|
||||
$name = $this->get_thing_name();
|
||||
// If it occurs while building modinfo based on somebody calling $cm->name,
|
||||
// we can't get $cm->name, and this line will cause a warning.
|
||||
$htmlname = @$this->format_info($name, $this->course);
|
||||
$htmlname = $this->format_info($name, $this->course);
|
||||
// Because we call format_info here, likely in the middle of building dynamic data for the
|
||||
// activity, there could be a chance that the name might not be available.
|
||||
if ($htmlname === '') {
|
||||
// So instead use the numbers (cmid) from the tag.
|
||||
$htmlname = preg_replace('~[^0-9]~', '', $name);
|
||||
@@ -739,11 +739,11 @@ abstract class info {
|
||||
$info = preg_replace_callback('~<AVAILABILITY_CMNAME_([0-9]+)/>~',
|
||||
function($matches) use($modinfo, $context) {
|
||||
$cm = $modinfo->get_cm($matches[1]);
|
||||
if ($cm->has_view() and $cm->uservisible) {
|
||||
if ($cm->has_view() and $cm->get_user_visible()) {
|
||||
// Help student by providing a link to the module which is preventing availability.
|
||||
return \html_writer::link($cm->url, format_string($cm->name, true, array('context' => $context)));
|
||||
return \html_writer::link($cm->get_url(), format_string($cm->get_name(), true, ['context' => $context]));
|
||||
} else {
|
||||
return format_string($cm->name, true, array('context' => $context));
|
||||
return format_string($cm->get_name(), true, ['context' => $context]);
|
||||
}
|
||||
}, $info);
|
||||
|
||||
|
||||
@@ -48,10 +48,10 @@ Feature: Confirm that conditions on completion no longer cause a bug
|
||||
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
|
||||
And I set the field with xpath "//div[contains(concat(' ', normalize-space(@class), ' '), ' availability-item ')][preceding-sibling::div]//select[@name='cm']" to "Page2"
|
||||
And I press "Save and return to course"
|
||||
And I should see "Not available unless:" in the ".activity.glossary" "css_element"
|
||||
Then I should see "Not available unless:" in the ".activity.glossary" "css_element"
|
||||
And I should see "The activity Page1 is marked complete" in the ".activity.glossary" "css_element"
|
||||
And I should see "The activity Page2 is marked complete" in the ".activity.glossary" "css_element"
|
||||
And I follow "TestGlossary"
|
||||
|
||||
# Behat will automatically check there is no error on this page.
|
||||
Then I should see "TestGlossary"
|
||||
And I am on the TestGlossary "glossary activity" page
|
||||
And I should see "TestGlossary"
|
||||
|
||||
@@ -86,13 +86,12 @@ Feature: availability_grade
|
||||
And I set the field "Minimum grade percentage (inclusive)" to "10"
|
||||
And I press "Save and return to course"
|
||||
|
||||
And I log out
|
||||
|
||||
# Log in as student without a grade yet.
|
||||
When I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I am on the "A1" "assign activity" page logged in as student1
|
||||
|
||||
# Do the assignment.
|
||||
And I follow "A1"
|
||||
And I click on "Add submission" "button"
|
||||
And I set the field "Online text" to "Q"
|
||||
And I click on "Save changes" "button"
|
||||
@@ -103,14 +102,12 @@ Feature: availability_grade
|
||||
And I should not see "P3" in the "region-main" "region"
|
||||
And I should not see "P4" in the "region-main" "region"
|
||||
And I should see "A1" in the "region-main" "region"
|
||||
And I log out
|
||||
|
||||
# Log back in as teacher.
|
||||
When I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I am on the "A1" "assign activity" page logged in as teacher1
|
||||
|
||||
# Give the assignment 40%.
|
||||
And I follow "A1"
|
||||
And I navigate to "View all submissions" in current page administration
|
||||
# Pick the grade link in the row that has s@example.com in it.
|
||||
And I click on "Grade" "link" in the "s@example.com" "table_row"
|
||||
@@ -118,11 +115,10 @@ Feature: availability_grade
|
||||
And I click on "Save changes" "button"
|
||||
And I press "OK"
|
||||
And I click on "Edit settings" "link"
|
||||
And I log out
|
||||
|
||||
# Log back in as student.
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I am on the "Course 1" course page logged in as student1
|
||||
|
||||
# Check pages are visible.
|
||||
Then I should see "P2" in the "region-main" "region"
|
||||
|
||||
@@ -95,8 +95,7 @@ Feature: availability_profile
|
||||
And I click on "Save and return to course" "button"
|
||||
|
||||
# Edit it again and check the setting still works.
|
||||
When I follow "P1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
When I am on the P1 "page activity editing" page
|
||||
And I expand all fieldsets
|
||||
Then the field "User profile field" matches value "Super field"
|
||||
And the field "Value to compare against" matches value "Bananaman"
|
||||
|
||||
@@ -511,4 +511,33 @@ class info_testcase extends advanced_testcase {
|
||||
sort($result);
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the info_module class when involved in a recursive call to $cm->name.
|
||||
*/
|
||||
public function test_info_recursive_name_call() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create a course and page.
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$page1 = $generator->create_module('page', ['course' => $course->id, 'name' => 'Page1']);
|
||||
|
||||
// Set invalid availability.
|
||||
$DB->set_field('course_modules', 'availability', 'not valid', ['id' => $page1->cmid]);
|
||||
|
||||
// Get the cm_info object.
|
||||
$this->setAdminUser();
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$cm1 = $modinfo->get_cm($page1->cmid);
|
||||
|
||||
// At this point we will generate dynamic data for $cm1, which will cause the debugging
|
||||
// call below.
|
||||
$this->assertEquals('Page1', $cm1->name);
|
||||
|
||||
$this->assertDebuggingCalled('Error processing availability data for ' .
|
||||
'‘Page1’: Invalid availability text');
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+17
-8
@@ -198,8 +198,11 @@ M.core_availability.form = {
|
||||
// If the groupmode and grouping id aren't set, disable it.
|
||||
var groupmode = Y.one('#id_groupmode');
|
||||
var groupingid = Y.one('#id_groupingid');
|
||||
if ((!groupmode || Number(groupmode.get('value')) === 0) &&
|
||||
(!groupingid || Number(groupingid.get('value')) === 0)) {
|
||||
var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1;
|
||||
var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1;
|
||||
|
||||
if ((!groupmode || Number(groupmode.get('value')) === 0 || !groupavailability) &&
|
||||
(!groupingid || Number(groupingid.get('value')) === 0 || !groupingavailability)) {
|
||||
this.restrictByGroup.set('disabled', true);
|
||||
return;
|
||||
}
|
||||
@@ -220,22 +223,28 @@ M.core_availability.form = {
|
||||
e.preventDefault();
|
||||
|
||||
// Add the condition.
|
||||
var groupmode = Y.one('#id_groupmode');
|
||||
var groupingid = Y.one('#id_groupingid');
|
||||
var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1;
|
||||
var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1;
|
||||
|
||||
var newChild;
|
||||
if (groupingid && Number(groupingid.get('value')) !== 0) {
|
||||
if (groupingid && Number(groupingid.get('value')) !== 0 && groupingavailability) {
|
||||
// Add a grouping restriction if one is specified.
|
||||
newChild = new M.core_availability.Item(
|
||||
{type: 'grouping', id: Number(groupingid.get('value'))}, true);
|
||||
} else {
|
||||
} else if (groupmode && groupavailability) {
|
||||
// Otherwise just add a group restriction.
|
||||
newChild = new M.core_availability.Item({type: 'group'}, true);
|
||||
}
|
||||
|
||||
// Refresh HTML.
|
||||
this.rootList.addChild(newChild);
|
||||
this.update();
|
||||
this.rootList.renumber();
|
||||
this.rootList.updateHtml();
|
||||
if (newChild !== null) {
|
||||
this.rootList.addChild(newChild);
|
||||
this.update();
|
||||
this.rootList.renumber();
|
||||
this.rootList.updateHtml();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+3
-3
File diff suppressed because one or more lines are too long
+17
-8
@@ -198,8 +198,11 @@ M.core_availability.form = {
|
||||
// If the groupmode and grouping id aren't set, disable it.
|
||||
var groupmode = Y.one('#id_groupmode');
|
||||
var groupingid = Y.one('#id_groupingid');
|
||||
if ((!groupmode || Number(groupmode.get('value')) === 0) &&
|
||||
(!groupingid || Number(groupingid.get('value')) === 0)) {
|
||||
var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1;
|
||||
var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1;
|
||||
|
||||
if ((!groupmode || Number(groupmode.get('value')) === 0 || !groupavailability) &&
|
||||
(!groupingid || Number(groupingid.get('value')) === 0 || !groupingavailability)) {
|
||||
this.restrictByGroup.set('disabled', true);
|
||||
return;
|
||||
}
|
||||
@@ -220,22 +223,28 @@ M.core_availability.form = {
|
||||
e.preventDefault();
|
||||
|
||||
// Add the condition.
|
||||
var groupmode = Y.one('#id_groupmode');
|
||||
var groupingid = Y.one('#id_groupingid');
|
||||
var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1;
|
||||
var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1;
|
||||
|
||||
var newChild;
|
||||
if (groupingid && Number(groupingid.get('value')) !== 0) {
|
||||
if (groupingid && Number(groupingid.get('value')) !== 0 && groupingavailability) {
|
||||
// Add a grouping restriction if one is specified.
|
||||
newChild = new M.core_availability.Item(
|
||||
{type: 'grouping', id: Number(groupingid.get('value'))}, true);
|
||||
} else {
|
||||
} else if (groupmode && groupavailability) {
|
||||
// Otherwise just add a group restriction.
|
||||
newChild = new M.core_availability.Item({type: 'group'}, true);
|
||||
}
|
||||
|
||||
// Refresh HTML.
|
||||
this.rootList.addChild(newChild);
|
||||
this.update();
|
||||
this.rootList.renumber();
|
||||
this.rootList.updateHtml();
|
||||
if (newChild !== null) {
|
||||
this.rootList.addChild(newChild);
|
||||
this.update();
|
||||
this.rootList.renumber();
|
||||
this.rootList.updateHtml();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Vendored
+17
-8
@@ -196,8 +196,11 @@ M.core_availability.form = {
|
||||
// If the groupmode and grouping id aren't set, disable it.
|
||||
var groupmode = Y.one('#id_groupmode');
|
||||
var groupingid = Y.one('#id_groupingid');
|
||||
if ((!groupmode || Number(groupmode.get('value')) === 0) &&
|
||||
(!groupingid || Number(groupingid.get('value')) === 0)) {
|
||||
var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1;
|
||||
var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1;
|
||||
|
||||
if ((!groupmode || Number(groupmode.get('value')) === 0 || !groupavailability) &&
|
||||
(!groupingid || Number(groupingid.get('value')) === 0 || !groupingavailability)) {
|
||||
this.restrictByGroup.set('disabled', true);
|
||||
return;
|
||||
}
|
||||
@@ -218,22 +221,28 @@ M.core_availability.form = {
|
||||
e.preventDefault();
|
||||
|
||||
// Add the condition.
|
||||
var groupmode = Y.one('#id_groupmode');
|
||||
var groupingid = Y.one('#id_groupingid');
|
||||
var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1;
|
||||
var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1;
|
||||
|
||||
var newChild;
|
||||
if (groupingid && Number(groupingid.get('value')) !== 0) {
|
||||
if (groupingid && Number(groupingid.get('value')) !== 0 && groupingavailability) {
|
||||
// Add a grouping restriction if one is specified.
|
||||
newChild = new M.core_availability.Item(
|
||||
{type: 'grouping', id: Number(groupingid.get('value'))}, true);
|
||||
} else {
|
||||
} else if (groupmode && groupavailability) {
|
||||
// Otherwise just add a group restriction.
|
||||
newChild = new M.core_availability.Item({type: 'group'}, true);
|
||||
}
|
||||
|
||||
// Refresh HTML.
|
||||
this.rootList.addChild(newChild);
|
||||
this.update();
|
||||
this.rootList.renumber();
|
||||
this.rootList.updateHtml();
|
||||
if (newChild !== null) {
|
||||
this.rootList.addChild(newChild);
|
||||
this.update();
|
||||
this.rootList.renumber();
|
||||
this.rootList.updateHtml();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4046,9 +4046,30 @@ class restore_contentbankcontent_structure_step extends restore_structure_step {
|
||||
$exists = $DB->record_exists('contentbank_content', $params);
|
||||
if (!$exists) {
|
||||
$params['configdata'] = $data->configdata;
|
||||
$params['usercreated'] = $this->get_mappingid('user', $data->usercreated);
|
||||
$params['usermodified'] = $this->get_mappingid('user', $data->usermodified);
|
||||
$params['timemodified'] = time();
|
||||
|
||||
// Trying to map users. Users cannot always be mapped, e.g. when copying.
|
||||
$params['usercreated'] = $this->get_mappingid('user', $data->usercreated);
|
||||
if (!$params['usercreated']) {
|
||||
// Leave the content creator unchanged when we are restoring the same site.
|
||||
// Otherwise use current user id.
|
||||
if ($this->task->is_samesite()) {
|
||||
$params['usercreated'] = $data->usercreated;
|
||||
} else {
|
||||
$params['usercreated'] = $this->task->get_userid();
|
||||
}
|
||||
}
|
||||
$params['usermodified'] = $this->get_mappingid('user', $data->usermodified);
|
||||
if (!$params['usermodified']) {
|
||||
// Leave the content modifier unchanged when we are restoring the same site.
|
||||
// Otherwise use current user id.
|
||||
if ($this->task->is_samesite()) {
|
||||
$params['usermodified'] = $data->usermodified;
|
||||
} else {
|
||||
$params['usermodified'] = $this->task->get_userid();
|
||||
}
|
||||
}
|
||||
|
||||
$newitemid = $DB->insert_record('contentbank_content', $params);
|
||||
$this->set_mapping('contentbank_content', $oldid, $newitemid, true);
|
||||
}
|
||||
|
||||
@@ -278,6 +278,73 @@ class core_backup_automated_backup_testcase extends advanced_testcase {
|
||||
$this->assertTrue($skipped);
|
||||
$this->expectOutputRegex("/Skipping $course->fullname \(Not modified since previous backup\)/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the task completes when coureid is missing.
|
||||
*/
|
||||
public function test_task_complete_when_courseid_is_missing() {
|
||||
global $DB;
|
||||
$admin = get_admin();
|
||||
$classobject = $this->backupcronautomatedhelper->return_this();
|
||||
|
||||
// Create this backup course.
|
||||
$backupcourse = new stdClass;
|
||||
$backupcourse->courseid = $this->course->id;
|
||||
$backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN;
|
||||
$DB->insert_record('backup_courses', $backupcourse);
|
||||
$backupcourse = $DB->get_record('backup_courses', ['courseid' => $this->course->id]);
|
||||
|
||||
// Create a backup task.
|
||||
$method = new ReflectionMethod('\backup_cron_automated_helper', 'push_course_backup_adhoc_task');
|
||||
$method->setAccessible(true); // Allow accessing of private method.
|
||||
$method->invokeArgs($classobject, [$backupcourse, $admin]);
|
||||
|
||||
// Delete course for this test.
|
||||
delete_course($this->course->id, false);
|
||||
|
||||
$task = core\task\manager::get_next_adhoc_task(time());
|
||||
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertStringContainsString('Invalid course id: ' . $this->course->id . ', task aborted.', $output);
|
||||
core\task\manager::adhoc_task_complete($task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the task completes when backup course is missing.
|
||||
*/
|
||||
public function test_task_complete_when_backup_course_is_missing() {
|
||||
global $DB;
|
||||
$admin = get_admin();
|
||||
$classobject = $this->backupcronautomatedhelper->return_this();
|
||||
|
||||
// Create this backup course.
|
||||
$backupcourse = new stdClass;
|
||||
$backupcourse->courseid = $this->course->id;
|
||||
$backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN;
|
||||
$DB->insert_record('backup_courses', $backupcourse);
|
||||
$backupcourse = $DB->get_record('backup_courses', ['courseid' => $this->course->id]);
|
||||
|
||||
// Create a backup task.
|
||||
$method = new ReflectionMethod('\backup_cron_automated_helper', 'push_course_backup_adhoc_task');
|
||||
$method->setAccessible(true); // Allow accessing of private method.
|
||||
$method->invokeArgs($classobject, [$backupcourse, $admin]);
|
||||
|
||||
// Delete backup course for this test.
|
||||
$DB->delete_records('backup_courses', ['courseid' => $this->course->id]);
|
||||
|
||||
$task = core\task\manager::get_next_adhoc_task(time());
|
||||
|
||||
ob_start();
|
||||
$task->execute();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertStringContainsString('Automated backup for course: ' . $this->course->fullname . ' encounters an error.',
|
||||
$output);
|
||||
core\task\manager::adhoc_task_complete($task);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -116,7 +116,9 @@ class backup_ui_stage_initial extends backup_ui_stage {
|
||||
if (isset($data->$name) && $data->$name != $setting->get_value()) {
|
||||
$setting->set_value($data->$name);
|
||||
$changes++;
|
||||
} else if (!isset($data->$name) && $setting->get_ui_type() == backup_setting::UI_HTML_CHECKBOX && $setting->get_value()) {
|
||||
} else if (!isset($data->$name) && $setting->get_value() &&
|
||||
$setting->get_ui_type() == backup_setting::UI_HTML_CHECKBOX &&
|
||||
$setting->get_status() !== backup_setting::LOCKED_BY_HIERARCHY) {
|
||||
$setting->set_value(0);
|
||||
$changes++;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ if (!is_null($action)) {
|
||||
} else { // Revoked badge.
|
||||
header("HTTP/1.0 410 Gone");
|
||||
$assertion = array();
|
||||
if ($obversion == OPEN_BADGES_V2) {
|
||||
if ($obversion >= OPEN_BADGES_V2) {
|
||||
$assertionurl = new moodle_url('/badges/assertion.php', array('b' => $hash));
|
||||
$assertion['id'] = $assertionurl->out();
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ if ($persistedissuer) {
|
||||
$returnurl = new moodle_url('/badges/backpack-connect.php',
|
||||
['action' => 'authorization', 'sesskey' => sesskey()]);
|
||||
|
||||
// If scope is not passed as parameter, use the issuer supported scopes.
|
||||
if (empty($scope)) {
|
||||
$scope = $issuer->get('scopessupported');
|
||||
}
|
||||
$client = new core_badges\oauth2\client($issuer, $returnurl, $scope, $externalbackpack);
|
||||
if ($client) {
|
||||
if (!$client->is_logged_in()) {
|
||||
|
||||
@@ -128,7 +128,7 @@ class core_badges_assertion {
|
||||
$email = empty($this->_data->backpackemail) ? $this->_data->email : $this->_data->backpackemail;
|
||||
$assertionurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'obversion' => $this->_obversion));
|
||||
|
||||
if ($this->_obversion == OPEN_BADGES_V2) {
|
||||
if ($this->_obversion >= OPEN_BADGES_V2) {
|
||||
$classurl = new moodle_url('/badges/badge_json.php', array('id' => $this->get_badge_id()));
|
||||
} else {
|
||||
$classurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'action' => 1));
|
||||
@@ -325,7 +325,7 @@ class core_badges_assertion {
|
||||
*/
|
||||
protected function embed_data_badge_version2 (&$json, $type = OPEN_BADGES_V2_TYPE_ASSERTION) {
|
||||
// Specification Version 2.0.
|
||||
if ($this->_obversion == OPEN_BADGES_V2) {
|
||||
if ($this->_obversion >= OPEN_BADGES_V2) {
|
||||
$badge = new badge($this->_data->id);
|
||||
if (empty($this->_data->courseid)) {
|
||||
$context = context_system::instance();
|
||||
|
||||
@@ -61,12 +61,12 @@ class external_backpack extends \moodleform {
|
||||
$mform->addElement('text', 'backpackapiurl', get_string('backpackapiurl', 'core_badges'));
|
||||
$mform->setType('backpackapiurl', PARAM_URL);
|
||||
$mform->addRule('backpackapiurl', null, 'required', null, 'client');
|
||||
$mform->addRule('backpackapiurl', get_string('maximumchars', '', 255), 'maxlength', 50, 'client');
|
||||
$mform->addRule('backpackapiurl', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
|
||||
|
||||
$mform->addElement('text', 'backpackweburl', get_string('backpackweburl', 'core_badges'));
|
||||
$mform->setType('backpackweburl', PARAM_URL);
|
||||
$mform->addRule('backpackweburl', null, 'required', null, 'client');
|
||||
$mform->addRule('backpackweburl', get_string('maximumchars', '', 255), 'maxlength', 50, 'client');
|
||||
$mform->addRule('backpackweburl', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
|
||||
|
||||
$apiversions = badges_get_badge_api_versions();
|
||||
$mform->addElement('select', 'apiversion', get_string('apiversion', 'core_badges'), $apiversions);
|
||||
|
||||
@@ -222,6 +222,7 @@ class client extends \core\oauth2\client {
|
||||
$callbackurl = self::callback_url();
|
||||
|
||||
if ($granttype == 'authorization_code') {
|
||||
$this->basicauth = true;
|
||||
$params = array('code' => $code,
|
||||
'grant_type' => $granttype,
|
||||
'redirect_uri' => $callbackurl->out(false),
|
||||
@@ -236,7 +237,7 @@ class client extends \core\oauth2\client {
|
||||
);
|
||||
}
|
||||
if ($this->basicauth) {
|
||||
$idsecret = urlencode($this->clientid) . ':' . urlencode($this->clientsecret);
|
||||
$idsecret = $this->clientid . ':' . $this->clientsecret;
|
||||
$this->setHeader('Authorization: Basic ' . base64_encode($idsecret));
|
||||
} else {
|
||||
$params['client_id'] = $this->clientid;
|
||||
@@ -244,11 +245,13 @@ class client extends \core\oauth2\client {
|
||||
}
|
||||
// Requests can either use http GET or POST.
|
||||
$response = $this->post($this->token_url(), $this->build_post_data($params));
|
||||
$r = json_decode($response);
|
||||
if ($this->info['http_code'] !== 200) {
|
||||
throw new moodle_exception('Could not upgrade oauth token');
|
||||
$debuginfo = !empty($this->error) ? $this->error : $response;
|
||||
throw new moodle_exception('oauth2refreshtokenerror', 'core_error', '', $this->info['http_code'], $debuginfo);
|
||||
}
|
||||
|
||||
$r = json_decode($response);
|
||||
|
||||
if (is_null($r)) {
|
||||
throw new moodle_exception("Could not decode JSON token response");
|
||||
}
|
||||
|
||||
@@ -240,13 +240,23 @@ class award_criteria_cohort extends award_criteria {
|
||||
return array($join, $where, $params);
|
||||
} else {
|
||||
// User is a member of ALL of the specified cohorts.
|
||||
$join = " LEFT JOIN {cohort_members} cm ON cm.userid = u.id";
|
||||
$join = ' LEFT JOIN {cohort_members} cm ON cm.userid = u.id';
|
||||
$where = ' AND (';
|
||||
$i = 0;
|
||||
foreach ($this->params as $param) {
|
||||
$i++;
|
||||
$where = ' AND cm.cohortid = :cohortid'.$i;
|
||||
if ($i == 0) {
|
||||
$where .= 'cm.cohortid = :cohortid'.$i;
|
||||
} else {
|
||||
$where .= ' OR cm.cohortid = :cohortid'.$i;
|
||||
}
|
||||
$params['cohortid'.$i] = $param['cohort'];
|
||||
$i++;
|
||||
}
|
||||
$where .= ')
|
||||
GROUP BY u.id, bi.badgeid
|
||||
HAVING COUNT(cm.cohortid) = :cohortcount';
|
||||
$params['cohortcount'] = $i;
|
||||
|
||||
return array($join, $where, $params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,35 +620,127 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test badges observer when cohort_member_added event is fired.
|
||||
* Test badges observer when cohort_member_added event is fired and user required to belong to any cohort.
|
||||
*
|
||||
* @covers award_criteria_cohort
|
||||
*/
|
||||
public function test_badges_observer_cohort_criteria_review() {
|
||||
public function test_badges_observer_any_cohort_criteria_review() {
|
||||
global $CFG;
|
||||
|
||||
require_once("$CFG->dirroot/cohort/lib.php");
|
||||
|
||||
$cohort = $this->getDataGenerator()->create_cohort();
|
||||
$cohort1 = $this->getDataGenerator()->create_cohort();
|
||||
$cohort2 = $this->getDataGenerator()->create_cohort();
|
||||
|
||||
$this->preventResetByRollback(); // Messaging is not compatible with transactions.
|
||||
|
||||
$badge = new badge($this->badgeid);
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
|
||||
// Set up the badge criteria.
|
||||
$criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
|
||||
$criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
|
||||
$criteriaoverall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_COHORT, 'badgeid' => $badge->id));
|
||||
$criteriaoverall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'cohort_cohorts' => array('0' => $cohort->id)));
|
||||
|
||||
// Make the badge active.
|
||||
$criteriaoverall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY,
|
||||
'cohort_cohorts' => array('0' => $cohort1->id, '1' => $cohort2->id)));
|
||||
$badge->set_status(BADGE_STATUS_ACTIVE);
|
||||
|
||||
// Reload it to contain criteria.
|
||||
$badge = new badge($this->badgeid);
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
|
||||
// Add the user to the cohort.
|
||||
cohort_add_member($cohort->id, $this->user->id);
|
||||
cohort_add_member($cohort2->id, $this->user->id);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
// Verify that the badge was awarded.
|
||||
$this->assertDebuggingCalled();
|
||||
$this->assertTrue($badge->is_issued($this->user->id));
|
||||
// As the badge has been awarded to user because core_badges_observer been called when the member has been added to the
|
||||
// cohort, there are no other users that can award this badge.
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
}
|
||||
|
||||
/**
|
||||
* Test badges observer when cohort_member_added event is fired and user required to belong to multiple (all) cohorts.
|
||||
*
|
||||
* @covers award_criteria_cohort
|
||||
*/
|
||||
public function test_badges_observer_all_cohort_criteria_review() {
|
||||
global $CFG;
|
||||
|
||||
require_once("$CFG->dirroot/cohort/lib.php");
|
||||
|
||||
$cohort1 = $this->getDataGenerator()->create_cohort();
|
||||
$cohort2 = $this->getDataGenerator()->create_cohort();
|
||||
$cohort3 = $this->getDataGenerator()->create_cohort();
|
||||
|
||||
// Add user2 to cohort1 and cohort3.
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
cohort_add_member($cohort3->id, $user2->id);
|
||||
cohort_add_member($cohort1->id, $user2->id);
|
||||
|
||||
// Add user3 to cohort1, cohort2 and cohort3.
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
cohort_add_member($cohort1->id, $user3->id);
|
||||
cohort_add_member($cohort2->id, $user3->id);
|
||||
cohort_add_member($cohort3->id, $user3->id);
|
||||
|
||||
$this->preventResetByRollback(); // Messaging is not compatible with transactions.
|
||||
|
||||
// Cohort criteria are used in site badges.
|
||||
$badge = new badge($this->badgeid);
|
||||
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
|
||||
// Set up the badge criteria.
|
||||
$criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
|
||||
$criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
|
||||
$criteriaoverall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_COHORT, 'badgeid' => $badge->id));
|
||||
$criteriaoverall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL,
|
||||
'cohort_cohorts' => array('0' => $cohort1->id, '1' => $cohort2->id, '2' => $cohort3->id)));
|
||||
$badge->set_status(BADGE_STATUS_ACTIVE);
|
||||
|
||||
// Reload it to contain criteria.
|
||||
$badge = new badge($this->badgeid);
|
||||
|
||||
// Verify that the badge was not awarded yet (ALL cohorts are needed and review_all_criteria has to be called).
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertFalse($badge->is_issued($user2->id));
|
||||
$this->assertFalse($badge->is_issued($user3->id));
|
||||
|
||||
// Verify that after calling review_all_criteria, users with the criteria (user3) award the badge instantly.
|
||||
$this->assertSame(1, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertFalse($badge->is_issued($user2->id));
|
||||
$this->assertTrue($badge->is_issued($user3->id));
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
// Add the user to the cohort1.
|
||||
cohort_add_member($cohort1->id, $this->user->id);
|
||||
|
||||
// Verify that the badge was not awarded yet (ALL cohorts are needed).
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
|
||||
// Add the user to the cohort3.
|
||||
cohort_add_member($cohort3->id, $this->user->id);
|
||||
|
||||
// Verify that the badge was not awarded yet (ALL cohorts are needed).
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
|
||||
// Add user to cohort2.
|
||||
cohort_add_member($cohort2->id, $this->user->id);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
// Verify that the badge was awarded (ALL cohorts).
|
||||
$this->assertTrue($badge->is_issued($this->user->id));
|
||||
// As the badge has been awarded to user because core_badges_observer been called when the member has been added to the
|
||||
// cohort, there are no other users that can award this badge.
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -694,6 +786,16 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
|
||||
$this->assertStringMatchesFormat($testassertion2->badge, json_encode($assertion2->get_badge_assertion()));
|
||||
$this->assertStringMatchesFormat($testassertion2->class, json_encode($assertion2->get_badge_class()));
|
||||
$this->assertStringMatchesFormat($testassertion2->issuer, json_encode($assertion2->get_issuer()));
|
||||
|
||||
// Test Openbadge specification version 2.1. It has the same format as OBv2.0.
|
||||
// Get assertion version 2.1.
|
||||
$award = reset($awards);
|
||||
$assertion2 = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V2P1);
|
||||
|
||||
// Make sure JSON strings have the same structure.
|
||||
$this->assertStringMatchesFormat($testassertion2->badge, json_encode($assertion2->get_badge_assertion()));
|
||||
$this->assertStringMatchesFormat($testassertion2->class, json_encode($assertion2->get_badge_class()));
|
||||
$this->assertStringMatchesFormat($testassertion2->issuer, json_encode($assertion2->get_issuer()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -120,4 +120,4 @@ Feature: Backpack badges
|
||||
And I click on "Delete" "button" in the "Delete site backpack" "dialogue"
|
||||
Then I should see "The site backpack has been deleted."
|
||||
And I should not see "https://dc.imsglobal.org"
|
||||
And "Delete" "button" should not exist
|
||||
And "Delete" "button" should not be visible
|
||||
|
||||
@@ -51,18 +51,18 @@ Feature: Award badges based on activity completion
|
||||
And I log out
|
||||
|
||||
Scenario: Student earns a badge using activity completion, but does not get passing grade
|
||||
When I log in as "student1"
|
||||
Given I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And the "Test quiz name" "quiz" activity with "auto" completion should be marked as not complete
|
||||
And I follow "Test quiz name"
|
||||
When I am on the "Test quiz name" "quiz activity" page
|
||||
And I press "Re-attempt quiz"
|
||||
And I set the field "False" to "1"
|
||||
And I press "Finish attempt ..."
|
||||
And I press "Submit all and finish"
|
||||
And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
|
||||
And I log out
|
||||
Then I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I should see "Recipients (1)"
|
||||
Then I should see "Recipients (1)"
|
||||
|
||||
@@ -64,6 +64,7 @@ Feature: Award badges based on competency completion
|
||||
And I set the following fields to these values:
|
||||
| Rating | C |
|
||||
And I click on "Rate" "button" in the "Rate" "dialogue"
|
||||
And I should see "The competency rating was manually set in the course"
|
||||
And I log out
|
||||
# See if we got the badge
|
||||
Then I log in as "user1"
|
||||
@@ -114,8 +115,8 @@ Feature: Award badges based on competency completion
|
||||
And I press "Rate"
|
||||
And I set the following fields to these values:
|
||||
| Rating | C |
|
||||
And I wait until the page is ready
|
||||
And I click on "Rate" "button" in the "Rate" "dialogue"
|
||||
And I should see "The competency rating was manually set in the course"
|
||||
And I log out
|
||||
# See if we got the badge
|
||||
Then I log in as "user1"
|
||||
@@ -168,8 +169,8 @@ Feature: Award badges based on competency completion
|
||||
And I press "Rate"
|
||||
And I set the following fields to these values:
|
||||
| Rating | C |
|
||||
And I wait until the page is ready
|
||||
And I click on "Rate" "button" in the "Rate" "dialogue"
|
||||
And I should see "The competency rating was manually set in the course"
|
||||
And I log out
|
||||
# We should not get the badge yet.
|
||||
Then I log in as "user1"
|
||||
@@ -184,8 +185,8 @@ Feature: Award badges based on competency completion
|
||||
And I press "Rate"
|
||||
And I set the following fields to these values:
|
||||
| Rating | C |
|
||||
And I wait until the page is ready
|
||||
And I click on "Rate" "button" in the "Rate" "dialogue"
|
||||
And I should see "The competency rating was manually set in the course"
|
||||
And I log out
|
||||
# See if we got the badge now.
|
||||
Then I log in as "user1"
|
||||
|
||||
@@ -147,7 +147,6 @@ Feature: My overview block pagination
|
||||
| student1 | C13 | student |
|
||||
When I log in as "student1"
|
||||
And I click on "[data-control='next']" "css_element" in the "Course overview" "block"
|
||||
And I wait until ".block_myoverview [data-control='next']" "css_element" exists
|
||||
Then the "class" attribute of ".block_myoverview [data-control='next']" "css_element" should contain "disabled"
|
||||
And I log out
|
||||
|
||||
@@ -181,7 +180,6 @@ Feature: My overview block pagination
|
||||
| student1 | C25 | student |
|
||||
When I log in as "student1"
|
||||
And I click on "[data-control='next']" "css_element" in the "Course overview" "block"
|
||||
And I wait until ".block_myoverview [data-control='next']" "css_element" exists
|
||||
Then the "class" attribute of ".block_myoverview [data-control='next']" "css_element" should not contain "disabled"
|
||||
And the "class" attribute of ".block_myoverview [data-control='previous']" "css_element" should not contain "disabled"
|
||||
And I should see "Course 13" in the "Course overview" "block"
|
||||
|
||||
@@ -27,9 +27,7 @@ Feature: Course overview block show users their progress on courses
|
||||
Then I should not see "0%" in the "Course overview" "block"
|
||||
|
||||
Scenario: User complete activity and verify his progress
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I follow "Test choice 1"
|
||||
Given I am on the "Test choice 1" "choice activity" page logged in as teacher1
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Completion tracking | Show activity as complete when conditions are met |
|
||||
@@ -40,8 +38,8 @@ Feature: Course overview block show users their progress on courses
|
||||
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
|
||||
Then I should see "Course 1" in the "Course overview" "block"
|
||||
And I should see "0%" in the "Course overview" "block"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Test choice 1"
|
||||
|
||||
And I am on the "Test choice 1" "choice activity" page
|
||||
And I follow "Dashboard" in the user menu
|
||||
And I click on "All (except removed from view)" "button" in the "Course overview" "block"
|
||||
And I should see "100%" in the "Course overview" "block"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['otherusers'] = 'Other Users ({$a})';
|
||||
$string['otherusers'] = 'Other users ({$a})';
|
||||
$string['onlinestatushiding_desc'] = 'If enabled, users have the option to hide their online status from other users.';
|
||||
$string['configtimetosee'] = 'Number of minutes determining the period of inactivity after which a user is no longer considered to be online.';
|
||||
$string['onlinestatushiding'] = 'Online status hiding';
|
||||
|
||||
@@ -37,7 +37,7 @@ Feature: The online users block allow you to see who is currently online on fron
|
||||
When I log in as "student1"
|
||||
And I am on site homepage
|
||||
Then I should not see "Admin User" in the "Online users" "block"
|
||||
And I should see "Other Users (1)" in the "Online users" "block"
|
||||
And I should see "Other users (1)" in the "Online users" "block"
|
||||
And I should see "Student 1" in the "Online users" "block"
|
||||
And I should see "Student 2" in the "Online users" "block"
|
||||
And I should see "3 online users" in the "Online users" "block"
|
||||
@@ -79,7 +79,7 @@ Feature: The online users block allow you to see who is currently online on fron
|
||||
And I am on site homepage
|
||||
Then I should see "2 online user" in the "Online users" "block"
|
||||
And I should not see "Admin" in the "Online users" "block"
|
||||
And I should see "Other Users (1)" in the "Online users" "block"
|
||||
And I should see "Other users (1)" in the "Online users" "block"
|
||||
And I should see "Student 2" in the "Online users" "block"
|
||||
And I should not see "Student 1" in the "Online users" "block"
|
||||
And I log out
|
||||
@@ -94,7 +94,7 @@ Feature: The online users block allow you to see who is currently online on fron
|
||||
And I am on site homepage
|
||||
Then I should see "3 online users" in the "Online users" "block"
|
||||
And I should not see "Admin" in the "Online users" "block"
|
||||
And I should see "Other Users (1)" in the "Online users" "block"
|
||||
And I should see "Other users (1)" in the "Online users" "block"
|
||||
And I should see "Student 2" in the "Online users" "block"
|
||||
And I should see "Student 1" in the "Online users" "block"
|
||||
|
||||
@@ -138,7 +138,7 @@ Feature: The online users block allow you to see who is currently online on fron
|
||||
And I am on site homepage
|
||||
And I should see "2 online user" in the "Online users" "block"
|
||||
And I should not see "Admin" in the "Online users" "block"
|
||||
And I should see "Other Users (1)" in the "Online users" "block"
|
||||
And I should see "Other users (1)" in the "Online users" "block"
|
||||
And I should see "Student 2" in the "Online users" "block"
|
||||
And I should not see "Student 1" in the "Online users" "block"
|
||||
And I log out
|
||||
@@ -148,6 +148,6 @@ Feature: The online users block allow you to see who is currently online on fron
|
||||
When I am on site homepage
|
||||
Then I should see "3 online users" in the "Online users" "block"
|
||||
And I should not see "Admin" in the "Online users" "block"
|
||||
And I should see "Other Users (1)" in the "Online users" "block"
|
||||
And I should see "Other users (1)" in the "Online users" "block"
|
||||
And I should see "Student 2" in the "Online users" "block"
|
||||
And I should see "Student 1" in the "Online users" "block"
|
||||
|
||||
@@ -19,7 +19,7 @@ Feature: The private files block allows users to store files privately in moodle
|
||||
| page | C1 | page1 | Test page name | Test page description |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I follow "Test page name"
|
||||
And I am on the "Test page name" "page activity" page
|
||||
And I add the "Private files" block
|
||||
And I should see "No files available" in the "Private files" "block"
|
||||
When I follow "Manage private files..."
|
||||
|
||||
+8
-12
@@ -16,24 +16,20 @@ Feature: The recently accessed items block allows users to easily access their m
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student1 | C2 | student |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage
|
||||
And I turn editing mode on
|
||||
And I add a "Forum" to section "1" and I fill the form with:
|
||||
| Forum name | Test forum name |
|
||||
| Forum type | Standard forum for general use |
|
||||
| Description | Test forum description |
|
||||
And I log out
|
||||
And the following "activity" exists:
|
||||
| course | C1 |
|
||||
| activity | forum |
|
||||
| idnumber | Test forum name |
|
||||
| name | Test forum name |
|
||||
And I log in as "student1"
|
||||
When I press "Customise this page"
|
||||
And I press "Customise this page"
|
||||
And I add the "Recently accessed items" block
|
||||
|
||||
Scenario: User has not accessed any item
|
||||
Then I should see "No recent items" in the "Recently accessed items" "block"
|
||||
|
||||
Scenario: User has accessed some items
|
||||
Given I am on "Course 1" course homepage
|
||||
When I follow "Test forum name"
|
||||
Given I change window size to "large"
|
||||
When I am on the "Test forum name" "forum activity" page
|
||||
And I follow "Dashboard" in the user menu
|
||||
And I change window size to "large"
|
||||
Then I should see "Test forum name" in the "Recently accessed items" "block"
|
||||
|
||||
@@ -42,19 +42,21 @@ class block_tag_youtube_edit_form extends block_edit_form {
|
||||
|
||||
// Category setting.
|
||||
$categorychoices = ['0' => get_string('anycategory', 'block_tag_youtube')];
|
||||
$hascategoryerror = false;
|
||||
$categoryerror = '';
|
||||
|
||||
try {
|
||||
// Get all video categories through an API call and add them to the category list.
|
||||
$categorychoices += $this->block->get_categories();
|
||||
} catch (Exception $e) {
|
||||
$hascategoryerror = true;
|
||||
$categoryerror = $e->getMessage();
|
||||
}
|
||||
$mform->addElement('select', 'config_category', get_string('category', 'block_tag_youtube'),
|
||||
$categorychoices);
|
||||
$mform->setDefault('config_category', 0);
|
||||
|
||||
if ($categoryerror) {
|
||||
if ($hascategoryerror) {
|
||||
$notification = $OUTPUT->notification(get_string('categoryerror', 'block_tag_youtube', $categoryerror),
|
||||
'error');
|
||||
$mform->addElement('static', 'config_category_error', '', $notification);
|
||||
|
||||
@@ -97,18 +97,26 @@ class provider implements
|
||||
public static function get_contexts_for_userid(int $userid) : contextlist {
|
||||
$contextlist = new contextlist();
|
||||
|
||||
// Only events belonging to that user will be exported.
|
||||
// Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
|
||||
$params = [
|
||||
'usercontext' => CONTEXT_USER,
|
||||
'modulecontext' => CONTEXT_MODULE,
|
||||
'cuserid' => $userid,
|
||||
'muserid' => $userid,
|
||||
'sitecontext' => CONTEXT_SYSTEM,
|
||||
'categorycontext' => CONTEXT_COURSECAT,
|
||||
'coursecontext' => CONTEXT_COURSE,
|
||||
'groupcontext' => CONTEXT_COURSE,
|
||||
'usercontext' => CONTEXT_USER,
|
||||
'cuserid' => $userid,
|
||||
'modulecontext' => CONTEXT_MODULE,
|
||||
'muserid' => $userid
|
||||
];
|
||||
|
||||
// Get contexts of Calendar Events for the owner.
|
||||
$sql = "SELECT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {event} e ON
|
||||
(e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
|
||||
(e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
|
||||
(e.courseid = ctx.instanceid AND e.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
|
||||
(e.courseid = ctx.instanceid AND e.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
|
||||
(e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
|
||||
WHERE e.userid = :cuserid
|
||||
UNION
|
||||
@@ -120,8 +128,12 @@ class provider implements
|
||||
WHERE e.userid = :muserid";
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Only export subscriptions belonging to that user.
|
||||
// Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
|
||||
$params = [
|
||||
'sitecontext' => CONTEXT_SYSTEM,
|
||||
'categorycontext' => CONTEXT_COURSECAT,
|
||||
'coursecontext' => CONTEXT_COURSE,
|
||||
'groupcontext' => CONTEXT_COURSE,
|
||||
'usercontext' => CONTEXT_USER,
|
||||
'userid' => $userid
|
||||
];
|
||||
@@ -130,6 +142,10 @@ class provider implements
|
||||
$sql = "SELECT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {event_subscriptions} s ON
|
||||
(s.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
|
||||
(s.categoryid = ctx.instanceid AND s.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
|
||||
(s.courseid = ctx.instanceid AND s.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
|
||||
(s.courseid = ctx.instanceid AND s.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
|
||||
(s.userid = ctx.instanceid AND s.eventtype = 'user' AND ctx.contextlevel = :usercontext)
|
||||
WHERE s.userid = :userid";
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
@@ -148,7 +164,8 @@ class provider implements
|
||||
|
||||
$context = $userlist->get_context();
|
||||
|
||||
// Calendar Events can exist at user (CONTEXT_USER), or Course Modules (CONTEXT_MODULE) contexts.
|
||||
// Calendar Events can exist at Site (CONTEXT_SYSTEM), Course Category (CONTEXT_COURSECAT),
|
||||
// Course and Course Group (CONTEXT_COURSE), User (CONTEXT_USER), or Course Modules (CONTEXT_MODULE) contexts.
|
||||
if ($context->contextlevel == CONTEXT_MODULE) {
|
||||
$params = ['cmid' => $context->instanceid];
|
||||
|
||||
@@ -159,21 +176,43 @@ class provider implements
|
||||
WHERE cm.id = :cmid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
} else if ($context->contextlevel == CONTEXT_USER) {
|
||||
$params = ['instanceid' => $context->instanceid];
|
||||
} else if ($context->contextlevel == CONTEXT_SYSTEM) {
|
||||
// Get contexts of Calendar Events for the owner.
|
||||
$sql = "SELECT userid FROM {event} WHERE eventtype = 'site'";
|
||||
$userlist->add_from_sql('userid', $sql, []);
|
||||
|
||||
// Get contexts for Calendar Subscriptions for the owner.
|
||||
$sql = "SELECT userid FROM {event_subscriptions} WHERE eventtype = 'site'";
|
||||
$userlist->add_from_sql('userid', $sql, []);
|
||||
} else if (in_array($context->contextlevel, [CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_USER])) {
|
||||
$eventfields = [
|
||||
CONTEXT_COURSECAT => 'categoryid',
|
||||
CONTEXT_COURSE => 'courseid',
|
||||
CONTEXT_USER => 'userid'
|
||||
];
|
||||
$eventfield = $eventfields[$context->contextlevel];
|
||||
|
||||
$eventtypes = [
|
||||
CONTEXT_COURSECAT => 'category',
|
||||
CONTEXT_COURSE => ['course' , 'group'],
|
||||
CONTEXT_USER => 'user'
|
||||
];
|
||||
list($eventtypesql, $eventtypeparams) = $DB->get_in_or_equal($eventtypes[$context->contextlevel], SQL_PARAMS_NAMED);
|
||||
|
||||
$params = $eventtypeparams + ['instanceid' => $context->instanceid];
|
||||
|
||||
// Get contexts of Calendar Events for the owner.
|
||||
$sql = "SELECT userid
|
||||
FROM {event}
|
||||
WHERE eventtype = 'user'
|
||||
AND userid = :instanceid";
|
||||
WHERE eventtype $eventtypesql
|
||||
AND $eventfield = :instanceid";
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
// Get contexts for Calendar Subscriptions for the owner.
|
||||
$sql = "SELECT userid
|
||||
FROM {event_subscriptions}
|
||||
WHERE eventtype = 'user'
|
||||
AND userid = :instanceid";
|
||||
WHERE eventtype $eventtypesql
|
||||
AND $eventfield = :instanceid";
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
}
|
||||
@@ -237,6 +276,9 @@ class provider implements
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
$allowedcontexts = [
|
||||
CONTEXT_SYSTEM,
|
||||
CONTEXT_COURSECAT,
|
||||
CONTEXT_COURSE,
|
||||
CONTEXT_MODULE,
|
||||
CONTEXT_USER
|
||||
];
|
||||
@@ -407,8 +449,9 @@ class provider implements
|
||||
protected static function get_calendar_event_ids_by_context(\context $context, $userids = array()) {
|
||||
global $DB;
|
||||
|
||||
// Only events can exist at user and course module contexts can be exported.
|
||||
if (!in_array($context->contextlevel, [CONTEXT_USER, CONTEXT_MODULE])) {
|
||||
// Calendar Events can exist at Site (CONTEXT_SYSTEM), Course Category (CONTEXT_COURSECAT),
|
||||
// Course and Course Group (CONTEXT_COURSE), User (CONTEXT_USER), or Course Modules (CONTEXT_MODULE) contexts.
|
||||
if (!in_array($context->contextlevel, [CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_USER, CONTEXT_MODULE])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -428,16 +471,35 @@ class provider implements
|
||||
JOIN {modules} m ON m.id = cm.module
|
||||
JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
|
||||
WHERE cm.id = :cmid
|
||||
AND e.userid <> 0
|
||||
$whereusersql";
|
||||
} else {
|
||||
$params = ['instanceid' => $context->instanceid];
|
||||
} else if ($context->contextlevel == CONTEXT_SYSTEM) { // Site events.
|
||||
$params = [];
|
||||
$sql = "SELECT DISTINCT e.id AS eventid
|
||||
FROM {event} e
|
||||
WHERE e.eventtype = 'site'
|
||||
$whereusersql";
|
||||
} else { // The rest.
|
||||
$eventfields = [
|
||||
CONTEXT_COURSECAT => 'categoryid',
|
||||
CONTEXT_COURSE => 'courseid',
|
||||
CONTEXT_USER => 'userid'
|
||||
];
|
||||
$eventfield = $eventfields[$context->contextlevel];
|
||||
|
||||
$eventtypes = [
|
||||
CONTEXT_COURSECAT => 'category',
|
||||
CONTEXT_COURSE => ['course' , 'group'],
|
||||
CONTEXT_USER => 'user'
|
||||
];
|
||||
list($eventtypesql, $eventtypeparams) = $DB->get_in_or_equal($eventtypes[$context->contextlevel], SQL_PARAMS_NAMED);
|
||||
|
||||
$params = $eventtypeparams + ['instanceid' => $context->instanceid];
|
||||
|
||||
// Get Calendar Events for the specified Moodle context.
|
||||
$sql = "SELECT DISTINCT e.id AS eventid
|
||||
FROM {event} e
|
||||
WHERE e.eventtype = 'user'
|
||||
AND e.userid = :instanceid
|
||||
WHERE e.eventtype $eventtypesql
|
||||
AND e.{$eventfield} = :instanceid
|
||||
$whereusersql";
|
||||
}
|
||||
|
||||
@@ -458,8 +520,9 @@ class provider implements
|
||||
protected static function get_calendar_subscription_ids_by_context(\context $context, $userids = array()) {
|
||||
global $DB;
|
||||
|
||||
// Only own user event subscriptions can be exported.
|
||||
if ($context->contextlevel != CONTEXT_USER) {
|
||||
// Calendar Subscriptions can exist at Site (CONTEXT_SYSTEM), Course Category (CONTEXT_COURSECAT),
|
||||
// Course and Course Group (CONTEXT_COURSE), or User (CONTEXT_USER) contexts.
|
||||
if (!in_array($context->contextlevel, [CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_USER])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -469,14 +532,40 @@ class provider implements
|
||||
list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
$whereusersql = "AND s.userid {$usersql}";
|
||||
}
|
||||
$params = ['instanceid' => $context->instanceid];
|
||||
|
||||
// Get Calendar Subscriptions for the specified context.
|
||||
$sql = "SELECT DISTINCT s.id AS subscriptionid
|
||||
FROM {event_subscriptions} s
|
||||
WHERE s.eventtype = 'user'
|
||||
AND s.userid = :instanceid
|
||||
$whereusersql";
|
||||
if ($context->contextlevel == CONTEXT_SYSTEM) {
|
||||
$params = [];
|
||||
|
||||
// Get Calendar Subscriptions for the system context.
|
||||
$sql = "SELECT DISTINCT s.id AS subscriptionid
|
||||
FROM {event_subscriptions} s
|
||||
WHERE s.eventtype = 'site'
|
||||
$whereusersql";
|
||||
} else {
|
||||
$eventfields = [
|
||||
CONTEXT_COURSECAT => 'categoryid',
|
||||
CONTEXT_COURSE => 'courseid',
|
||||
CONTEXT_USER => 'userid'
|
||||
];
|
||||
$eventfield = $eventfields[$context->contextlevel];
|
||||
|
||||
$eventtypes = [
|
||||
CONTEXT_COURSECAT => 'category',
|
||||
CONTEXT_COURSE => ['course' , 'group'],
|
||||
CONTEXT_USER => 'user'
|
||||
];
|
||||
list($eventtypesql, $eventtypeparams) = $DB->get_in_or_equal($eventtypes[$context->contextlevel], SQL_PARAMS_NAMED);
|
||||
|
||||
$params = $eventtypeparams + ['instanceid' => $context->instanceid];
|
||||
|
||||
// Get Calendar Subscriptions for the specified context.
|
||||
$sql = "SELECT DISTINCT s.id AS subscriptionid
|
||||
FROM {event_subscriptions} s
|
||||
WHERE s.eventtype $eventtypesql
|
||||
AND s.{$eventfield} = :instanceid
|
||||
$whereusersql";
|
||||
}
|
||||
|
||||
$params += $userparams;
|
||||
|
||||
return $DB->get_records_sql($sql, $params);
|
||||
@@ -498,8 +587,12 @@ class provider implements
|
||||
list($contextsql1, $contextparams1) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
|
||||
list($contextsql2, $contextparams2) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
|
||||
|
||||
// Only User and Course Modules contexts belonging to the user can be exported.
|
||||
// Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
|
||||
$params = [
|
||||
'sitecontext' => CONTEXT_SYSTEM,
|
||||
'categorycontext' => CONTEXT_COURSECAT,
|
||||
'coursecontext' => CONTEXT_COURSE,
|
||||
'groupcontext' => CONTEXT_COURSE,
|
||||
'usercontext' => CONTEXT_USER,
|
||||
'cuserid' => $userid,
|
||||
'modulecontext' => CONTEXT_MODULE,
|
||||
@@ -522,6 +615,10 @@ class provider implements
|
||||
ctx.id AS ctxid
|
||||
FROM {context} ctx
|
||||
INNER JOIN {event} e ON
|
||||
(e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
|
||||
(e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
|
||||
(e.courseid = ctx.instanceid AND e.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
|
||||
(e.courseid = ctx.instanceid AND e.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
|
||||
(e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
|
||||
WHERE e.userid = :cuserid
|
||||
AND ctx.id {$contextsql1}
|
||||
@@ -557,6 +654,10 @@ class provider implements
|
||||
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
|
||||
|
||||
$params = [
|
||||
'sitecontext' => CONTEXT_SYSTEM,
|
||||
'categorycontext' => CONTEXT_COURSECAT,
|
||||
'coursecontext' => CONTEXT_COURSE,
|
||||
'groupcontext' => CONTEXT_COURSE,
|
||||
'usercontext' => CONTEXT_USER,
|
||||
'userid' => $user->id
|
||||
];
|
||||
@@ -571,6 +672,10 @@ class provider implements
|
||||
s.eventtype as eventtype
|
||||
FROM {context} c
|
||||
INNER JOIN {event_subscriptions} s ON
|
||||
(s.eventtype = 'site' AND c.contextlevel = :sitecontext) OR
|
||||
(s.categoryid = c.instanceid AND s.eventtype = 'category' AND c.contextlevel = :categorycontext) OR
|
||||
(s.courseid = c.instanceid AND s.eventtype = 'course' AND c.contextlevel = :coursecontext) OR
|
||||
(s.courseid = c.instanceid AND s.eventtype = 'group' AND c.contextlevel = :groupcontext) OR
|
||||
(s.userid = c.instanceid AND s.eventtype = 'user' AND c.contextlevel = :usercontext)
|
||||
WHERE s.userid = :userid
|
||||
AND c.id {$contextsql}";
|
||||
|
||||
+42
-92
@@ -234,18 +234,9 @@ class calendar_event {
|
||||
$data->eventtype = 'user';
|
||||
}
|
||||
|
||||
// Only user and user override type of events should record the user id.
|
||||
// For all other event types we set userid to 0 as they are considered shared events.
|
||||
// Default to the current user.
|
||||
if (empty($data->userid)) {
|
||||
if ($this->allow_custom_userid($data)) {
|
||||
$data->userid = $USER->id;
|
||||
} else {
|
||||
$data->userid = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($data->courseid) && $data->eventtype == 'site') {
|
||||
$data->courseid = SITEID;
|
||||
$data->userid = $USER->id;
|
||||
}
|
||||
|
||||
if (!empty($data->timeduration) && is_array($data->timeduration)) {
|
||||
@@ -323,28 +314,6 @@ class calendar_event {
|
||||
return !empty($this->properties->{$key});
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether user id should be stored on the event or not.
|
||||
*
|
||||
* Only user events and user overrides type of events should retain user id.
|
||||
*
|
||||
* @param stdClass $data The event data object.
|
||||
* @return bool
|
||||
*/
|
||||
protected function allow_custom_userid(stdClass $data): bool {
|
||||
if ($data->eventtype === 'user') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$isactionevent = !empty($data->type) && $data->type == CALENDAR_EVENT_TYPE_ACTION;
|
||||
$isuseroverride = !empty($data->priority) && $data->priority == CALENDAR_EVENT_USER_OVERRIDE_PRIORITY;
|
||||
if ($isactionevent && $isuseroverride) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the context value needed for an event.
|
||||
*
|
||||
@@ -519,18 +488,49 @@ class calendar_event {
|
||||
}
|
||||
|
||||
if ($usingeditor) {
|
||||
switch ($this->properties->eventtype) {
|
||||
case 'user':
|
||||
$this->properties->courseid = 0;
|
||||
$this->properties->course = 0;
|
||||
$this->properties->groupid = 0;
|
||||
$this->properties->userid = $USER->id;
|
||||
break;
|
||||
case 'site':
|
||||
$this->properties->courseid = SITEID;
|
||||
$this->properties->course = SITEID;
|
||||
$this->properties->groupid = 0;
|
||||
$this->properties->userid = $USER->id;
|
||||
break;
|
||||
case 'course':
|
||||
$this->properties->groupid = 0;
|
||||
$this->properties->userid = $USER->id;
|
||||
break;
|
||||
case 'category':
|
||||
$this->properties->groupid = 0;
|
||||
$this->properties->category = 0;
|
||||
$this->properties->userid = $USER->id;
|
||||
break;
|
||||
case 'group':
|
||||
$this->properties->userid = $USER->id;
|
||||
break;
|
||||
default:
|
||||
// We should NEVER get here, but just incase we do lets fail gracefully.
|
||||
$usingeditor = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// If we are actually using the editor, we recalculate the context because some default values
|
||||
// were set when calculate_context() was called from the constructor.
|
||||
$this->properties->context = $this->calculate_context();
|
||||
$this->editorcontext = $this->get_context();
|
||||
if ($usingeditor) {
|
||||
$this->properties->context = $this->calculate_context();
|
||||
$this->editorcontext = $this->get_context();
|
||||
}
|
||||
|
||||
$editor = $this->properties->description;
|
||||
$this->properties->format = $this->properties->description['format'];
|
||||
$this->properties->description = $this->properties->description['text'];
|
||||
}
|
||||
|
||||
$this->set_default_event_ids();
|
||||
|
||||
// Insert the event into the database.
|
||||
$this->properties->id = $DB->insert_record('event', $this->properties);
|
||||
|
||||
@@ -675,8 +675,6 @@ class calendar_event {
|
||||
$event->trigger();
|
||||
}
|
||||
} else {
|
||||
$this->set_default_event_ids();
|
||||
|
||||
$DB->update_record('event', $this->properties);
|
||||
$event = self::load($this->properties->id);
|
||||
$this->properties = $event->properties();
|
||||
@@ -871,49 +869,6 @@ class calendar_event {
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default event ids.
|
||||
*/
|
||||
protected function set_default_event_ids(): void {
|
||||
global $USER;
|
||||
$userid = (!empty($this->properties->userid)) ? $this->properties->userid : $USER->id;
|
||||
switch ($this->properties->eventtype) {
|
||||
case 'user':
|
||||
$this->properties->courseid = 0;
|
||||
$this->properties->course = 0;
|
||||
$this->properties->groupid = 0;
|
||||
$this->properties->userid = $userid;
|
||||
break;
|
||||
case 'site':
|
||||
$this->properties->courseid = SITEID;
|
||||
$this->properties->course = SITEID;
|
||||
$this->properties->groupid = 0;
|
||||
$this->properties->userid = 0;
|
||||
break;
|
||||
case 'course':
|
||||
$this->properties->groupid = 0;
|
||||
$this->properties->userid = 0;
|
||||
break;
|
||||
case 'category':
|
||||
$this->properties->groupid = 0;
|
||||
$this->properties->category = 0;
|
||||
$this->properties->userid = 0;
|
||||
break;
|
||||
case 'group':
|
||||
$this->properties->userid = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Only user overrides type of events should store user's id.
|
||||
$isactionevent = !empty($this->properties->type) && $this->properties->type == CALENDAR_EVENT_TYPE_ACTION;
|
||||
$isuseroverride = $isactionevent && !empty($this->properties->priority) &&
|
||||
$this->properties->priority == CALENDAR_EVENT_USER_OVERRIDE_PRIORITY;
|
||||
|
||||
if ($isuseroverride) {
|
||||
$this->properties->userid = $userid;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of an event
|
||||
*
|
||||
@@ -2441,9 +2396,7 @@ function calendar_get_default_courses($courseid = null, $fields = '*', $canmanag
|
||||
$prefixedfields = array_map(function($value) {
|
||||
return 'c.' . trim(strtolower($value));
|
||||
}, $fieldlist);
|
||||
if (!in_array('c.visible', $prefixedfields) && !in_array('c.*', $prefixedfields)) {
|
||||
$prefixedfields[] = 'c.visible';
|
||||
}
|
||||
|
||||
$courses = get_courses('all', 'c.shortname', implode(',', $prefixedfields));
|
||||
} else {
|
||||
$courses = enrol_get_users_courses($userid, true, $fields);
|
||||
@@ -2820,9 +2773,7 @@ function calendar_add_subscription($sub) {
|
||||
// User events.
|
||||
$sub->courseid = 0;
|
||||
}
|
||||
|
||||
// Only subscriptions for user type should store user id, shared events (course,site...) should have userid set to 0.
|
||||
$sub->userid = ($sub->eventtype == 'user') ? $USER->id : 0;
|
||||
$sub->userid = $USER->id;
|
||||
|
||||
// File subscriptions never update.
|
||||
if (empty($sub->url)) {
|
||||
@@ -2951,7 +2902,7 @@ function calendar_add_icalendar_event($event, $unused = null, $subscriptionid, $
|
||||
// We should never do anything with an event without a subscription reference.
|
||||
$sub = calendar_get_subscription($subscriptionid);
|
||||
$eventrecord->subscriptionid = $subscriptionid;
|
||||
$eventrecord->userid = ($sub->eventtype == 'user') ? $sub->userid : 0;
|
||||
$eventrecord->userid = $sub->userid;
|
||||
$eventrecord->groupid = $sub->groupid;
|
||||
$eventrecord->courseid = $sub->courseid;
|
||||
$eventrecord->categoryid = $sub->categoryid;
|
||||
@@ -3253,6 +3204,7 @@ function calendar_update_subscription($subscription) {
|
||||
* @return bool true if current user can edit the subscription else false
|
||||
*/
|
||||
function calendar_can_edit_subscription($subscriptionorid) {
|
||||
global $USER;
|
||||
if (is_array($subscriptionorid)) {
|
||||
$subscription = (object)$subscriptionorid;
|
||||
} else if (is_object($subscriptionorid)) {
|
||||
@@ -3273,7 +3225,7 @@ function calendar_can_edit_subscription($subscriptionorid) {
|
||||
calendar_get_allowed_types($allowed, $courseid, null, $category);
|
||||
switch ($subscription->eventtype) {
|
||||
case 'user':
|
||||
return $allowed->user;
|
||||
return ($USER->id == $subscription->userid && $allowed->user);
|
||||
case 'course':
|
||||
if (isset($allowed->courses[$courseid])) {
|
||||
return $allowed->courses[$courseid];
|
||||
@@ -3310,8 +3262,6 @@ function calendar_get_calendar_context($subscription) {
|
||||
// Determine context based on calendar type.
|
||||
if ($subscription->eventtype === 'site') {
|
||||
$context = \context_course::instance(SITEID);
|
||||
} else if ($subscription->eventtype === 'category') {
|
||||
$context = \context_coursecat::instance($subscription->categoryid);
|
||||
} else if ($subscription->eventtype === 'group' || $subscription->eventtype === 'course') {
|
||||
$context = \context_course::instance($subscription->courseid);
|
||||
} else {
|
||||
|
||||
@@ -608,8 +608,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 1; $i < 6; $i++) {
|
||||
create_event([
|
||||
'name' => sprintf('Event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course1->id,
|
||||
@@ -619,8 +619,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 6; $i < 12; $i++) {
|
||||
create_event([
|
||||
'name' => sprintf('Event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course2->id,
|
||||
@@ -663,8 +663,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 1; $i < 6; $i++) {
|
||||
create_event([
|
||||
'name' => sprintf('Event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course1->id,
|
||||
@@ -674,8 +674,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 6; $i < 12; $i++) {
|
||||
create_event([
|
||||
'name' => sprintf('Event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course2->id,
|
||||
@@ -719,8 +719,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 1; $i < 6; $i++) {
|
||||
create_event([
|
||||
'name' => sprintf('Event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course1->id,
|
||||
@@ -730,8 +730,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 6; $i < 12; $i++) {
|
||||
create_event([
|
||||
'name' => sprintf('Event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course2->id,
|
||||
@@ -773,8 +773,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 1; $i < 21; $i++) {
|
||||
$records[] = create_event([
|
||||
'name' => sprintf('Event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course1->id,
|
||||
@@ -784,8 +784,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 21; $i < 41; $i++) {
|
||||
$records[] = create_event([
|
||||
'name' => sprintf('Event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course2->id,
|
||||
@@ -830,8 +830,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 1; $i < 41; $i++) {
|
||||
create_event([
|
||||
'name' => sprintf('Event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course1->id,
|
||||
@@ -901,8 +901,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 1; $i < 21; $i++) {
|
||||
create_event([
|
||||
'name' => sprintf('Event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course1->id,
|
||||
@@ -912,8 +912,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 21; $i < 41; $i++) {
|
||||
create_event([
|
||||
'name' => sprintf('Event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course2->id,
|
||||
@@ -979,8 +979,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 1; $i < 11; $i++) {
|
||||
$records[] = create_event([
|
||||
'name' => sprintf('1 event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course1->id,
|
||||
@@ -990,8 +990,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 1; $i < 11; $i++) {
|
||||
$records[] = create_event([
|
||||
'name' => sprintf('2 event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course1->id,
|
||||
@@ -1002,8 +1002,8 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
|
||||
for ($i = 1; $i < 11; $i++) {
|
||||
$records[] = create_event([
|
||||
'name' => sprintf('3 event %d', $i),
|
||||
'eventtype' => 'due',
|
||||
'userid' => 0,
|
||||
'eventtype' => 'user',
|
||||
'userid' => $user->id,
|
||||
'timesort' => $i,
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
'courseid' => $course2->id,
|
||||
|
||||
@@ -96,6 +96,13 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
} else {
|
||||
$prop->repeat = 1;
|
||||
}
|
||||
if (empty($prop->userid)) {
|
||||
if (!empty($userid)) {
|
||||
$prop->userid = $userid;
|
||||
} else {
|
||||
$prop->userid = 0;
|
||||
}
|
||||
}
|
||||
if (!isset($prop->courseid)) {
|
||||
$prop->courseid = $SITE->id;
|
||||
}
|
||||
@@ -384,54 +391,44 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
$this->setUser($user);
|
||||
$events = core_calendar_external::get_calendar_events($paramevents, $options);
|
||||
$events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
|
||||
// There should be a single user event.
|
||||
$this->assertEquals(1, count($events['events']));
|
||||
// There should be only two events (course and group).
|
||||
$this->assertEquals(2, count($events['warnings']));
|
||||
$this->assertEquals(2, count($events['events'])); // site, user.
|
||||
$this->assertEquals(2, count($events['warnings'])); // course, group.
|
||||
|
||||
$role = $DB->get_record('role', array('shortname' => 'student'));
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
|
||||
$events = core_calendar_external::get_calendar_events($paramevents, $options);
|
||||
$events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
|
||||
// There should be a site and both course events.
|
||||
$this->assertEquals(3, count($events['events']));
|
||||
// There should be a warning related to group event.
|
||||
$this->assertEquals(1, count($events['warnings']));
|
||||
$this->assertEquals(4, count($events['events'])); // site, user, both course events.
|
||||
$this->assertEquals(1, count($events['warnings'])); // group.
|
||||
|
||||
$options = array ('siteevents' => true, 'userevents' => true, 'timeend' => time() + HOURSECS);
|
||||
$events = core_calendar_external::get_calendar_events($paramevents, $options);
|
||||
$events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
|
||||
// There should be a site and a course event.
|
||||
$this->assertEquals(2, count($events['events']));
|
||||
// There should be a warning related to group event.
|
||||
$this->assertEquals(1, count($events['warnings']));
|
||||
$this->assertEquals(3, count($events['events'])); // site, user, one course event.
|
||||
$this->assertEquals(1, count($events['warnings'])); // group.
|
||||
|
||||
groups_add_member($group, $user);
|
||||
$events = core_calendar_external::get_calendar_events($paramevents, $options);
|
||||
$events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
|
||||
// There should be a site, group, one course event.
|
||||
$this->assertEquals(3, count($events['events']));
|
||||
$this->assertEquals(4, count($events['events'])); // site, user, group, one course event.
|
||||
$this->assertEquals(0, count($events['warnings']));
|
||||
|
||||
$paramevents = array ('courseids' => array($course->id), 'groupids' => array($group->id));
|
||||
$events = core_calendar_external::get_calendar_events($paramevents, $options);
|
||||
$events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
|
||||
// There should be only site, course and group events.
|
||||
$this->assertEquals(3, count($events['events']));
|
||||
$this->assertEquals(4, count($events['events'])); // site, user, group, one course event.
|
||||
$this->assertEquals(0, count($events['warnings']));
|
||||
|
||||
$paramevents = array ('groupids' => array($group->id, 23));
|
||||
$events = core_calendar_external::get_calendar_events($paramevents, $options);
|
||||
$events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
|
||||
// There should be only site and group events.
|
||||
$this->assertEquals(2, count($events['events']));
|
||||
$this->assertEquals(3, count($events['events'])); // site, user, group.
|
||||
$this->assertEquals(1, count($events['warnings']));
|
||||
|
||||
$paramevents = array ('courseids' => array(23));
|
||||
$events = core_calendar_external::get_calendar_events($paramevents, $options);
|
||||
$events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
|
||||
// There should be a single site event.
|
||||
$this->assertEquals(1, count($events['events']));
|
||||
$this->assertEquals(2, count($events['events'])); // site, user.
|
||||
$this->assertEquals(1, count($events['warnings']));
|
||||
|
||||
$paramevents = array ();
|
||||
@@ -472,7 +469,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
$events = core_calendar_external::get_calendar_events($paramevents, $options);
|
||||
$events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
|
||||
|
||||
$this->assertCount(4, $events['events']);
|
||||
$this->assertCount(5, $events['events']);
|
||||
|
||||
// Hide the assignment.
|
||||
set_coursemodule_visible($assign->cmid, 0);
|
||||
@@ -483,7 +480,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
$events = core_calendar_external::get_calendar_events($paramevents, $options);
|
||||
$events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
|
||||
// Expect one less.
|
||||
$this->assertCount(3, $events['events']);
|
||||
$this->assertCount(4, $events['events']);
|
||||
|
||||
// Create some category events.
|
||||
$this->setAdminUser();
|
||||
@@ -842,7 +839,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
$now = time();
|
||||
// Create two events - one for everybody in the course and one only for the first student.
|
||||
$event1 = $this->create_calendar_event('Base event', 0, 'due', 0, $now + DAYSECS, $params + ['courseid' => $course->id]);
|
||||
$event2 = $this->create_calendar_event('User event', 0, 'user', 0, $now + 2 * DAYSECS, $params + ['courseid' => 0]);
|
||||
$event2 = $this->create_calendar_event('User event', $user->id, 'due', 0, $now + 2*DAYSECS, $params + ['courseid' => 0]);
|
||||
|
||||
// Retrieve course events for the second student - only one "Base event" is returned.
|
||||
$this->setUser($user2);
|
||||
@@ -854,13 +851,14 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
$this->assertEquals(0, count($events['warnings']));
|
||||
$this->assertEquals('Base event', $events['events'][0]['name']);
|
||||
|
||||
// Retrieve events for the first student.
|
||||
// Retrieve events for the first student - both events are returned.
|
||||
$this->setUser($user);
|
||||
$events = core_calendar_external::get_calendar_events($paramevents, $options);
|
||||
$events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
|
||||
$this->assertEquals(1, count($events['events']));
|
||||
$this->assertEquals(2, count($events['events']));
|
||||
$this->assertEquals(0, count($events['warnings']));
|
||||
$this->assertEquals('Base event', $events['events'][0]['name']);
|
||||
$this->assertEquals('User event', $events['events'][1]['name']);
|
||||
|
||||
// Retrieve events by id as a teacher, 'User event' should be returned since teacher has access to this course.
|
||||
$this->setUser($teacher);
|
||||
@@ -1723,7 +1721,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
$timedurationuntil->add($interval);
|
||||
$formdata = [
|
||||
'id' => 0,
|
||||
'userid' => 0,
|
||||
'userid' => $user->id,
|
||||
'modulename' => '',
|
||||
'instance' => 0,
|
||||
'visible' => 1,
|
||||
@@ -1784,7 +1782,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
$timedurationuntil->add($interval);
|
||||
$formdata = [
|
||||
'id' => 0,
|
||||
'userid' => 0,
|
||||
'userid' => $user->id,
|
||||
'modulename' => '',
|
||||
'instance' => 0,
|
||||
'visible' => 1,
|
||||
@@ -1830,7 +1828,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
);
|
||||
|
||||
$event = $result['event'];
|
||||
$this->assertEquals(0, $event['userid']);
|
||||
$this->assertEquals($user->id, $event['userid']);
|
||||
$this->assertEquals($formdata['eventtype'], $event['eventtype']);
|
||||
$this->assertEquals($formdata['name'], $event['name']);
|
||||
}
|
||||
@@ -1850,7 +1848,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
$timedurationuntil->add($interval);
|
||||
$formdata = [
|
||||
'id' => 0,
|
||||
'userid' => 0,
|
||||
'userid' => $user->id,
|
||||
'modulename' => '',
|
||||
'instance' => 0,
|
||||
'visible' => 1,
|
||||
@@ -1961,7 +1959,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
);
|
||||
|
||||
$event = $result['event'];
|
||||
$this->assertEquals(0, $event['userid']);
|
||||
$this->assertEquals($user->id, $event['userid']);
|
||||
$this->assertEquals($formdata['eventtype'], $event['eventtype']);
|
||||
$this->assertEquals($formdata['name'], $event['name']);
|
||||
$this->assertEquals($formdata['courseid'], $event['course']['id']);
|
||||
@@ -2049,7 +2047,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
$timedurationuntil->add($interval);
|
||||
$formdata = [
|
||||
'id' => 0,
|
||||
'userid' => 0,
|
||||
'userid' => $user->id,
|
||||
'modulename' => '',
|
||||
'instance' => 0,
|
||||
'visible' => 1,
|
||||
@@ -2165,7 +2163,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
);
|
||||
|
||||
$event = $result['event'];
|
||||
$this->assertEquals(0, $event['userid']);
|
||||
$this->assertEquals($user->id, $event['userid']);
|
||||
$this->assertEquals($formdata['eventtype'], $event['eventtype']);
|
||||
$this->assertEquals($formdata['name'], $event['name']);
|
||||
$this->assertEquals($group->id, $event['groupid']);
|
||||
@@ -2188,7 +2186,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
$timedurationuntil->add($interval);
|
||||
$formdata = [
|
||||
'id' => 0,
|
||||
'userid' => 0,
|
||||
'userid' => $user->id,
|
||||
'modulename' => '',
|
||||
'instance' => 0,
|
||||
'visible' => 1,
|
||||
@@ -2239,7 +2237,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
);
|
||||
|
||||
$event = $result['event'];
|
||||
$this->assertEquals(0, $event['userid']);
|
||||
$this->assertEquals($user->id, $event['userid']);
|
||||
$this->assertEquals($formdata['eventtype'], $event['eventtype']);
|
||||
$this->assertEquals($formdata['name'], $event['name']);
|
||||
$this->assertEquals($group->id, $event['groupid']);
|
||||
@@ -2262,7 +2260,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
$timedurationuntil->add($interval);
|
||||
$formdata = [
|
||||
'id' => 0,
|
||||
'userid' => 0,
|
||||
'userid' => $user->id,
|
||||
'modulename' => '',
|
||||
'instance' => 0,
|
||||
'visible' => 1,
|
||||
@@ -2312,7 +2310,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
);
|
||||
|
||||
$event = $result['event'];
|
||||
$this->assertEquals(0, $event['userid']);
|
||||
$this->assertEquals($user->id, $event['userid']);
|
||||
$this->assertEquals($formdata['eventtype'], $event['eventtype']);
|
||||
$this->assertEquals($formdata['name'], $event['name']);
|
||||
$this->assertEquals($group->id, $event['groupid']);
|
||||
@@ -2584,10 +2582,8 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
|
||||
$category = $generator->create_category(['visible' => 0]);
|
||||
$name = 'Category Event (category: ' . $category->id . ')';
|
||||
$record = new stdClass();
|
||||
$record->eventtype = 'category';
|
||||
$record->categoryid = $category->id;
|
||||
$record->userid = 0;
|
||||
$categoryevent = $this->create_calendar_event($name, 0, 'category', 0, time(), $record);
|
||||
$categoryevent = $this->create_calendar_event($name, $USER->id, 'category', 0, time(), $record);
|
||||
|
||||
$events = [
|
||||
'eventids' => [$categoryevent->id]
|
||||
|
||||
@@ -66,6 +66,102 @@ function create_event($properties) {
|
||||
return $event->create($record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create a x number of events for each event type.
|
||||
*
|
||||
* @param int $quantity The quantity of events to be created.
|
||||
* @return array List of created events.
|
||||
*/
|
||||
function create_standard_events(int $quantity): array {
|
||||
$types = ['site', 'category', 'course', 'group', 'user'];
|
||||
|
||||
$events = [];
|
||||
foreach ($types as $eventtype) {
|
||||
// Create five events of each event type.
|
||||
for ($i = 0; $i < $quantity; $i++) {
|
||||
$events[] = create_event(['eventtype' => $eventtype]);
|
||||
}
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create an action event.
|
||||
*
|
||||
* @param array $data The event data.
|
||||
* @return bool|calendar_event
|
||||
*/
|
||||
function create_action_event(array $data) {
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/calendar/lib.php');
|
||||
|
||||
if (!isset($data['modulename']) || !isset($data['instance'])) {
|
||||
throw new coding_exception('Module and instance should be specified when creating an action event.');
|
||||
}
|
||||
|
||||
$isuseroverride = isset($data->priority) && $data->priority == CALENDAR_EVENT_USER_OVERRIDE_PRIORITY;
|
||||
if ($isuseroverride) {
|
||||
if (!in_array($data['modulename'], ['assign', 'lesson', 'quiz'])) {
|
||||
throw new coding_exception('Only assign, lesson and quiz modules supports overrides');
|
||||
}
|
||||
}
|
||||
|
||||
$event = array_merge($data, [
|
||||
'eventtype' => isset($data['eventtype']) ? $data['eventtype'] : 'open',
|
||||
'courseid' => isset($data['courseid']) ? $data['courseid'] : 0,
|
||||
'instance' => $data['instance'],
|
||||
'modulename' => $data['modulename'],
|
||||
'type' => CALENDAR_EVENT_TYPE_ACTION,
|
||||
]);
|
||||
|
||||
return create_event($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create an user override calendar event.
|
||||
*
|
||||
* @param string $modulename The modulename.
|
||||
* @param int $instanceid The instance id.
|
||||
* @param int $userid The user id.
|
||||
* @return calendar_event|false
|
||||
*/
|
||||
function create_user_override_event(string $modulename, int $instanceid, int $userid) {
|
||||
if (!isset($userid)) {
|
||||
throw new coding_exception('Must specify userid when creating a user override.');
|
||||
}
|
||||
|
||||
return create_action_event([
|
||||
'modulename' => $modulename,
|
||||
'instance' => $instanceid,
|
||||
'userid' => $userid,
|
||||
'priority' => CALENDAR_EVENT_USER_OVERRIDE_PRIORITY,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create an group override calendar event.
|
||||
*
|
||||
* @param string $modulename The modulename.
|
||||
* @param int $instanceid The instance id.
|
||||
* @param int $courseid The course id.
|
||||
* @param int $groupid The group id.
|
||||
* @return calendar_event|false
|
||||
*/
|
||||
function create_group_override_event(string $modulename, int $instanceid, int $courseid, int $groupid) {
|
||||
if (!isset($groupid)) {
|
||||
throw new coding_exception('Must specify groupid when creating a group override.');
|
||||
}
|
||||
|
||||
return create_action_event([
|
||||
'groupid' => $groupid,
|
||||
'courseid' => $courseid,
|
||||
'modulename' => $modulename,
|
||||
'instance' => $instanceid,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A test factory that will create action events.
|
||||
*
|
||||
|
||||
@@ -154,8 +154,8 @@ class core_calendar_local_api_testcase extends advanced_testcase {
|
||||
'courseid' => $course->id,
|
||||
'modulename' => 'assign',
|
||||
'instance' => $moduleinstance->id,
|
||||
'userid' => 0,
|
||||
'eventtype' => 'due',
|
||||
'userid' => 1,
|
||||
'eventtype' => 'user',
|
||||
'repeats' => 0,
|
||||
'timestart' => 1,
|
||||
];
|
||||
@@ -205,8 +205,8 @@ class core_calendar_local_api_testcase extends advanced_testcase {
|
||||
'courseid' => $course->id,
|
||||
'modulename' => 'assign',
|
||||
'instance' => $moduleinstance->id,
|
||||
'userid' => 0,
|
||||
'eventtype' => 'due',
|
||||
'userid' => 1,
|
||||
'eventtype' => 'user',
|
||||
'repeats' => 0,
|
||||
'timestart' => 1,
|
||||
];
|
||||
@@ -257,8 +257,8 @@ class core_calendar_local_api_testcase extends advanced_testcase {
|
||||
'courseid' => $course->id,
|
||||
'modulename' => 'assign',
|
||||
'instance' => $moduleinstance->id,
|
||||
'userid' => 0,
|
||||
'eventtype' => 'due',
|
||||
'userid' => 1,
|
||||
'eventtype' => 'user',
|
||||
'repeats' => 0,
|
||||
'timestart' => 1,
|
||||
];
|
||||
|
||||
+90
-103
@@ -80,8 +80,8 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
$course3context = context_course::instance($course3->id);
|
||||
|
||||
// Add Category Calendar Events for Category.
|
||||
$this->create_test_standard_calendar_event('category', 0, time(), '', $category->id);
|
||||
$this->create_test_standard_calendar_event('category', 0, time(), '', $category->id);
|
||||
$this->create_test_standard_calendar_event('category', $user->id, time(), '', $category->id);
|
||||
$this->create_test_standard_calendar_event('category', $user->id, time(), '', $category->id);
|
||||
|
||||
// Add User Calendar Events for User.
|
||||
$this->create_test_standard_calendar_event('user', $user->id, time(), '');
|
||||
@@ -89,43 +89,38 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
$this->create_test_standard_calendar_event('user', $user->id, time(), '', 0, $course2->id);
|
||||
|
||||
// Add a Course Calendar Event for Course 1.
|
||||
$this->create_test_standard_calendar_event('course', 0, time(), '', 0, $course1->id);
|
||||
$this->create_test_standard_calendar_event('course', $user->id, time(), '', 0, $course1->id);
|
||||
|
||||
// Add a Course Assignment Action Calendar Event for Course 2.
|
||||
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
|
||||
$params['course'] = $course2->id;
|
||||
$params['assignsubmission_onlinetext_enabled'] = 1;
|
||||
$assign1 = $assigngenerator->create_instance($params);
|
||||
$assign1cm = get_coursemodule_from_instance('assign', $assign1->id);
|
||||
$assign1context = context_module::instance($assign1cm->id);
|
||||
$this->create_test_action_calendar_event('duedate', $course2->id, $assign1->id, 'assign', 0, time());
|
||||
$this->create_test_action_calendar_event('gradingduedate', $course2->id, $assign1->id, 'assign', 0, time());
|
||||
|
||||
// Add a due date event similar to a user override in another assign instance.
|
||||
$assign2 = $assigngenerator->create_instance($params);
|
||||
$assign2cm = get_coursemodule_from_instance('assign', $assign2->id);
|
||||
$assign2context = context_module::instance($assign2cm->id);
|
||||
$this->create_test_action_calendar_event('duedate', $course2->id, $assign2->id, 'assign', $user->id, time());
|
||||
$this->create_test_action_calendar_event('gradingduedate', $course2->id, $assign2->id, 'assign', $user->id, time());
|
||||
$instance = $generator->create_instance($params);
|
||||
$cm = get_coursemodule_from_instance('assign', $instance->id);
|
||||
$modulecontext = context_module::instance($cm->id);
|
||||
$assign = new assign($modulecontext, $cm, $course2);
|
||||
$this->create_test_action_calendar_event('duedate', $course2->id, $instance->id, 'assign', $user->id, time());
|
||||
$this->create_test_action_calendar_event('gradingduedate', $course2->id, $instance->id, 'assign', $user->id, time());
|
||||
|
||||
// Add a Calendar Subscription and Group Calendar Event to Course 3.
|
||||
$this->create_test_calendar_subscription('course', 'https://calendar.google.com/', 0, 0, $course3->id);
|
||||
$this->create_test_standard_calendar_event('group', 0, time(), '', 0, $course3->id, $course3group->id);
|
||||
$this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user->id, 0, $course3->id);
|
||||
$this->create_test_standard_calendar_event('group', $user->id, time(), '', 0, $course3->id, $course3group->id);
|
||||
|
||||
// The user will be in these contexts.
|
||||
$usercontextids = [
|
||||
$usercontext->id,
|
||||
$assign2context->id,
|
||||
$categorycontext->id,
|
||||
$course1context->id,
|
||||
$modulecontext->id,
|
||||
$course3context->id
|
||||
];
|
||||
// Retrieve the user's context ids.
|
||||
$contextids = provider::get_contexts_for_userid($user->id);
|
||||
|
||||
// Check the user context list and retrieved user context lists contains the same number of records.
|
||||
$this->assertCount(count($usercontextids), $contextids->get_contextids());
|
||||
$this->assertEquals(count($usercontextids), count($contextids->get_contextids()));
|
||||
// There should be no difference between the contexts.
|
||||
$this->assertEmpty(array_diff($usercontextids, $contextids->get_contextids()));
|
||||
// Check that the module context for the assignment without user override event is not included.
|
||||
$this->assertNotContains($assign1context->id, $contextids->get_contextids());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,12 +150,12 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
$event1 = $this->create_test_standard_calendar_event('user', $user->id, time(), '');
|
||||
|
||||
// Add Category Calendar Events for Category.
|
||||
$event2 = $this->create_test_standard_calendar_event('category', 0, time(), '', $category->id);
|
||||
$event2 = $this->create_test_standard_calendar_event('category', $user->id, time(), '', $category->id);
|
||||
|
||||
// Add two Course Calendar Event for Course 1 and set the same time (1 day a head).
|
||||
$time = strtotime('+1 day', time());
|
||||
$event3 = $this->create_test_standard_calendar_event('course', 0, $time, 'ABC', 0, $course1->id);
|
||||
$event4 = $this->create_test_standard_calendar_event('course', 0, $time, 'DEF', 0, $course1->id);
|
||||
$event3 = $this->create_test_standard_calendar_event('course', $user->id, $time, 'ABC', 0, $course1->id);
|
||||
$event4 = $this->create_test_standard_calendar_event('course', $user->id, $time, 'DEF', 0, $course1->id);
|
||||
|
||||
// Add a Course Assignment Action Calendar Event for Course 2.
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
|
||||
@@ -174,7 +169,7 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
|
||||
// Add a Calendar Subscription and Group Calendar Event to Course 3.
|
||||
$subscription1 = $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user->id, 0, $course3->id);
|
||||
$event6 = $this->create_test_standard_calendar_event('group', 0, time(), '', 0, $course3->id, $course3group->id);
|
||||
$event6 = $this->create_test_standard_calendar_event('group', $user->id, time(), '', 0, $course3->id, $course3group->id);
|
||||
|
||||
// Retrieve the user's context ids.
|
||||
$contextlist = provider::get_contexts_for_userid($user->id);
|
||||
@@ -325,16 +320,16 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
|
||||
// Add a Course Calendar Event by User 1 for Course 1 and Course 2.
|
||||
$this->setUser($user1);
|
||||
$this->create_test_standard_calendar_event('course', 0, time(), '', 0, $course1->id);
|
||||
$this->create_test_standard_calendar_event('course', 0, time(), '', 0, $course2->id);
|
||||
$this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course1->id);
|
||||
$this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course2->id);
|
||||
|
||||
// Add a Calendar Subscription by User 1 for Course 1.
|
||||
$this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user1->id, 0, $course1->id);
|
||||
|
||||
// Add a Course Calendar Event by User 2 for Course 1 and Course 2.
|
||||
$this->setUser($user2);
|
||||
$this->create_test_standard_calendar_event('course', 0, time(), '', 0, $course1->id);
|
||||
$this->create_test_standard_calendar_event('course', 0, time(), '', 0, $course2->id);
|
||||
$this->create_test_standard_calendar_event('course', $user2->id, time(), '', 0, $course1->id);
|
||||
$this->create_test_standard_calendar_event('course', $user2->id, time(), '', 0, $course2->id);
|
||||
|
||||
// Add a Calendar Subscription by User 2 for Course 2.
|
||||
$this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user2->id, 0, $course2->id);
|
||||
@@ -347,23 +342,23 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
$cm = get_coursemodule_from_instance('assign', $instance->id);
|
||||
$modulecontext = context_module::instance($cm->id);
|
||||
$assign = new assign($modulecontext, $cm, $course2);
|
||||
$this->create_test_action_calendar_event('duedate', $course2->id, $instance->id, 'assign', 0, time());
|
||||
$this->create_test_action_calendar_event('duedate', $course2->id, $instance->id, 'assign', $user2->id, time());
|
||||
$this->create_test_action_calendar_event('gradingduedate', $course2->id, $instance->id, 'assign', $user2->id, time());
|
||||
|
||||
// Delete all Calendar Events for all Users by Context for Course 1.
|
||||
provider::delete_data_for_all_users_in_context($course1context);
|
||||
|
||||
// The course1 events should still exist, since it belongs to the course and not to the user who created them.
|
||||
// Verify all Calendar Events for Course 1 were deleted.
|
||||
$events = $DB->get_records('event', array('courseid' => $course1->id));
|
||||
$this->assertCount(2, $events);
|
||||
// Course1 subscription should still exist, since it belongs to the course and not to the user who created them.
|
||||
$this->assertCount(0, $events);
|
||||
// Verify all Calendar Subscriptions for Course 1 were deleted.
|
||||
$subscriptions = $DB->get_records('event_subscriptions', array('courseid' => $course1->id));
|
||||
$this->assertCount(1, $subscriptions);
|
||||
$this->assertCount(0, $subscriptions);
|
||||
|
||||
// The course2 events should still exist, since it belongs to the course and not to the user who created them.
|
||||
// Verify all Calendar Events for Course 2 exists still.
|
||||
$events = $DB->get_records('event', array('courseid' => $course2->id));
|
||||
$this->assertCount(4, $events);
|
||||
// Course2 subscription should still exist, since it belongs to the course and not to the user who created them.
|
||||
// Verify all Calendar Subscriptions for Course 2 exists still.
|
||||
$subscriptions = $DB->get_records('event_subscriptions', array('courseid' => $course2->id));
|
||||
$this->assertCount(1, $subscriptions);
|
||||
|
||||
@@ -372,10 +367,10 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
|
||||
// Verify all Calendar Events for Course 2 context were deleted.
|
||||
$events = $DB->get_records('event', array('courseid' => $course2->id, 'modulename' => '0'));
|
||||
$this->assertCount(2, $events);
|
||||
$this->assertCount(0, $events);
|
||||
// Verify all Calendar Subscriptions for Course 2 were deleted.
|
||||
$subscriptions = $DB->get_records('event_subscriptions', array('courseid' => $course2->id));
|
||||
$this->assertCount(1, $subscriptions);
|
||||
$this->assertCount(0, $subscriptions);
|
||||
|
||||
// Verify all Calendar Events for the assignment exists still.
|
||||
$events = $DB->get_records('event', array('modulename' => 'assign'));
|
||||
@@ -384,9 +379,9 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
// Delete all Calendar Events for all Users by Context for the assignment.
|
||||
provider::delete_data_for_all_users_in_context($modulecontext);
|
||||
|
||||
// Verify that the action event still exists since it is not a user override.
|
||||
// Verify all Calendar Events for the assignment context were deleted.
|
||||
$events = $DB->get_records('event', array('modulename' => 'assign'));
|
||||
$this->assertCount(1, $events);
|
||||
$this->assertCount(0, $events);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -406,28 +401,25 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
|
||||
// Add 6 events for user 1 for various contexts.
|
||||
// Add 5 Calendar Events for User 1 for various contexts.
|
||||
$this->setUser($user1);
|
||||
$this->create_test_standard_calendar_event('user', $user1->id, time(), '');
|
||||
$this->create_test_standard_calendar_event('user', $user1->id, time(), '');
|
||||
$this->create_test_standard_calendar_event('site', 0, time(), '', 0, 1);
|
||||
$this->create_test_standard_calendar_event('category', 0, time(), '', $category->id);
|
||||
$this->create_test_standard_calendar_event('course', 0, time(), '', 0, $course1->id);
|
||||
$this->create_test_standard_calendar_event('course', 0, time(), '', 0, $course2->id);
|
||||
$this->create_test_standard_calendar_event('site', $user1->id, time(), '', 0, 1);
|
||||
$this->create_test_standard_calendar_event('category', $user1->id, time(), '', $category->id);
|
||||
$this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course1->id);
|
||||
$this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course2->id);
|
||||
|
||||
// Add two subscription for user 1 at course and user contexts.
|
||||
$this->create_test_calendar_subscription('course', 'https://calendar.google.com/', 0, 0, $course2->id);
|
||||
$this->create_test_calendar_subscription('user', 'https://calendar.google.com/', $user1->id);
|
||||
// Add 1 Calendar Subscription for User 1 at course context.
|
||||
$this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user1->id, 0, $course2->id);
|
||||
|
||||
// Add four events for user 2 for various contexts.
|
||||
// Add 3 Calendar Events for User 2 for various contexts.
|
||||
$this->setUser($user2);
|
||||
$this->create_test_standard_calendar_event('user', $user2->id, time(), '');
|
||||
$this->create_test_standard_calendar_event('user', $user2->id, time(), '');
|
||||
$this->create_test_standard_calendar_event('category', 0, time(), '', $category->id);
|
||||
$this->create_test_standard_calendar_event('course', 0, time(), '', 0, $course1->id);
|
||||
$this->create_test_standard_calendar_event('category', $user2->id, time(), '', $category->id);
|
||||
$this->create_test_standard_calendar_event('course', $user2->id, time(), '', 0, $course1->id);
|
||||
|
||||
// Add a calendar Subscription for User 2 at course context.
|
||||
$this->create_test_calendar_subscription('user', 'https://calendar.google.com/', $user2->id, 0, $course2->id);
|
||||
// Add 1 Calendar Subscription for User 2 at course context.
|
||||
$this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user2->id, 0, $course2->id);
|
||||
|
||||
// Retrieve the user's context ids.
|
||||
$contextlist = provider::get_contexts_for_userid($user1->id);
|
||||
@@ -436,31 +428,17 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
// Delete all Calendar data for User 1.
|
||||
provider::delete_data_for_user($approvedcontextlist);
|
||||
|
||||
// All user events created by user1 should have been removed.
|
||||
// Test all Calendar Events and Subscriptions for User 1 equals zero.
|
||||
$events = $DB->get_records('event', ['userid' => $user1->id]);
|
||||
$this->assertCount(0, $events);
|
||||
// The subscription created by user1 should have been deleted.
|
||||
$eventsubscriptions = $DB->get_records('event_subscriptions', ['userid' => $user1->id]);
|
||||
$this->assertCount(0, $eventsubscriptions);
|
||||
|
||||
// Test all calendar events and subscriptions for user2 still exists and matches the same number created.
|
||||
// Test all Calendar Events and Subscriptions for User 2 still exists and matches the same number created.
|
||||
$events = $DB->get_records('event', ['userid' => $user2->id]);
|
||||
$this->assertCount(2, $events);
|
||||
// The subscription created by user2 should have not been deleted.
|
||||
$this->assertCount(3, $events);
|
||||
$eventsubscriptions = $DB->get_records('event_subscriptions', ['userid' => $user2->id]);
|
||||
$this->assertCount(1, $eventsubscriptions);
|
||||
|
||||
// There should be only six "shared" events created (userid equal zero).
|
||||
// One site, one category and two course events that were created by user1.
|
||||
// One category and one course event created by user2.
|
||||
$events = $DB->get_records('event', ['userid' => 0]);
|
||||
$this->assertCount(6, $events);
|
||||
|
||||
// There should be eight event created in total.
|
||||
// One site, one category and two course events that were created by user1.
|
||||
// One category, one course and two user events created by user2.
|
||||
$events = $DB->get_records('event');
|
||||
$this->assertCount(8, $events);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -496,10 +474,10 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
|
||||
// Add Category Calendar Events for Category.
|
||||
$this->setUser($user1);
|
||||
$this->create_test_standard_calendar_event('category', 0, time(), '',
|
||||
$this->create_test_standard_calendar_event('category', $user1->id, time(), '',
|
||||
$category->id);
|
||||
$this->setUser($user2);
|
||||
$this->create_test_standard_calendar_event('category', 0, time(), '',
|
||||
$this->create_test_standard_calendar_event('category', $user2->id, time(), '',
|
||||
$category->id);
|
||||
|
||||
// Add User Calendar Events for user1 and user2.
|
||||
@@ -515,10 +493,10 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
|
||||
// Add a Course Calendar Events for Course 1.
|
||||
$this->setUser($user1);
|
||||
$this->create_test_standard_calendar_event('course', 0, time(), '',
|
||||
$this->create_test_standard_calendar_event('course', $user1->id, time(), '',
|
||||
0, $course1->id);
|
||||
$this->setUser($user2);
|
||||
$this->create_test_standard_calendar_event('course', 0, time(), '',
|
||||
$this->create_test_standard_calendar_event('course', $user2->id, time(), '',
|
||||
0, $course1->id);
|
||||
|
||||
// Add a Course Assignment Action Calendar Event for Course 2.
|
||||
@@ -536,7 +514,7 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
'assign', $user2->id, time());
|
||||
|
||||
// Add a Calendar Subscription and Group Calendar Event to Course 3.
|
||||
$this->create_test_standard_calendar_event('group', 0, time(), '', 0,
|
||||
$this->create_test_standard_calendar_event('group', $user2->id, time(), '', 0,
|
||||
$course3->id, $course3group->id);
|
||||
$this->setUser($user3);
|
||||
$this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user3->id,
|
||||
@@ -552,22 +530,28 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
$this->assertTrue(in_array($user2->id, $userlist2->get_userids()));
|
||||
// The user list for course1context should not return any users.
|
||||
// The user list for course1context should return user1 and user2.
|
||||
$userlist3 = new \core_privacy\local\request\userlist($course1context, $component);
|
||||
provider::get_users_in_context($userlist3);
|
||||
$this->assertCount(0, $userlist3);
|
||||
$this->assertCount(2, $userlist3);
|
||||
$this->assertTrue(in_array($user1->id, $userlist3->get_userids()));
|
||||
$this->assertTrue(in_array($user2->id, $userlist3->get_userids()));
|
||||
// The user list for course2context should not return any users.
|
||||
$userlist4 = new \core_privacy\local\request\userlist($course2context, $component);
|
||||
provider::get_users_in_context($userlist4);
|
||||
$this->assertCount(0, $userlist4);
|
||||
// The user list for course3context should not return any users.
|
||||
// The user list for course3context should return user2 and user3.
|
||||
$userlist5 = new \core_privacy\local\request\userlist($course3context, $component);
|
||||
provider::get_users_in_context($userlist5);
|
||||
$this->assertCount(0, $userlist5);
|
||||
// The user list for categorycontext should not return any users.
|
||||
$this->assertCount(2, $userlist5);
|
||||
$this->assertTrue(in_array($user2->id, $userlist5->get_userids()));
|
||||
$this->assertTrue(in_array($user3->id, $userlist5->get_userids()));
|
||||
// The user list for categorycontext should return user1 and user2.
|
||||
$userlist6 = new \core_privacy\local\request\userlist($categorycontext, $component);
|
||||
provider::get_users_in_context($userlist6);
|
||||
$this->assertCount(0, $userlist6);
|
||||
$this->assertCount(2, $userlist6);
|
||||
$this->assertTrue(in_array($user1->id, $userlist6->get_userids()));
|
||||
$this->assertTrue(in_array($user2->id, $userlist6->get_userids()));
|
||||
// The user list for modulecontext should return user2.
|
||||
$userlist7 = new \core_privacy\local\request\userlist($modulecontext, $component);
|
||||
provider::get_users_in_context($userlist7);
|
||||
@@ -612,10 +596,10 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
|
||||
// Add Category Calendar Events for Category.
|
||||
$this->setUser($user1);
|
||||
$this->create_test_standard_calendar_event('category', 0, time(), '',
|
||||
$this->create_test_standard_calendar_event('category', $user1->id, time(), '',
|
||||
$category->id);
|
||||
$this->setUser($user2);
|
||||
$this->create_test_standard_calendar_event('category', 0, time(), '',
|
||||
$this->create_test_standard_calendar_event('category', $user2->id, time(), '',
|
||||
$category->id);
|
||||
|
||||
// Add User Calendar Events for user1 and user2.
|
||||
@@ -631,10 +615,10 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
|
||||
// Add a Course Calendar Events for Course 1.
|
||||
$this->setUser($user1);
|
||||
$this->create_test_standard_calendar_event('course', 0, time(), '',
|
||||
$this->create_test_standard_calendar_event('course', $user1->id, time(), '',
|
||||
0, $course1->id);
|
||||
$this->setUser($user2);
|
||||
$this->create_test_standard_calendar_event('course', 0, time(), '',
|
||||
$this->create_test_standard_calendar_event('course', $user2->id, time(), '',
|
||||
0, $course1->id);
|
||||
|
||||
// Add a Course Assignment Action Calendar Event for Course 2.
|
||||
@@ -652,7 +636,7 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
'assign', $user2->id, time());
|
||||
|
||||
// Add a Calendar Subscription and Group Calendar Event to Course 3.
|
||||
$this->create_test_standard_calendar_event('group', 0, time(), '', 0,
|
||||
$this->create_test_standard_calendar_event('group', $user2->id, time(), '', 0,
|
||||
$course3->id, $course3group->id);
|
||||
$this->setUser($user3);
|
||||
$this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user3->id,
|
||||
@@ -666,22 +650,22 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
// The user list for course1context should not return any users.
|
||||
// The user list for course1context should return user1 and user2.
|
||||
$userlist3 = new \core_privacy\local\request\userlist($course1context, $component);
|
||||
provider::get_users_in_context($userlist3);
|
||||
$this->assertCount(0, $userlist3);
|
||||
$this->assertCount(2, $userlist3);
|
||||
// The user list for course2context should not return any users.
|
||||
$userlist4 = new \core_privacy\local\request\userlist($course2context, $component);
|
||||
provider::get_users_in_context($userlist4);
|
||||
$this->assertCount(0, $userlist4);
|
||||
// The user list for course3context should not return any users.
|
||||
// The user list for course3context should return user2 and user3.
|
||||
$userlist5 = new \core_privacy\local\request\userlist($course3context, $component);
|
||||
provider::get_users_in_context($userlist5);
|
||||
$this->assertCount(0, $userlist5);
|
||||
// The user list for categorycontext should not return any users.
|
||||
$this->assertCount(2, $userlist5);
|
||||
// The user list for categorycontext should return user1 and user2.
|
||||
$userlist6 = new \core_privacy\local\request\userlist($categorycontext, $component);
|
||||
provider::get_users_in_context($userlist6);
|
||||
$this->assertCount(0, $userlist6);
|
||||
$this->assertCount(2, $userlist6);
|
||||
// The user list for modulecontext should return user2.
|
||||
$userlist7 = new \core_privacy\local\request\userlist($modulecontext, $component);
|
||||
provider::get_users_in_context($userlist7);
|
||||
@@ -709,25 +693,28 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
$approvedlist2 = new approved_userlist($course1context, $component, []);
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist2);
|
||||
// The user list for course1context should not return any users.
|
||||
// The user list for course1context should return user1 and user2.
|
||||
$userlist3 = new \core_privacy\local\request\userlist($course1context, $component);
|
||||
provider::get_users_in_context($userlist3);
|
||||
$this->assertCount(0, $userlist3);
|
||||
$this->assertCount(2, $userlist3);
|
||||
$this->assertTrue(in_array($user1->id, $userlist3->get_userids()));
|
||||
$this->assertTrue(in_array($user2->id, $userlist3->get_userids()));
|
||||
|
||||
// Convert $userlist3 into an approved_contextlist.
|
||||
// Pass the ID of user1 as a value for the approved user list.
|
||||
$approvedlist2 = new approved_userlist($course1context, $component, [$user1->id]);
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist2);
|
||||
// The user list for course1context should not return any users.
|
||||
// The user list for course1context should return user2.
|
||||
$userlist3 = new \core_privacy\local\request\userlist($course1context, $component);
|
||||
provider::get_users_in_context($userlist3);
|
||||
$this->assertCount(0, $userlist3);
|
||||
$this->assertCount(1, $userlist3);
|
||||
$this->assertTrue(in_array($user2->id, $userlist3->get_userids()));
|
||||
|
||||
// The user list for course3context should not return any users.
|
||||
// The user list for course3context should still return user2 and user3.
|
||||
$userlist5 = new \core_privacy\local\request\userlist($course3context, $component);
|
||||
provider::get_users_in_context($userlist5);
|
||||
$this->assertCount(0, $userlist5);
|
||||
$this->assertCount(2, $userlist5);
|
||||
|
||||
// Convert $userlist6 into an approved_contextlist.
|
||||
$approvedlist3 = new approved_userlist($categorycontext, $component, $userlist6->get_userids());
|
||||
@@ -775,7 +762,7 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
'categoryid' => $categoryid,
|
||||
'courseid' => $courseid,
|
||||
'groupid' => $groupid,
|
||||
'userid' => ($eventtype == 'user') ? $userid : 0,
|
||||
'userid' => $userid,
|
||||
'modulename' => 0,
|
||||
'instance' => 0,
|
||||
'eventtype' => $eventtype,
|
||||
@@ -835,7 +822,7 @@ class core_calendar_privacy_testcase extends provider_testcase {
|
||||
'categoryid' => $categoryid,
|
||||
'courseid' => $courseid,
|
||||
'groupid' => $groupid,
|
||||
'userid' => ($eventtype == 'user') ? $userid : 0,
|
||||
'userid' => $userid,
|
||||
'eventtype' => $eventtype
|
||||
];
|
||||
|
||||
|
||||
@@ -36,12 +36,10 @@ Feature: Restrict activity availability through date conditions
|
||||
| x[year] | 2037 |
|
||||
And I press "Save and return to course"
|
||||
And I log out
|
||||
When I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I am on the "Course 1" course page logged in as student1
|
||||
Then I should see "Available from 31 December 2037"
|
||||
And "Test assignment 1" "link" should not exist in the "page" "region"
|
||||
And "Test assignment 1" activity should be dimmed
|
||||
And "Test assignment 1" "link" should not exist
|
||||
And I log out
|
||||
|
||||
@javascript
|
||||
Scenario: Show activity hidden to students when available until date is in past
|
||||
@@ -60,6 +58,5 @@ Feature: Restrict activity availability through date conditions
|
||||
And I click on ".availability-item .availability-eye img" "css_element"
|
||||
And I press "Save and return to course"
|
||||
And I log out
|
||||
When I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
Then I should not see "Test assignment 2"
|
||||
When I am on the "Course 1" course page logged in as student1
|
||||
Then I should not see "Test assignment 2" in the "page" "region"
|
||||
|
||||
@@ -17,13 +17,16 @@ Feature: Restrict activity availability through grade conditions
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And I log in as "teacher1"
|
||||
And the following "activity" exists:
|
||||
| course | C1 |
|
||||
| activity | assign |
|
||||
| idnumber | Grade assignment |
|
||||
| name | Grade assignment |
|
||||
| assignsubmission_onlinetext_enabled | 1 |
|
||||
| assignsubmission_file_enabled | 0 |
|
||||
| submissiondrafts | 0 |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Assignment" to section "1" and I fill the form with:
|
||||
| Assignment name | Grade assignment |
|
||||
| Description | Grade this assignment to revoke restriction on restricted assignment |
|
||||
| assignsubmission_onlinetext_enabled | 1 |
|
||||
| assignsubmission_file_enabled | 0 |
|
||||
# Adding the page like this because id_availableform_enabled needs to be clicked to trigger the action.
|
||||
And I add a "Page" to section "2"
|
||||
And I expand all fieldsets
|
||||
@@ -31,15 +34,15 @@ Feature: Restrict activity availability through grade conditions
|
||||
And I click on "Grade" "button" in the "Add restriction..." "dialogue"
|
||||
And I click on "min" "checkbox"
|
||||
And I set the following fields to these values:
|
||||
| Name | Test page name |
|
||||
| Description | Restricted page, till grades in Grade assignment is at least 20% |
|
||||
| Page content | Test page contents |
|
||||
| id | Grade assignment |
|
||||
| minval | 20 |
|
||||
| Name | Test page name |
|
||||
| Description | Restricted page, till grades in Grade assignment is at least 20% |
|
||||
| Page content | Test page contents |
|
||||
| id | Grade assignment |
|
||||
| minval | 20 |
|
||||
And I press "Save and return to course"
|
||||
And I log out
|
||||
When I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
When I am on the "Course 1" course page logged in as student1
|
||||
Then I should see "Not available unless: You achieve a required score in Grade assignment"
|
||||
And "Test page name" activity should be dimmed
|
||||
And "Test page name" "link" should not exist
|
||||
@@ -50,9 +53,8 @@ Feature: Restrict activity availability through grade conditions
|
||||
And I press "Save changes"
|
||||
And I should see "Submitted for grading"
|
||||
And I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Grade assignment"
|
||||
|
||||
And I am on the "Grade assignment" "assign activity" page logged in as teacher1
|
||||
And I navigate to "View all submissions" in current page administration
|
||||
And I click on "Grade" "link" in the "Student First" "table_row"
|
||||
And I set the following fields to these values:
|
||||
@@ -61,7 +63,7 @@ Feature: Restrict activity availability through grade conditions
|
||||
And I press "OK"
|
||||
And I follow "Edit settings"
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
And I am on the "Course 1" course page logged in as student1
|
||||
And "Test page name" activity should be visible
|
||||
And I should not see "Not available unless: You achieve a required score in Grade assignment"
|
||||
|
||||
@@ -76,16 +76,14 @@ Feature: Restrict sections availability through completion or grade conditions
|
||||
And I am on "Course 1" course homepage
|
||||
Then I should see "Not available unless: You achieve a required score in Grade assignment"
|
||||
And "Test page name" activity should be hidden
|
||||
And I follow "Grade assignment"
|
||||
And I am on the "Grade assignment" "assign activity" page
|
||||
And I press "Add submission"
|
||||
And I set the following fields to these values:
|
||||
| Online text | I'm the student submission |
|
||||
And I press "Save changes"
|
||||
And I should see "Submitted for grading"
|
||||
And I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Grade assignment"
|
||||
And I am on the "Grade assignment" "assign activity" page logged in as teacher1
|
||||
And I navigate to "View all submissions" in current page administration
|
||||
And I click on "Grade" "link" in the "Student First" "table_row"
|
||||
And I set the following fields to these values:
|
||||
@@ -94,7 +92,6 @@ Feature: Restrict sections availability through completion or grade conditions
|
||||
And I press "OK"
|
||||
And I follow "Edit settings"
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I am on the "Course 1" Course page logged in as student1
|
||||
And "Test page name" activity should be visible
|
||||
And I should not see "Not available unless: You achieve a required score in Grade assignment"
|
||||
|
||||
@@ -44,4 +44,4 @@ Feature: Manage H5P content from the content bank
|
||||
And I expand "Site pages" node
|
||||
And I click on "Content bank" "link"
|
||||
When I follow "filltheblanks.h5p"
|
||||
Then "region-main-settings-menu" "region" should not exist
|
||||
Then "Rename" "link" should not exist in the "region-main-settings-menu" "region"
|
||||
|
||||
@@ -58,7 +58,7 @@ Feature: Delete H5P file from the content bank
|
||||
And I click on "Content bank" "link" in the "Navigation" "block"
|
||||
And I should see "filltheblanks.h5p"
|
||||
And I follow "filltheblanks.h5p"
|
||||
Then ".header-actions-container" "css_element" should not exist
|
||||
Then "Delete" "link" should not exist in the "region-main-settings-menu" "region"
|
||||
And I click on "Content bank" "link"
|
||||
And I click on "Upload" "link"
|
||||
And I click on "Choose a file..." "button"
|
||||
|
||||
@@ -159,3 +159,37 @@ Feature: Content bank use editor feature
|
||||
And I click on "Content bank" "link"
|
||||
And I follow "filltheblanks.h5p"
|
||||
Then "Edit" "link" should not exist in the "region-main" "region"
|
||||
|
||||
Scenario: Teachers keep their content authoring in copied courses
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "contentbank content" exist:
|
||||
| contextlevel | reference | contenttype | user | contentname | filepath |
|
||||
| Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
|
||||
| Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p |
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Copy course" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Course full name | Copy |
|
||||
| Course short name | Copy |
|
||||
| Teacher | 1 |
|
||||
When I press "Copy and view"
|
||||
And I trigger cron
|
||||
And I am on homepage
|
||||
And I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Copy" course homepage
|
||||
And I expand "Site pages" node
|
||||
And I click on "Content bank" "link"
|
||||
And I follow "ipsums.h5p"
|
||||
Then "Edit" "link" should exist in the "region-main" "region"
|
||||
And I click on "Content bank" "link"
|
||||
And I follow "filltheblanks.h5p"
|
||||
Then "Edit" "link" should not exist in the "region-main" "region"
|
||||
|
||||
@@ -24,16 +24,18 @@ Feature: Search content in the content bank
|
||||
And I expand "Site pages" node
|
||||
And I click on "Content bank" "link"
|
||||
And I should see "santjordi.h5p"
|
||||
And "Clear search input" "button" should not exist
|
||||
And "Clear search input" "button" should not be visible
|
||||
And I should not see "items found"
|
||||
When I set the field "Search" to "book"
|
||||
Then "Clear search input" "button" should exist
|
||||
# Waiting for the animation to show the button to finish.
|
||||
And I wait "1" seconds
|
||||
Then "Clear search input" "button" should be visible
|
||||
And I should see "3 items found"
|
||||
And I should see "SantJordi_book"
|
||||
And I should see "mathsbook.h5p"
|
||||
And I should see "historybook.h5p"
|
||||
And I set the field "Search" to "sant"
|
||||
And "Clear search input" "button" should exist
|
||||
And "Clear search input" "button" should be visible
|
||||
And I should see "5 items found"
|
||||
And I set the field "Search" to "santjordi"
|
||||
And I should see "4 items found"
|
||||
@@ -42,10 +44,14 @@ Feature: Search content in the content bank
|
||||
And I should see "SantJordi_book"
|
||||
And I should see "Dragon_santjordi.h5p"
|
||||
And I click on "Clear search input" "button"
|
||||
And "Clear search input" "button" should not exist
|
||||
# Waiting for the animation to hide the button to finish.
|
||||
And I wait "1" seconds
|
||||
And "Clear search input" "button" should not be visible
|
||||
And I should not see "items found"
|
||||
And I set the field "Search" to ".h5p"
|
||||
And "Clear search input" "button" should exist
|
||||
# Waiting for the animation to show the button to finish.
|
||||
And I wait "1" seconds
|
||||
And "Clear search input" "button" should be visible
|
||||
And I should see "7 items found"
|
||||
And I set the field "Search" to "friend"
|
||||
And I should see "0 items found"
|
||||
|
||||
@@ -103,6 +103,10 @@ class no_recent_accesses extends course_enrolments {
|
||||
return get_string('nocoursestudents', 'course');
|
||||
}
|
||||
|
||||
if (!$fortraining && !$course->get_course_data()->visible) {
|
||||
return get_string('hiddenfromstudents');
|
||||
}
|
||||
|
||||
if ($course->get_end() && $course->get_end() < $course->get_start()) {
|
||||
return get_string('errorendbeforestart', 'course');
|
||||
}
|
||||
|
||||
+24
-25
@@ -57,8 +57,6 @@ M.course_dndupload = {
|
||||
// The selector identifying the list of modules within a section (note changing this may require
|
||||
// changes to the get_mods_element function)
|
||||
modslistselector: 'ul.section',
|
||||
// Original onbeforeunload event.
|
||||
originalUnloadEvent: null,
|
||||
|
||||
/**
|
||||
* Initalise the drag and drop upload interface
|
||||
@@ -765,7 +763,10 @@ M.course_dndupload = {
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 1) {
|
||||
this.originalUnloadEvent = window.onbeforeunload;
|
||||
self.reportUploadDirtyState(true);
|
||||
// Trigger form upload start events.
|
||||
require(['core_form/events'], function(FormEvent) {
|
||||
FormEvent.triggerUploadStarted(section.get('id'));
|
||||
});
|
||||
}
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 200) {
|
||||
@@ -793,7 +794,10 @@ M.course_dndupload = {
|
||||
} else {
|
||||
new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
|
||||
}
|
||||
self.reportUploadDirtyState(false);
|
||||
// Trigger form upload complete events.
|
||||
require(['core_form/events'], function(FormEvent) {
|
||||
FormEvent.triggerUploadCompleted(section.get('id'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1025,7 +1029,10 @@ M.course_dndupload = {
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 1) {
|
||||
this.originalUnloadEvent = window.onbeforeunload;
|
||||
self.reportUploadDirtyState(true);
|
||||
// Trigger form upload start events.
|
||||
require(['core_form/events'], function(FormEvent) {
|
||||
FormEvent.triggerUploadStarted(section.get('id'));
|
||||
});
|
||||
}
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 200) {
|
||||
@@ -1043,13 +1050,24 @@ M.course_dndupload = {
|
||||
} else {
|
||||
// Error - remove the dummy element
|
||||
resel.parent.removeChild(resel.li);
|
||||
// Trigger form upload complete events.
|
||||
require(['core_form/events'], function(FormEvent) {
|
||||
FormEvent.triggerUploadCompleted(section.get('id'));
|
||||
});
|
||||
new M.core.alert({message: result.error});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Trigger form upload complete events.
|
||||
require(['core_form/events'], function(FormEvent) {
|
||||
FormEvent.triggerUploadCompleted(section.get('id'));
|
||||
});
|
||||
new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
|
||||
}
|
||||
self.reportUploadDirtyState(false);
|
||||
// Trigger form upload complete events.
|
||||
require(['core_form/events'], function(FormEvent) {
|
||||
FormEvent.triggerUploadCompleted(section.get('id'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1083,24 +1101,5 @@ M.course_dndupload = {
|
||||
if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {
|
||||
M.core.actionmenu.newDOMNode(node);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the event to prevent user navigate away when upload progress still running.
|
||||
*
|
||||
* @param {bool} enable true if upload progress is running, false otherwise
|
||||
*/
|
||||
reportUploadDirtyState: function(enable) {
|
||||
if (!enable) {
|
||||
window.onbeforeunload = this.originalUnloadEvent;
|
||||
} else {
|
||||
window.onbeforeunload = function(e) {
|
||||
var warningMessage = M.util.get_string('changesmadereallygoaway', 'moodle');
|
||||
if (e) {
|
||||
e.returnValue = warningMessage;
|
||||
}
|
||||
return warningMessage;
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+24
-6
@@ -4720,21 +4720,39 @@ function course_get_recent_courses(int $userid = null, int $limit = 0, int $offs
|
||||
$basefields = array('id', 'idnumber', 'summary', 'summaryformat', 'startdate', 'enddate', 'category',
|
||||
'shortname', 'fullname', 'timeaccess', 'component', 'visible');
|
||||
|
||||
$sort = trim($sort);
|
||||
if (empty($sort)) {
|
||||
$sort = 'timeaccess DESC';
|
||||
} else {
|
||||
// The SQL string for sorting can define sorting by multiple columns.
|
||||
$rawsorts = explode(',', $sort);
|
||||
$sorts = array();
|
||||
// Validate and trim the sort parameters in the SQL string for sorting.
|
||||
foreach ($rawsorts as $rawsort) {
|
||||
$rawsort = trim($rawsort);
|
||||
$sorts[] = trim($rawsort);
|
||||
$sort = trim($rawsort);
|
||||
$sortparams = explode(' ', $sort);
|
||||
// A valid sort statement can not have more than 2 params (ex. 'summary desc' or 'timeaccess').
|
||||
if (count($sortparams) > 2) {
|
||||
throw new invalid_parameter_exception(
|
||||
'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].');
|
||||
}
|
||||
$sortfield = trim($sortparams[0]);
|
||||
// Validate the value which defines the field to sort by.
|
||||
if (!in_array($sortfield, $basefields)) {
|
||||
throw new invalid_parameter_exception('Invalid field in the sort parameter, allowed fields: ' .
|
||||
implode(', ', $basefields) . '.');
|
||||
}
|
||||
$sortdirection = isset($sortparams[1]) ? trim($sortparams[1]) : '';
|
||||
// Validate the value which defines the sort direction (if present).
|
||||
$allowedsortdirections = ['asc', 'desc'];
|
||||
if (!empty($sortdirection) && !in_array(strtolower($sortdirection), $allowedsortdirections)) {
|
||||
throw new invalid_parameter_exception('Invalid sort direction in the sort parameter, allowed values: ' .
|
||||
implode(', ', $allowedsortdirections) . '.');
|
||||
}
|
||||
$sorts[] = $sort;
|
||||
}
|
||||
$sort = implode(',', $sorts);
|
||||
}
|
||||
|
||||
$orderby = "ORDER BY $sort";
|
||||
|
||||
$ctxfields = context_helper::get_preload_record_columns_sql('ctx');
|
||||
|
||||
$coursefields = 'c.' .join(',', $basefields);
|
||||
@@ -4767,7 +4785,7 @@ function course_get_recent_courses(int $userid = null, int $limit = 0, int $offs
|
||||
AND ue.timestart < :now1
|
||||
AND (ue.timeend = 0 OR ue.timeend > :now2)
|
||||
))
|
||||
$orderby";
|
||||
ORDER BY $sort";
|
||||
|
||||
$now = round(time(), -2); // Improves db caching.
|
||||
$params = ['userid' => $userid, 'contextlevel' => CONTEXT_COURSE, 'visible' => 1, 'status' => ENROL_USER_ACTIVE,
|
||||
|
||||
@@ -621,10 +621,23 @@ abstract class moodleform_mod extends moodleform {
|
||||
if (!empty($CFG->enableavailability)) {
|
||||
// Add special button to end of previous section if groups/groupings
|
||||
// are enabled.
|
||||
if ($this->_features->groups || $this->_features->groupings) {
|
||||
|
||||
$availabilityplugins = \core\plugininfo\availability::get_enabled_plugins();
|
||||
$groupavailability = $this->_features->groups && array_key_exists('group', $availabilityplugins);
|
||||
$groupingavailability = $this->_features->groupings && array_key_exists('grouping', $availabilityplugins);
|
||||
|
||||
if ($groupavailability || $groupingavailability) {
|
||||
// When creating the button, we need to set type=button to prevent it behaving as a submit.
|
||||
$mform->addElement('static', 'restrictgroupbutton', '',
|
||||
html_writer::tag('button', get_string('restrictbygroup', 'availability'),
|
||||
array('id' => 'restrictbygroup', 'disabled' => 'disabled', 'class' => 'btn btn-secondary')));
|
||||
html_writer::tag('button', get_string('restrictbygroup', 'availability'), [
|
||||
'id' => 'restrictbygroup',
|
||||
'type' => 'button',
|
||||
'disabled' => 'disabled',
|
||||
'class' => 'btn btn-secondary',
|
||||
'data-groupavailability' => $groupavailability,
|
||||
'data-groupingavailability' => $groupingavailability
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
// Availability field. This is just a textarea; the user interface
|
||||
|
||||
@@ -8,32 +8,26 @@ Feature: Edit completion settings of an activity
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | enablecompletion |
|
||||
| Course 1 | C1 | 1 |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Page" to section "1" and I fill the form with:
|
||||
| Name | TestPage |
|
||||
| Description | x |
|
||||
| Page content | x |
|
||||
| Completion tracking | 2 |
|
||||
| Require view | 1 |
|
||||
And I am on "Course 1" course homepage
|
||||
And the following "activities" exist:
|
||||
| activity | course | idnumber | intro | name | completion | completionview |
|
||||
| page | C1 | p1 | x | TestPage | 2 | 1 |
|
||||
|
||||
Scenario: Completion is not locked when the activity has not yet been viewed
|
||||
Given I click on "Edit settings" "link" in the "TestPage" activity
|
||||
Given I am on the TestPage "Page Activity editing" page logged in as admin
|
||||
When I expand all fieldsets
|
||||
Then I should see "Completion tracking"
|
||||
And I should not see "Completion options locked"
|
||||
|
||||
Scenario: Completion is locked after the activity has been viewed
|
||||
Given I follow "TestPage"
|
||||
When I follow "Edit settings"
|
||||
Given I am on the TestPage "Page Activity" page logged in as admin
|
||||
When I am on the TestPage "Page Activity editing" page
|
||||
And I expand all fieldsets
|
||||
Then I should see "Completion options locked"
|
||||
|
||||
@javascript
|
||||
Scenario: Pressing the unlock button allows the user to edit completion settings
|
||||
Given I follow "TestPage"
|
||||
When I navigate to "Edit settings" in current page administration
|
||||
Given I am on the TestPage "Page Activity" page logged in as admin
|
||||
When I am on the TestPage "Page Activity editing" page
|
||||
And I expand all fieldsets
|
||||
And I press "Unlock completion options"
|
||||
Then I should see "Completion options unlocked"
|
||||
@@ -45,8 +39,8 @@ Feature: Edit completion settings of an activity
|
||||
|
||||
@javascript
|
||||
Scenario: Even when completion is locked, the user can still set the date
|
||||
Given I follow "TestPage"
|
||||
When I navigate to "Edit settings" in current page administration
|
||||
Given I am on the TestPage "Page Activity" page logged in as admin
|
||||
And I am on the TestPage "Page Activity editing" page
|
||||
And I expand all fieldsets
|
||||
When I click on "id_completionexpected_enabled" "checkbox"
|
||||
And I set the field "id_completionexpected_year" to "2013"
|
||||
|
||||
@@ -51,8 +51,8 @@ Feature: Display and choose from the available activities in course
|
||||
And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback." in the "help" "core_course > Activity chooser screen"
|
||||
And I should see "Back" in the "help" "core_course > Activity chooser screen"
|
||||
When I click on "Back" "button" in the "help" "core_course > Activity chooser screen"
|
||||
Then "modules" "core_course > Activity chooser screen" should exist
|
||||
And "help" "core_course > Activity chooser screen" should not exist
|
||||
Then "modules" "core_course > Activity chooser screen" should be visible
|
||||
And "help" "core_course > Activity chooser screen" should not be visible
|
||||
And "Back" "button" should not exist in the "modules" "core_course > Activity chooser screen"
|
||||
And I should not see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback." in the "Add an activity or resource" "dialogue"
|
||||
|
||||
@@ -123,24 +123,26 @@ Feature: Display and choose from the available activities in course
|
||||
And I should see "Lesson" in the "Add an activity or resource" "dialogue"
|
||||
When I set the field "search" to ""
|
||||
And I should not see "1 results found" in the "Add an activity or resource" "dialogue"
|
||||
Then ".searchresultscontainer" "css_element" should not exist
|
||||
Then ".searchresultscontainer" "css_element" should not be visible
|
||||
And ".optionscontainer" "css_element" should exist
|
||||
|
||||
Scenario: Teacher can not see a "clear" button if a search query is not entered in the activity chooser search bar
|
||||
When I click on "Add an activity or resource" "button" in the "Topic 1" "section"
|
||||
Then "Clear search input" "button" should not exist
|
||||
Then "Clear search input" "button" should not be visible
|
||||
|
||||
Scenario: Teacher can see a "clear" button after entering a search query in the activity chooser search bar
|
||||
Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
|
||||
When I set the field "search" to "Search query"
|
||||
Then "Clear search input" "button" should not exist
|
||||
Then "Clear search input" "button" should not be visible
|
||||
|
||||
Scenario: Teacher can not see a "clear" button if the search query is removed in the activity chooser search bar
|
||||
Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
|
||||
And I set the field "search" to "Search query"
|
||||
And "Clear search input" "button" should exist
|
||||
When I set the field "search" to ""
|
||||
Then "Clear search input" "button" should not exist
|
||||
# Waiting for the animation to hide the button to finish.
|
||||
And I wait "1" seconds
|
||||
Then "Clear search input" "button" should not be visible
|
||||
|
||||
Scenario: Teacher can instantly remove the search query from the activity search bar by clicking on the "clear" button
|
||||
Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
|
||||
@@ -148,7 +150,7 @@ Feature: Display and choose from the available activities in course
|
||||
And I should see "results found" in the "Add an activity or resource" "dialogue"
|
||||
When I click on "Clear search input" "button"
|
||||
Then I should not see "Search query"
|
||||
And ".searchresultscontainer" "css_element" should not exist
|
||||
And ".searchresultscontainer" "css_element" should not be visible
|
||||
And ".optionscontainer" "css_element" should exist
|
||||
|
||||
Scenario: Teacher gets the base case for the Activity Chooser tab mode
|
||||
|
||||
@@ -45,9 +45,7 @@ Feature: Activity navigation involving activities with access restrictions
|
||||
|
||||
@javascript
|
||||
Scenario: Activity navigation involving activities with access restrictions
|
||||
Given I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I follow "Page 1"
|
||||
Given I am on the "Page 1" "page activity" page logged in as student1
|
||||
Then I should see "Page 2" in the "#next-activity-link" "css_element"
|
||||
# Activity that has access restriction should not show up in the dropdown.
|
||||
And the "Jump to..." select box should not contain "Page 3"
|
||||
@@ -56,7 +54,7 @@ Feature: Activity navigation involving activities with access restrictions
|
||||
And I should see "Page 2" in the "#prev-activity-link" "css_element"
|
||||
And the "Jump to..." select box should not contain "Page 3"
|
||||
# Navigate to Page 2.
|
||||
And I follow "Page 2"
|
||||
And I click on "Page 2" "link" in the "page-content" "region"
|
||||
# Since Page 2 has now been viewed and deemed completed, Page 3 can now be accessed.
|
||||
And I should see "Page 3" in the "#next-activity-link" "css_element"
|
||||
And the "Jump to..." select box should contain "Page 3"
|
||||
|
||||
@@ -1346,44 +1346,6 @@ class behat_course extends behat_base {
|
||||
return $this->getSession()->getPage()->findButton(get_string('turneditingoff')) ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the category with the given idnumber.
|
||||
*
|
||||
* Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return string
|
||||
* @throws ExpectationException
|
||||
*/
|
||||
protected function get_category_id($idnumber) {
|
||||
global $DB;
|
||||
try {
|
||||
return $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber), MUST_EXIST);
|
||||
} catch (dml_missing_record_exception $ex) {
|
||||
throw new ExpectationException(sprintf("There is no category in the database with the idnumber '%s'", $idnumber),
|
||||
$this->getSession());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the course with the given idnumber.
|
||||
*
|
||||
* Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return string
|
||||
* @throws ExpectationException
|
||||
*/
|
||||
protected function get_course_id($idnumber) {
|
||||
global $DB;
|
||||
try {
|
||||
return $DB->get_field('course', 'id', array('idnumber' => $idnumber), MUST_EXIST);
|
||||
} catch (dml_missing_record_exception $ex) {
|
||||
throw new ExpectationException(sprintf("There is no course in the database with the idnumber '%s'", $idnumber),
|
||||
$this->getSession());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the category node from within the listing on the management page.
|
||||
*
|
||||
|
||||
@@ -20,15 +20,12 @@ Feature: Managers can create courses
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add the "Latest announcements" block
|
||||
Then "Latest announcements" "block" should exist
|
||||
And I follow "Announcements"
|
||||
And I am on the Announcements "forum activity" page
|
||||
And "Add a new topic" "link" should exist
|
||||
And "Subscription mode > Forced subscription" "link" should not exist in current page administration
|
||||
And "Subscription mode > Forced subscription" "text" should exist in current page administration
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Announcements"
|
||||
And I am on the Announcements "forum activity" page logged in as student1
|
||||
And "Add a new topic" "link" should not exist
|
||||
And "Forced subscription" "text" should exist in current page administration
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@ Feature: View subfolders in a course in-line
|
||||
| Name | Test folder |
|
||||
| Display folder contents | On a separate page |
|
||||
| Show subfolders expanded | |
|
||||
And I should see "Test folder"
|
||||
And I follow "Test folder"
|
||||
And I am on the "Test folder" "folder activity" page
|
||||
And I press "Edit"
|
||||
And I press "Create folder"
|
||||
And I set the field "New folder name" to "Test subfolder 1"
|
||||
@@ -32,7 +31,7 @@ Feature: View subfolders in a course in-line
|
||||
Scenario: Add a folder with two subfolders - view on separate page
|
||||
Given I am on "Course 1" course homepage
|
||||
And I should not see "Test subfolder 1"
|
||||
And I follow "Test folder"
|
||||
And I am on the "Test folder" "folder activity" page
|
||||
And I should see "Test subfolder 1"
|
||||
And I press "Edit"
|
||||
And I press "Create folder"
|
||||
@@ -41,13 +40,13 @@ Feature: View subfolders in a course in-line
|
||||
And I press "Save changes"
|
||||
When I am on "Course 1" course homepage
|
||||
Then I should not see "Test subfolder 2"
|
||||
And I follow "Test folder"
|
||||
And I am on the "Test folder" "folder activity" page
|
||||
And I should see "Test subfolder 2"
|
||||
Given I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test folder" "folder activity editing" page
|
||||
And I set the field "Show subfolders expanded" to "1"
|
||||
When I am on "Course 1" course homepage
|
||||
Then I should not see "Test subfolder 2"
|
||||
And I follow "Test folder"
|
||||
And I am on the "Test folder" "folder activity" page
|
||||
And I should see "Test subfolder 2"
|
||||
|
||||
@javascript
|
||||
@@ -63,8 +62,7 @@ Feature: View subfolders in a course in-line
|
||||
And I press "Save and return to course"
|
||||
Then I should see "Test subfolder 1"
|
||||
And I should not see "Test sub subfolder"
|
||||
Given I open "Test folder" actions menu
|
||||
When I click on "Edit settings" "link" in the "Test folder" activity
|
||||
And I am on the "Test folder" "folder activity editing" page
|
||||
And I set the field "Show subfolders expanded" to "1"
|
||||
And I press "Save and return to course"
|
||||
Then I should see "Test subfolder 1"
|
||||
|
||||
@@ -4550,7 +4550,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 0,
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 1,
|
||||
'expecteddbqueries' => 4,
|
||||
'expectedresult' => $buildexpectedresult(0, 0)
|
||||
],
|
||||
'less than query limit' => [
|
||||
@@ -4558,7 +4558,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 2,
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 1,
|
||||
'expecteddbqueries' => 2,
|
||||
'expectedresult' => $buildexpectedresult(2, 0)
|
||||
],
|
||||
'more than query limit' => [
|
||||
@@ -4566,7 +4566,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 7,
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 3,
|
||||
'expecteddbqueries' => 4,
|
||||
'expectedresult' => $buildexpectedresult(7, 0)
|
||||
],
|
||||
'limit less than query limit' => [
|
||||
@@ -4574,7 +4574,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 7,
|
||||
'limit' => 2,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 1,
|
||||
'expecteddbqueries' => 2,
|
||||
'expectedresult' => $buildexpectedresult(2, 0)
|
||||
],
|
||||
'limit less than query limit with offset' => [
|
||||
@@ -4582,7 +4582,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 7,
|
||||
'limit' => 2,
|
||||
'offset' => 2,
|
||||
'expecteddbqueries' => 1,
|
||||
'expecteddbqueries' => 2,
|
||||
'expectedresult' => $buildexpectedresult(2, 2)
|
||||
],
|
||||
'limit less than total' => [
|
||||
@@ -4590,7 +4590,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 9,
|
||||
'limit' => 6,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 2,
|
||||
'expecteddbqueries' => 3,
|
||||
'expectedresult' => $buildexpectedresult(6, 0)
|
||||
],
|
||||
'less results than limit' => [
|
||||
@@ -4598,7 +4598,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 9,
|
||||
'limit' => 20,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 3,
|
||||
'expecteddbqueries' => 4,
|
||||
'expectedresult' => $buildexpectedresult(9, 0)
|
||||
],
|
||||
'less results than limit exact divisible' => [
|
||||
@@ -4606,7 +4606,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 9,
|
||||
'limit' => 20,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 4,
|
||||
'expecteddbqueries' => 5,
|
||||
'expectedresult' => $buildexpectedresult(9, 0)
|
||||
],
|
||||
'less results than limit with offset' => [
|
||||
@@ -4614,7 +4614,7 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
'totalcourses' => 9,
|
||||
'limit' => 10,
|
||||
'offset' => 5,
|
||||
'expecteddbqueries' => 2,
|
||||
'expecteddbqueries' => 3,
|
||||
'expectedresult' => $buildexpectedresult(4, 5)
|
||||
],
|
||||
];
|
||||
@@ -5442,7 +5442,16 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
// Every course accessed, order by shortname DESC. The last create course ($course[2]) should have the greater shortname.
|
||||
$result = course_get_recent_courses($student->id, 0, 0, 'shortname DESC');
|
||||
$this->assertCount(3, $result);
|
||||
$this->assertEquals($courses[2]->id, array_shift($result)->id);
|
||||
$this->assertEquals($courses[2]->id, array_values($result)[0]->id);
|
||||
$this->assertEquals($courses[1]->id, array_values($result)[1]->id);
|
||||
$this->assertEquals($courses[0]->id, array_values($result)[2]->id);
|
||||
|
||||
// Every course accessed, order by shortname ASC.
|
||||
$result = course_get_recent_courses($student->id, 0, 0, 'shortname ASC');
|
||||
$this->assertCount(3, $result);
|
||||
$this->assertEquals($courses[0]->id, array_values($result)[0]->id);
|
||||
$this->assertEquals($courses[1]->id, array_values($result)[1]->id);
|
||||
$this->assertEquals($courses[2]->id, array_values($result)[2]->id);
|
||||
|
||||
$guestcourse = $generator->create_course(
|
||||
(object)array('shortname' => 'guestcourse',
|
||||
@@ -5464,6 +5473,56 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
$this->assertArrayNotHasKey($courses[0]->id, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the validation of the sort value in course_get_recent_courses().
|
||||
*
|
||||
* @dataProvider course_get_recent_courses_sort_validation_provider
|
||||
* @param string $sort The sort value
|
||||
* @param string $expectedexceptionmsg The expected exception message
|
||||
*/
|
||||
public function test_course_get_recent_courses_sort_validation(string $sort, string $expectedexceptionmsg) {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
if (!empty($expectedexceptionmsg)) {
|
||||
$this->expectException('invalid_parameter_exception');
|
||||
$this->expectExceptionMessage($expectedexceptionmsg);
|
||||
}
|
||||
course_get_recent_courses($user->id, 0, 0, $sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_course_get_recent_courses_sort_validation().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function course_get_recent_courses_sort_validation_provider() {
|
||||
return [
|
||||
'Invalid sort format (SQL injection attempt)' =>
|
||||
[
|
||||
'shortname DESC LIMIT 1--',
|
||||
'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].',
|
||||
],
|
||||
'Sort uses \'sort by\' field that does not exist' =>
|
||||
[
|
||||
'shortname DESC, xyz ASC',
|
||||
'Invalid field in the sort parameter, allowed fields: id, idnumber, summary, summaryformat, ' .
|
||||
'startdate, enddate, category, shortname, fullname, timeaccess, component, visible.',
|
||||
],
|
||||
'Sort uses invalid value for the sorting direction' =>
|
||||
[
|
||||
'shortname xyz, lastaccess',
|
||||
'Invalid sort direction in the sort parameter, allowed values: asc, desc.',
|
||||
],
|
||||
'Valid sort format' =>
|
||||
[
|
||||
'shortname asc, timeaccess',
|
||||
''
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the course_get_recent_courses function.
|
||||
*/
|
||||
|
||||
@@ -2765,7 +2765,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
/**
|
||||
* Test cases for the get_enrolled_courses_by_timeline_classification test.
|
||||
*/
|
||||
public function get_get_enrolled_courses_by_timeline_classification_test_cases() {
|
||||
public function get_get_enrolled_courses_by_timeline_classification_test_cases():array {
|
||||
$now = time();
|
||||
$day = 86400;
|
||||
|
||||
@@ -2864,6 +2864,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 2,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0
|
||||
],
|
||||
@@ -2873,6 +2874,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
|
||||
'expectednextoffset' => 15
|
||||
],
|
||||
@@ -2881,6 +2883,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 2,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => ['afuture', 'bfuture'],
|
||||
'expectednextoffset' => 4
|
||||
],
|
||||
@@ -2889,6 +2892,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 2,
|
||||
'offset' => 2,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => ['bfuture', 'cfuture'],
|
||||
'expectednextoffset' => 7
|
||||
],
|
||||
@@ -2897,6 +2901,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 5,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
|
||||
'expectednextoffset' => 13
|
||||
],
|
||||
@@ -2905,6 +2910,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 10,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
|
||||
'expectednextoffset' => 15
|
||||
],
|
||||
@@ -2913,6 +2919,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'future',
|
||||
'limit' => 10,
|
||||
'offset' => 5,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
|
||||
'expectednextoffset' => 15
|
||||
],
|
||||
@@ -2921,6 +2928,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'all',
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => [
|
||||
'afuture',
|
||||
'ainprogress',
|
||||
@@ -2945,6 +2953,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 0,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => [
|
||||
'afuture',
|
||||
'ainprogress',
|
||||
@@ -2959,6 +2968,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => [
|
||||
'bpast',
|
||||
'cfuture',
|
||||
@@ -2973,9 +2983,161 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 50,
|
||||
'sort' => 'shortname ASC',
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 50
|
||||
],
|
||||
'all limit and offset with sort ul.timeaccess desc' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'inprogress',
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'sort' => 'ul.timeaccess desc',
|
||||
'expectedcourses' => [
|
||||
'ainprogress',
|
||||
'binprogress',
|
||||
'cinprogress',
|
||||
'dinprogress',
|
||||
'einprogress'
|
||||
],
|
||||
'expectednextoffset' => 15
|
||||
],
|
||||
'all limit and offset with sort sql injection for sort or 1==1' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => 'ul.timeaccess desc or 1==1',
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with sql injection of sort a custom one' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "ul.timeaccess LIMIT 1--",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong sort direction' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "ul.timeaccess abcdasc",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong sort direction' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "ul.timeaccess.foo ascd",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong sort param' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "foobar",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong field name' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "ul.foobar",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong field separator' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "ul.timeaccess.foo",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong field separator #' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "ul#timeaccess",
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong field separator $' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => 'ul$timeaccess',
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with wrong field name' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => 'timeaccess123',
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0,
|
||||
'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()'
|
||||
],
|
||||
'all limit and offset with no sort direction for ul' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'inprogress',
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'sort' => "ul.timeaccess",
|
||||
'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
|
||||
'expectednextoffset' => 15,
|
||||
],
|
||||
'all limit and offset with valid field name and no prefix, test for ul' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'inprogress',
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'sort' => "timeaccess",
|
||||
'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
|
||||
'expectednextoffset' => 15,
|
||||
],
|
||||
'all limit and offset with valid field name and no prefix' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "fullname",
|
||||
'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
|
||||
'expectednextoffset' => 10,
|
||||
],
|
||||
'all limit and offset with valid field name and no prefix and with sort direction' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'all',
|
||||
'limit' => 5,
|
||||
'offset' => 5,
|
||||
'sort' => "fullname desc",
|
||||
'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
|
||||
'expectednextoffset' => 10,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2987,16 +3149,20 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
* @param string $classification Timeline classification
|
||||
* @param int $limit Maximum number of results
|
||||
* @param int $offset Offset the unfiltered courses result set by this amount
|
||||
* @param string $sort sort the courses
|
||||
* @param array $expectedcourses Expected courses in result
|
||||
* @param int $expectednextoffset Expected next offset value in result
|
||||
* @param string|null $expectedexception Expected exception string
|
||||
*/
|
||||
public function test_get_enrolled_courses_by_timeline_classification(
|
||||
$coursedata,
|
||||
$classification,
|
||||
$limit,
|
||||
$offset,
|
||||
$sort,
|
||||
$expectedcourses,
|
||||
$expectednextoffset
|
||||
$expectednextoffset,
|
||||
$expectedexception = null
|
||||
) {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
@@ -3013,6 +3179,11 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
|
||||
$this->setUser($student);
|
||||
|
||||
if (isset($expectedexception)) {
|
||||
$this->expectException('coding_exception');
|
||||
$this->expectExceptionMessage($expectedexception);
|
||||
}
|
||||
|
||||
// NOTE: The offset applies to the unfiltered full set of courses before the classification
|
||||
// filtering is done.
|
||||
// E.g. In our example if an offset of 2 is given then it would mean the first
|
||||
@@ -3021,7 +3192,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
$classification,
|
||||
$limit,
|
||||
$offset,
|
||||
'shortname ASC'
|
||||
$sort
|
||||
);
|
||||
$result = external_api::clean_returnvalue(
|
||||
core_course_external::get_enrolled_courses_by_timeline_classification_returns(),
|
||||
@@ -3032,7 +3203,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
||||
return $course['shortname'];
|
||||
}, $result['courses']);
|
||||
|
||||
$this->assertEquals($expectedcourses, $actual);
|
||||
$this->assertEqualsCanonicalizing($expectedcourses, $actual);
|
||||
$this->assertEquals($expectednextoffset, $result['nextoffset']);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1,2 +1,2 @@
|
||||
define ("core_customfield/form",["jquery","core/str","core/notification","core/ajax","core/templates","core/sortable_list","core/inplace_editable"],function(a,b,c,d,e,f){var g=function(f,g,h,i,j){b.get_strings([{key:"confirm"},{key:"confirmdelete"+g,component:"core_customfield"},{key:"yes"},{key:"no"}]).done(function(b){c.confirm(b[0],b[1],b[2],b[3],function(){var b="field"===g?"core_customfield_delete_field":"core_customfield_delete_category";d.call([{methodname:b,args:{id:f}},{methodname:"core_customfield_reload_template",args:{component:h,area:i,itemid:j}}])[1].then(function(a){return e.render("core_customfield/list",a)}).then(function(b,c){e.replaceNode(a("[data-region=\"list-page\"]"),b,c);return null}).fail(c.exception)})}).fail(c.exception)},h=function(b,f,g){var h=d.call([{methodname:"core_customfield_create_category",args:{component:b,area:f,itemid:g}},{methodname:"core_customfield_reload_template",args:{component:b,area:f,itemid:g}}]),i;h[0].then(function(a){i=a;return null}).fail(c.exception);h[1].then(function(a){return e.render("core_customfield/list",a)}).then(function(b,c){e.replaceNode(a("[data-region=\"list-page\"]"),b,c);window.location.href="#category-"+i;return null}).fail(c.exception)};return{init:function init(){var e=a("#customfield_catlist"),i=e.attr("data-component"),j=e.attr("data-area"),k=e.attr("data-itemid");a("[data-role=deletefield]").on("click",function(b){g(a(this).attr("data-id"),"field",i,j,k);b.preventDefault()});a("[data-role=deletecategory]").on("click",function(b){g(a(this).attr("data-id"),"category",i,j,k);b.preventDefault()});a("[data-role=addnewcategory]").on("click",function(){h(i,j,k)});var l=function(a){return a.closest("[data-category-id]").find("[data-inplaceeditable][data-itemtype=category][data-component=core_customfield]").attr("data-value")},m=new f(a("#customfield_catlist .categorieslist"),{moveHandlerSelector:".movecategory [data-drag-type=move]"});m.getElementName=function(b){return a.Deferred().resolve(l(b))};a("[data-category-id]").on("sortablelist-drop",function(a,b){if(b.positionChanged){var e=d.call([{methodname:"core_customfield_move_category",args:{id:b.element.data("category-id"),beforeid:b.targetNextElement.data("category-id")}}]);e[0].fail(c.exception)}a.stopPropagation()});var n=new f(a("#customfield_catlist .fieldslist tbody"),{moveHandlerSelector:".movefield [data-drag-type=move]"});n.getDestinationName=function(c,d){if(!d.length){return b.get_string("totopofcategory","customfield",l(c))}else if(d.attr("data-field-name")){return b.get_string("afterfield","customfield",d.attr("data-field-name"))}else{return a.Deferred().resolve("")}};a("[data-field-name]").on("sortablelist-drop",function(a,b){a.stopPropagation();if(b.positionChanged){var e=d.call([{methodname:"core_customfield_move_field",args:{id:b.element.data("field-id"),beforeid:b.targetNextElement.data("field-id"),categoryid:+b.targetList.closest("[data-category-id]").attr("data-category-id")}}]);e[0].fail(c.exception)}}).on("sortablelist-drag",function(d){d.stopPropagation();b.get_string("therearenofields","core_customfield").then(function(b){a("#customfield_catlist .categorieslist").children().each(function(){var c=a(this).find(a(".field")),d=a(this).find(a(".nofields"));if(!c.length&&!d.length){a(this).find("tbody").append("<tr class=\"nofields\"><td colspan=\"5\">"+b+"</td></tr>")}if(c.length&&d.length){d.remove()}});return null}).fail(c.exception)});a("[data-category-id], [data-field-name]").on("sortablelist-dragstart",function(b,c){setTimeout(function(){a(".sortable-list-is-dragged").width(c.element.width())},501)})}}});
|
||||
define ("core_customfield/form",["jquery","core/str","core/notification","core/ajax","core/templates","core/sortable_list","core/pending","core/inplace_editable"],function(a,b,c,d,e,f,g){var h=function(f,h,i,j,k){var l=new g("core_customfield/form:confirmDelete");b.get_strings([{key:"confirm"},{key:"confirmdelete"+h,component:"core_customfield"},{key:"yes"},{key:"no"}]).then(function(b){c.confirm(b[0],b[1],b[2],b[3],function(){var b=new g("core_customfield/form:confirmDelete"),l="field"===h?"core_customfield_delete_field":"core_customfield_delete_category";d.call([{methodname:l,args:{id:f}},{methodname:"core_customfield_reload_template",args:{component:i,area:j,itemid:k}}])[1].then(function(a){return e.render("core_customfield/list",a)}).then(function(b,c){e.replaceNode(a("[data-region=\"list-page\"]"),b,c);return null}).then(b.resolve).catch(c.exception)})}).then(l.resolve).catch(c.exception)},i=function(b,f,h){var i=new g("core_customfield/form:confirmDelete"),j=d.call([{methodname:"core_customfield_create_category",args:{component:b,area:f,itemid:h}},{methodname:"core_customfield_reload_template",args:{component:b,area:f,itemid:h}}]),k;j[0].then(function(a){k=a;return null}).catch(c.exception);j[1].then(function(a){return e.render("core_customfield/list",a)}).then(function(b,c){e.replaceNode(a("[data-region=\"list-page\"]"),b,c);window.location.href="#category-"+k;return null}).catch(c.exception);Promise.all(j).then(i.resolve).catch()};return{init:function init(){var e=a("#customfield_catlist"),j=e.attr("data-component"),k=e.attr("data-area"),l=e.attr("data-itemid");a("[data-role=deletefield]").on("click",function(b){h(a(this).attr("data-id"),"field",j,k,l);b.preventDefault()});a("[data-role=deletecategory]").on("click",function(b){h(a(this).attr("data-id"),"category",j,k,l);b.preventDefault()});a("[data-role=addnewcategory]").on("click",function(){i(j,k,l)});var m=function(a){return a.closest("[data-category-id]").find("[data-inplaceeditable][data-itemtype=category][data-component=core_customfield]").attr("data-value")},n=new f(a("#customfield_catlist .categorieslist"),{moveHandlerSelector:".movecategory [data-drag-type=move]"});n.getElementName=function(b){return a.Deferred().resolve(m(b))};a("[data-category-id]").on("sortablelist-drop",function(a,b){if(b.positionChanged){var e=new g("core_customfield/form:categoryid:on:sortablelist-drop");d.call([{methodname:"core_customfield_move_category",args:{id:b.element.data("category-id"),beforeid:b.targetNextElement.data("category-id")}}])[0].then(e.resolve).catch(c.exception)}a.stopPropagation()});var o=new f(a("#customfield_catlist .fieldslist tbody"),{moveHandlerSelector:".movefield [data-drag-type=move]"});o.getDestinationName=function(c,d){if(!d.length){return b.get_string("totopofcategory","customfield",m(c))}else if(d.attr("data-field-name")){return b.get_string("afterfield","customfield",d.attr("data-field-name"))}else{return a.Deferred().resolve("")}};a("[data-field-name]").on("sortablelist-drop",function(a,b){a.stopPropagation();if(b.positionChanged){var e=new g("core_customfield/form:fieldname:on:sortablelist-drop");d.call([{methodname:"core_customfield_move_field",args:{id:b.element.data("field-id"),beforeid:b.targetNextElement.data("field-id"),categoryid:+b.targetList.closest("[data-category-id]").attr("data-category-id")}}])[0].then(e.resolve).catch(c.exception)}});a("[data-field-name]").on("sortablelist-drag",function(d){var e=new g("core_customfield/form:fieldname:on:sortablelist-drag");d.stopPropagation();b.get_string("therearenofields","core_customfield").then(function(b){a("#customfield_catlist .categorieslist").children().each(function(){var c=a(this).find(a(".field")),d=a(this).find(a(".nofields"));if(!c.length&&!d.length){a(this).find("tbody").append("<tr class=\"nofields\"><td colspan=\"5\">"+b+"</td></tr>")}if(c.length&&d.length){d.remove()}});return null}).then(e.resolve).catch(c.exception)});a("[data-category-id], [data-field-name]").on("sortablelist-dragstart",function(b,c){setTimeout(function(){a(".sortable-list-is-dragged").width(c.element.width())},501)})}}});
|
||||
//# sourceMappingURL=form.min.js.map
|
||||
|
||||
File diff suppressed because one or more lines are too long
+66
-27
@@ -21,9 +21,16 @@
|
||||
* @copyright 2018 Toni Barbera
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(['jquery', 'core/str', 'core/notification', 'core/ajax', 'core/templates', 'core/sortable_list', 'core/inplace_editable'],
|
||||
function(
|
||||
$, Str, Notification, Ajax, Templates, SortableList) {
|
||||
define([
|
||||
'jquery',
|
||||
'core/str',
|
||||
'core/notification',
|
||||
'core/ajax',
|
||||
'core/templates',
|
||||
'core/sortable_list',
|
||||
'core/pending',
|
||||
'core/inplace_editable',
|
||||
], function($, Str, Notification, Ajax, Templates, SortableList, Pending) {
|
||||
|
||||
/**
|
||||
* Display confirmation dialogue
|
||||
@@ -35,25 +42,36 @@ define(['jquery', 'core/str', 'core/notification', 'core/ajax', 'core/templates'
|
||||
* @param {Number} itemid
|
||||
*/
|
||||
var confirmDelete = function(id, type, component, area, itemid) {
|
||||
var pendingPromise = new Pending('core_customfield/form:confirmDelete');
|
||||
Str.get_strings([
|
||||
{'key': 'confirm'},
|
||||
{'key': 'confirmdelete' + type, component: 'core_customfield'},
|
||||
{'key': 'yes'},
|
||||
{'key': 'no'},
|
||||
]).done(function(s) {
|
||||
])
|
||||
.then(function(s) {
|
||||
Notification.confirm(s[0], s[1], s[2], s[3], function() {
|
||||
var pendingDeletePromise = new Pending('core_customfield/form:confirmDelete');
|
||||
var func = (type === 'field') ? 'core_customfield_delete_field' : 'core_customfield_delete_category';
|
||||
Ajax.call([
|
||||
{methodname: func, args: {id: id}},
|
||||
{methodname: 'core_customfield_reload_template', args: {component: component, area: area, itemid: itemid}}
|
||||
])[1].then(function(response) {
|
||||
])[1]
|
||||
.then(function(response) {
|
||||
return Templates.render('core_customfield/list', response);
|
||||
}).then(function(html, js) {
|
||||
})
|
||||
.then(function(html, js) {
|
||||
Templates.replaceNode($('[data-region="list-page"]'), html, js);
|
||||
return null;
|
||||
}).fail(Notification.exception);
|
||||
})
|
||||
.then(pendingDeletePromise.resolve)
|
||||
.catch(Notification.exception);
|
||||
});
|
||||
}).fail(Notification.exception);
|
||||
|
||||
return;
|
||||
})
|
||||
.then(pendingPromise.resolve)
|
||||
.catch(Notification.exception);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -64,24 +82,31 @@ define(['jquery', 'core/str', 'core/notification', 'core/ajax', 'core/templates'
|
||||
* @param {Number} itemid
|
||||
*/
|
||||
var createNewCategory = function(component, area, itemid) {
|
||||
var pendingPromise = new Pending('core_customfield/form:confirmDelete');
|
||||
var promises = Ajax.call([
|
||||
{methodname: 'core_customfield_create_category', args: {component: component, area: area, itemid: itemid}},
|
||||
{methodname: 'core_customfield_reload_template', args: {component: component, area: area, itemid: itemid}}
|
||||
]),
|
||||
categoryid;
|
||||
{methodname: 'core_customfield_create_category', args: {component: component, area: area, itemid: itemid}},
|
||||
{methodname: 'core_customfield_reload_template', args: {component: component, area: area, itemid: itemid}}
|
||||
]);
|
||||
var categoryid;
|
||||
|
||||
promises[0].then(function(response) {
|
||||
categoryid = response;
|
||||
return null;
|
||||
}).fail(Notification.exception);
|
||||
}).catch(Notification.exception);
|
||||
|
||||
promises[1].then(function(response) {
|
||||
return Templates.render('core_customfield/list', response);
|
||||
}).then(function(html, js) {
|
||||
})
|
||||
.then(function(html, js) {
|
||||
Templates.replaceNode($('[data-region="list-page"]'), html, js);
|
||||
window.location.href = '#category-' + categoryid;
|
||||
return null;
|
||||
}).fail(Notification.exception);
|
||||
})
|
||||
.catch(Notification.exception);
|
||||
|
||||
Promise.all(promises)
|
||||
.then(pendingPromise.resolve)
|
||||
.catch();
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -89,18 +114,21 @@ define(['jquery', 'core/str', 'core/notification', 'core/ajax', 'core/templates'
|
||||
* Initialise the custom fields manager
|
||||
*/
|
||||
init: function() {
|
||||
var mainlist = $('#customfield_catlist'),
|
||||
component = mainlist.attr('data-component'),
|
||||
area = mainlist.attr('data-area'),
|
||||
itemid = mainlist.attr('data-itemid');
|
||||
var mainlist = $('#customfield_catlist');
|
||||
var component = mainlist.attr('data-component');
|
||||
var area = mainlist.attr('data-area');
|
||||
var itemid = mainlist.attr('data-itemid');
|
||||
|
||||
$("[data-role=deletefield]").on('click', function(e) {
|
||||
confirmDelete($(this).attr('data-id'), 'field', component, area, itemid);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$("[data-role=deletecategory]").on('click', function(e) {
|
||||
confirmDelete($(this).attr('data-id'), 'category', component, area, itemid);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$('[data-role=addnewcategory]').on('click', function() {
|
||||
createNewCategory(component, area, itemid);
|
||||
});
|
||||
@@ -124,7 +152,8 @@ define(['jquery', 'core/str', 'core/notification', 'core/ajax', 'core/templates'
|
||||
|
||||
$('[data-category-id]').on('sortablelist-drop', function(evt, info) {
|
||||
if (info.positionChanged) {
|
||||
var promises = Ajax.call([
|
||||
var pendingPromise = new Pending('core_customfield/form:categoryid:on:sortablelist-drop');
|
||||
Ajax.call([
|
||||
{
|
||||
methodname: 'core_customfield_move_category',
|
||||
args: {
|
||||
@@ -133,8 +162,9 @@ define(['jquery', 'core/str', 'core/notification', 'core/ajax', 'core/templates'
|
||||
}
|
||||
|
||||
},
|
||||
]);
|
||||
promises[0].fail(Notification.exception);
|
||||
])[0]
|
||||
.then(pendingPromise.resolve)
|
||||
.catch(Notification.exception);
|
||||
}
|
||||
evt.stopPropagation(); // Important for nested lists to prevent multiple targets.
|
||||
});
|
||||
@@ -158,7 +188,8 @@ define(['jquery', 'core/str', 'core/notification', 'core/ajax', 'core/templates'
|
||||
$('[data-field-name]').on('sortablelist-drop', function(evt, info) {
|
||||
evt.stopPropagation(); // Important for nested lists to prevent multiple targets.
|
||||
if (info.positionChanged) {
|
||||
var promises = Ajax.call([
|
||||
var pendingPromise = new Pending('core_customfield/form:fieldname:on:sortablelist-drop');
|
||||
Ajax.call([
|
||||
{
|
||||
methodname: 'core_customfield_move_field',
|
||||
args: {
|
||||
@@ -167,11 +198,17 @@ define(['jquery', 'core/str', 'core/notification', 'core/ajax', 'core/templates'
|
||||
categoryid: Number(info.targetList.closest('[data-category-id]').attr('data-category-id'))
|
||||
},
|
||||
},
|
||||
]);
|
||||
promises[0].fail(Notification.exception);
|
||||
])[0]
|
||||
.then(pendingPromise.resolve)
|
||||
.catch(Notification.exception);
|
||||
}
|
||||
}).on('sortablelist-drag', function(evt) {
|
||||
});
|
||||
|
||||
$('[data-field-name]').on('sortablelist-drag', function(evt) {
|
||||
var pendingPromise = new Pending('core_customfield/form:fieldname:on:sortablelist-drag');
|
||||
|
||||
evt.stopPropagation(); // Important for nested lists to prevent multiple targets.
|
||||
|
||||
// Refreshing fields tables.
|
||||
Str.get_string('therearenofields', 'core_customfield').then(function(s) {
|
||||
$('#customfield_catlist .categorieslist').children().each(function() {
|
||||
@@ -187,7 +224,9 @@ define(['jquery', 'core/str', 'core/notification', 'core/ajax', 'core/templates'
|
||||
}
|
||||
});
|
||||
return null;
|
||||
}).fail(Notification.exception);
|
||||
})
|
||||
.then(pendingPromise.resolve)
|
||||
.catch(Notification.exception);
|
||||
});
|
||||
|
||||
$('[data-category-id], [data-field-name]').on('sortablelist-dragstart',
|
||||
|
||||
@@ -55,12 +55,16 @@ class field_controller extends \core_customfield\field_controller {
|
||||
|
||||
$mform->addElement('text', 'configdata[displaysize]', get_string('displaysize', 'customfield_text'), ['size' => 6]);
|
||||
$mform->setType('configdata[displaysize]', PARAM_INT);
|
||||
$mform->setDefault('configdata[displaysize]', 50);
|
||||
if (!$this->get_configdata_property('displaysize')) {
|
||||
$mform->setDefault('configdata[displaysize]', 50);
|
||||
}
|
||||
$mform->addRule('configdata[displaysize]', null, 'numeric', null, 'client');
|
||||
|
||||
$mform->addElement('text', 'configdata[maxlength]', get_string('maxlength', 'customfield_text'), ['size' => 6]);
|
||||
$mform->setType('configdata[maxlength]', PARAM_INT);
|
||||
$mform->setDefault('configdata[maxlength]', 1333);
|
||||
if (!$this->get_configdata_property('maxlength')) {
|
||||
$mform->setDefault('configdata[maxlength]', 1333);
|
||||
}
|
||||
$mform->addRule('configdata[maxlength]', null, 'numeric', null, 'client');
|
||||
|
||||
$mform->addElement('selectyesno', 'configdata[ispassword]', get_string('ispassword', 'customfield_text'));
|
||||
@@ -113,7 +117,7 @@ class field_controller extends \core_customfield\field_controller {
|
||||
$errors['configdata[link]'] = get_string('errorconfiglinkplaceholder', 'customfield_text');
|
||||
} else if (!validateUrlSyntax(str_replace('$$', 'XYZ', $link), 's+H?S?F-E-u-P-a?I?p?f?q?r?')) {
|
||||
// This validation is more strict than PARAM_URL - it requires the protocol and it must be either http or https.
|
||||
$errors['configdata[link]'] = get_string('errorconfigdisplaysize', 'customfield_text');
|
||||
$errors['configdata[link]'] = get_string('errorconfiglinksyntax', 'customfield_text');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
This files describes API changes in /dataformat/ download system,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 3.9.8 ===
|
||||
* Optional $callback for the following \core\dataformat methods now receive a second argument to define
|
||||
whether the current format writer supports HTML:
|
||||
- download_data()
|
||||
- write_data()
|
||||
- write_data_to_filearea()
|
||||
|
||||
=== 3.9 ===
|
||||
* The following methods have been added to the base dataformat class to allow instances to export to a local
|
||||
file. They can be overridden in extending classes to define how files should be created:
|
||||
|
||||
@@ -188,7 +188,9 @@ $removeenabled = $canunenrol ? '' : 'disabled="disabled"';
|
||||
</td>
|
||||
<td id="buttonscell">
|
||||
<div id="addcontrols">
|
||||
<input name="add" <?php echo $addenabled; ?> id="add" type="submit" value="<?php echo $OUTPUT->larrow().' '.get_string('add'); ?>" title="<?php print_string('add'); ?>" /><br />
|
||||
<input class="btn btn-secondary" name="add" <?php echo $addenabled; ?> id="add" type="submit"
|
||||
value="<?php echo $OUTPUT->larrow() . ' ' . get_string('add'); ?>"
|
||||
title="<?php print_string('add'); ?>" /><br />
|
||||
|
||||
<div class="enroloptions">
|
||||
|
||||
@@ -205,7 +207,9 @@ $removeenabled = $canunenrol ? '' : 'disabled="disabled"';
|
||||
</div>
|
||||
|
||||
<div id="removecontrols">
|
||||
<input name="remove" id="remove" <?php echo $removeenabled; ?> type="submit" value="<?php echo get_string('remove').' '.$OUTPUT->rarrow(); ?>" title="<?php print_string('remove'); ?>" />
|
||||
<input class="btn btn-secondary" name="remove" id="remove" <?php echo $removeenabled; ?> type="submit"
|
||||
value="<?php echo get_string('remove') . ' ' . $OUTPUT->rarrow(); ?>"
|
||||
title="<?php print_string('remove'); ?>" />
|
||||
</div>
|
||||
</td>
|
||||
<td id="potentialcell">
|
||||
|
||||
@@ -1055,7 +1055,8 @@ class enrol_meta_plugin_testcase extends advanced_testcase {
|
||||
$this->assertArrayHasKey('customint2', $errors);
|
||||
|
||||
// Test with valid data.
|
||||
$validgroup = reset($groups);
|
||||
reset($groups);
|
||||
$validgroup = key($groups);
|
||||
$data = [
|
||||
'customint1' => [$metacourse2->id],
|
||||
'customint2' => $validgroup
|
||||
|
||||
@@ -98,6 +98,7 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I press "Enrol me"
|
||||
And I should see "You are enrolled in the course"
|
||||
And I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
@@ -118,6 +119,7 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I press "Enrol me"
|
||||
And I should see "You are enrolled in the course"
|
||||
And I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
@@ -1036,6 +1036,59 @@ class core_enrollib_testcase extends advanced_testcase {
|
||||
$this->assertEquals([$course1->id, $course2->id, $course3->id, $course4->id], array_keys($courses));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for {@see test_enrol_get_my_courses_by_time}
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function enrol_get_my_courses_by_time_provider(): array {
|
||||
return [
|
||||
'No start or end time' =>
|
||||
[null, null, true],
|
||||
'Start time now, no end time' =>
|
||||
[0, null, true],
|
||||
'Start time now, end time in the future' =>
|
||||
[0, MINSECS, true],
|
||||
'Start time in the past, no end time' =>
|
||||
[-MINSECS, null, true],
|
||||
'Start time in the past, end time in the future' =>
|
||||
[-MINSECS, MINSECS, true],
|
||||
'Start time in the past, end time in the past' =>
|
||||
[-DAYSECS, -HOURSECS, false],
|
||||
'Start time in the future' =>
|
||||
[MINSECS, null, false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that expected course enrolments are returned when they have timestart / timeend specified
|
||||
*
|
||||
* @param int|null $timestartoffset Null for 0, otherwise offset from current time
|
||||
* @param int|null $timeendoffset Null for 0, otherwise offset from current time
|
||||
* @param bool $expectreturn
|
||||
*
|
||||
* @dataProvider enrol_get_my_courses_by_time_provider
|
||||
*/
|
||||
public function test_enrol_get_my_courses_by_time(?int $timestartoffset, ?int $timeendoffset, bool $expectreturn): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$time = time();
|
||||
$timestart = $timestartoffset === null ? 0 : $time + $timestartoffset;
|
||||
$timeend = $timeendoffset === null ? 0 : $time + $timeendoffset;
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$user = $this->getDataGenerator()->create_and_enrol($course, 'student', null, 'manual', $timestart, $timeend);
|
||||
$this->setUser($user);
|
||||
|
||||
$courses = enrol_get_my_courses();
|
||||
if ($expectreturn) {
|
||||
$this->assertCount(1, $courses);
|
||||
$this->assertEquals($course->id, reset($courses)->id);
|
||||
} else {
|
||||
$this->assertEmpty($courses);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* test_course_users
|
||||
*
|
||||
@@ -1362,4 +1415,77 @@ class core_enrollib_testcase extends advanced_testcase {
|
||||
$durationinday = $duration / DAYSECS;
|
||||
$this->assertEquals(9, $durationinday);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_enrolled_with_capabilities_join cannotmatchanyrows attribute.
|
||||
*
|
||||
* @dataProvider get_enrolled_with_capabilities_join_cannotmatchanyrows_data()
|
||||
* @param string $capability the tested capability
|
||||
* @param bool $useprohibit if the capability must be assigned to prohibit
|
||||
* @param int $expectedmatch expected cannotmatchanyrows value
|
||||
* @param int $expectedcount expceted count value
|
||||
*/
|
||||
public function test_get_enrolled_with_capabilities_join_cannotmatchanyrows(
|
||||
string $capability,
|
||||
bool $useprohibit,
|
||||
int $expectedmatch,
|
||||
int $expectedcount
|
||||
) {
|
||||
global $DB, $CFG;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$context = context_course::instance($course->id);
|
||||
|
||||
$roleid = $CFG->defaultuserroleid;
|
||||
|
||||
// Override capability if necessary.
|
||||
if ($useprohibit && $capability) {
|
||||
assign_capability($capability, CAP_PROHIBIT, $roleid, $context);
|
||||
}
|
||||
|
||||
// Check if we must enrol or not.
|
||||
$this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
|
||||
|
||||
$join = get_enrolled_with_capabilities_join($context, '', $capability);
|
||||
|
||||
// Execute query.
|
||||
$sql = "SELECT COUNT(DISTINCT u.id)
|
||||
FROM {user} u {$join->joins}
|
||||
WHERE {$join->wheres}";
|
||||
$countrecords = $DB->count_records_sql($sql, $join->params);
|
||||
|
||||
// Validate cannotmatchanyrows.
|
||||
$this->assertEquals($expectedmatch, $join->cannotmatchanyrows);
|
||||
$this->assertEquals($expectedcount, $countrecords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_get_enrolled_with_capabilities_join_cannotmatchanyrows
|
||||
*
|
||||
* @return @array of testing scenarios
|
||||
*/
|
||||
public function get_enrolled_with_capabilities_join_cannotmatchanyrows_data() {
|
||||
return [
|
||||
'no prohibits, no capability' => [
|
||||
'capability' => '',
|
||||
'useprohibit' => false,
|
||||
'expectedmatch' => 0,
|
||||
'expectedcount' => 1,
|
||||
],
|
||||
'no prohibits with capability' => [
|
||||
'capability' => 'moodle/course:manageactivities',
|
||||
'useprohibit' => false,
|
||||
'expectedmatch' => 0,
|
||||
'expectedcount' => 1,
|
||||
],
|
||||
'prohibits with capability' => [
|
||||
'capability' => 'moodle/course:manageactivities',
|
||||
'useprohibit' => true,
|
||||
'expectedmatch' => 1,
|
||||
'expectedcount' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,7 @@ Feature: Render H5P content using filters
|
||||
|
||||
@javascript @external
|
||||
Scenario: Render an external H5P content URL.
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
Given I am on the PageName1 "page activity editing" page logged in as teacher1
|
||||
And I set the field "Page content" to "<div>Go for it</div>https://moodle.h5p.com/content/1290772960722742119/embed"
|
||||
When I click on "Save and display" "button"
|
||||
And I wait until the page is ready
|
||||
@@ -36,10 +33,7 @@ Feature: Render H5P content using filters
|
||||
|
||||
@javascript
|
||||
Scenario: Add an external H5P content URL in a link with the URL. Should be rendered.
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
Given I am on the PageName1 "page activity editing" page logged in as teacher1
|
||||
# This content won't be displayed, so this scenario shouldn't be labeled as external.
|
||||
And I set the field "Page content" to "<a href='https://moodle.h5p.com/content/1290772960722742119/embed'>https://moodle.h5p.com/content/1290772960722742119/embed</a>"
|
||||
When I click on "Save and display" "button"
|
||||
@@ -48,10 +42,7 @@ Feature: Render H5P content using filters
|
||||
Then I should see "Lorum ipsum"
|
||||
|
||||
Scenario: Add an external H5P content URL in a link with text. Shouldn't be rendered.
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
Given I am on the PageName1 "page activity editing" page logged in as teacher1
|
||||
# This content won't be displayed, so this scenario shouldn't be labeled as external.
|
||||
And I set the field "Page content" to "<a href='https://moodle.h5p.com/content/1290772960722742119/embed'>Here you are the content</a>"
|
||||
When I click on "Save and display" "button"
|
||||
@@ -67,8 +58,7 @@ Feature: Render H5P content using filters
|
||||
| Name | ipsumFile |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager
|
||||
And I press "Save and return to course"
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the PageName1 "page activity editing" page
|
||||
And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
|
||||
And I click on "Browse repositories..." "button" in the "Insert H5P" "dialogue"
|
||||
And I click on "Server files" "link" in the ".fp-repo-area" "css_element"
|
||||
@@ -85,9 +75,7 @@ Feature: Render H5P content using filters
|
||||
Then I should see "Lorum ipsum"
|
||||
And I switch to the main frame
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "PageName1"
|
||||
And I am on the PageName1 "page activity" page logged in as student1
|
||||
# Switch to iframe created by filter
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
# Switch to iframe created by embed.php page
|
||||
@@ -104,8 +92,7 @@ Feature: Render H5P content using filters
|
||||
| Name | ipsumFile |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager
|
||||
And I press "Save and return to course"
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the PageName1 "page activity editing" page
|
||||
And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
|
||||
And I click on "Browse repositories..." "button" in the "Insert H5P" "dialogue"
|
||||
And I click on "Server files" "link" in the ".fp-repo-area" "css_element"
|
||||
@@ -129,8 +116,7 @@ Feature: Render H5P content using filters
|
||||
| Name | ipsumFileTeacher |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager
|
||||
And I press "Save and return to course"
|
||||
And I follow "PageName1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the PageName1 "page activity editing" page
|
||||
And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
|
||||
And I click on "Browse repositories..." "button" in the "Insert H5P" "dialogue"
|
||||
And I click on "Server files" "link" in the ".fp-repo-area" "css_element"
|
||||
@@ -153,8 +139,7 @@ Feature: Render H5P content using filters
|
||||
| Name | ipsumFile |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager
|
||||
And I press "Save and return to course"
|
||||
And I follow "PageName2"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the PageName2 "page activity editing" page
|
||||
And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
|
||||
And I click on "Browse repositories..." "button" in the "Insert H5P" "dialogue"
|
||||
And I click on "Server files" "link" in the ".fp-repo-area" "css_element"
|
||||
@@ -172,9 +157,7 @@ Feature: Render H5P content using filters
|
||||
And I should see "Lorum ipsum"
|
||||
And I switch to the main frame
|
||||
And I log out
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "PageName1"
|
||||
And I am on the PageName1 "page activity" page logged in as teacher1
|
||||
# Switch to iframe created by filter
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
# Switch to iframe created by embed.php page
|
||||
|
||||
@@ -23,6 +23,16 @@ class grade_export_xml extends grade_export {
|
||||
public $plugin = 'xml';
|
||||
public $updatedgradesonly = false; // default to export ALL grades
|
||||
|
||||
/**
|
||||
* Ensure we produce correctly formed XML content by encoding idnumbers appropriately
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return string
|
||||
*/
|
||||
private static function xml_export_idnumber(string $idnumber): string {
|
||||
return htmlspecialchars($idnumber, ENT_QUOTES | ENT_XML1);
|
||||
}
|
||||
|
||||
/**
|
||||
* To be implemented by child classes
|
||||
* @param boolean $feedback
|
||||
@@ -84,9 +94,11 @@ class grade_export_xml extends grade_export {
|
||||
}
|
||||
|
||||
// only need id number
|
||||
fwrite($handle, "\t\t<assignment>{$grade_item->idnumber}</assignment>\n");
|
||||
$gradeitemidnumber = self::xml_export_idnumber($grade_item->idnumber);
|
||||
fwrite($handle, "\t\t<assignment>{$gradeitemidnumber}</assignment>\n");
|
||||
// this column should be customizable to use either student id, idnumber, uesrname or email.
|
||||
fwrite($handle, "\t\t<student>{$user->idnumber}</student>\n");
|
||||
$useridnumber = self::xml_export_idnumber($user->idnumber);
|
||||
fwrite($handle, "\t\t<student>{$useridnumber}</student>\n");
|
||||
// Format and display the grade in the selected display type (real, letter, percentage).
|
||||
if (is_array($this->displaytype)) {
|
||||
// Grades display type came from the return of export_bulk_export_data() on grade publishing.
|
||||
|
||||
@@ -12,10 +12,12 @@ Feature: I need to export grades as xml
|
||||
| username | firstname | lastname | email | idnumber |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com | t1 |
|
||||
| student1 | Student | 1 | student1@example.com | s1 |
|
||||
| student2 | Student | 2 | student2@example.com | 'Bill'&"Ben"<tag>Hello</tag> |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
And the following "activities" exist:
|
||||
| activity | course | idnumber | name | intro |
|
||||
| assign | C1 | a1 | Test assignment name | Submit something! |
|
||||
@@ -24,15 +26,21 @@ Feature: I need to export grades as xml
|
||||
And I navigate to "View > Grader report" in the course gradebook
|
||||
And I turn editing mode on
|
||||
And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment name"
|
||||
And I give the grade "42.00" to the user "Student 2" for the grade item "Test assignment name"
|
||||
And I press "Save changes"
|
||||
|
||||
@javascript
|
||||
Scenario: Export grades as text
|
||||
Scenario: Export grades as XML
|
||||
When I navigate to "Export > XML file" in the course gradebook
|
||||
And I expand all fieldsets
|
||||
And I set the field "Grade export decimal places" to "1"
|
||||
And I press "Download"
|
||||
Then I should see "s1"
|
||||
And I should see "a1"
|
||||
And I should see "80.0"
|
||||
And I should not see "80.00"
|
||||
Then I should see "s1" in the "//results//result[1]//student" "xpath_element"
|
||||
And I should see "a1" in the "//results//result[1]//assignment" "xpath_element"
|
||||
And I should see "80.0" in the "//results//result[1]//score" "xpath_element"
|
||||
And I should not see "80.00" in the "//results//result[1]//score" "xpath_element"
|
||||
# Ensure we have the encoded ID number of student 2.
|
||||
And I should see "'Bill'&\"Ben\"<tag>Hello</tag>" in the "//results//result[2]//student" "xpath_element"
|
||||
And I should see "a1" in the "//results//result[2]//assignment" "xpath_element"
|
||||
And I should see "42.0" in the "//results//result[2]//score" "xpath_element"
|
||||
And I should not see "42.00" in the "//results//result[2]//score" "xpath_element"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user