Compare commits
586 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f497f8138 | |||
| 9e034f8312 | |||
| e6d4476bed | |||
| 7bfe4e5875 | |||
| e3e4d1dddf | |||
| 9e476db4d1 | |||
| 0b0e171563 | |||
| 63aa2d72af | |||
| 5269c824b3 | |||
| dca4195a7c | |||
| 2447afb150 | |||
| 672760c211 | |||
| 443dc0e29c | |||
| 209c8f1c0e | |||
| 0305c55494 | |||
| e1a5ffb198 | |||
| 07f8fb90ba | |||
| bb79d059ae | |||
| a2f060e24b | |||
| 2b5e84f107 | |||
| 9145e64e24 | |||
| 31db1386ca | |||
| 688e04192d | |||
| 3237df4e25 | |||
| c232b64d9b | |||
| dd99f2eb86 | |||
| 492da54f6c | |||
| e7f245f9a5 | |||
| 834a3727b4 | |||
| 217bdb4c6f | |||
| 833600b648 | |||
| b2ca897a48 | |||
| f18b6e98a9 | |||
| 194574e656 | |||
| 725360af8a | |||
| 63531cebe9 | |||
| 1d134892c8 | |||
| fd39d575c5 | |||
| e1bd6dc81d | |||
| bca2a8ad97 | |||
| 7784e41de2 | |||
| 4f77d7808e | |||
| db97df13b6 | |||
| f743ec6fd8 | |||
| 277d3168b8 | |||
| f2f8137964 | |||
| a19da6770a | |||
| 518259180f | |||
| 201111ffd7 | |||
| c96c7e461f | |||
| b3f12c9761 | |||
| 5f8baebeed | |||
| 4c2b7dc647 | |||
| a9ce091f0c | |||
| ec0b0aa279 | |||
| a52c55e79d | |||
| 1f9050a2e6 | |||
| 83c98546b1 | |||
| 3ec133e062 | |||
| 502d0cae12 | |||
| 3300ba4676 | |||
| 0c4f57af6e | |||
| f2fb5fe428 | |||
| ace24d32cd | |||
| 634892618d | |||
| 59d23170cb | |||
| 7d66057a0b | |||
| 0859682802 | |||
| 24a56ecc51 | |||
| 850eeefc28 | |||
| d6d77abe39 | |||
| 0f6459b758 | |||
| 0960437fb4 | |||
| cff57e5846 | |||
| 667dcf4ce2 | |||
| 02e244d1a9 | |||
| f8c9c0a475 | |||
| 056a95d77d | |||
| 46d1003ea3 | |||
| aecebb12df | |||
| 01ed3b0410 | |||
| 3392f0a438 | |||
| ff013f4395 | |||
| f2ec040259 | |||
| 464b8b4670 | |||
| e376beef97 | |||
| 9bd72fd9d5 | |||
| 63568873cc | |||
| f3be834fa7 | |||
| bcab539678 | |||
| fec7949f37 | |||
| dac2009c4b | |||
| 74d9a08142 | |||
| b726820c4f | |||
| aebe923947 | |||
| 2c2ea4565c | |||
| 366d1ee79b | |||
| 441130b7d6 | |||
| f06752f9c5 | |||
| 468a24606d | |||
| cbd7b7da6d | |||
| 94a88b461a | |||
| 746f530b34 | |||
| b145ff806d | |||
| 9785d83f50 | |||
| 052044b828 | |||
| 48ec158921 | |||
| 6594769211 | |||
| cffe464dd5 | |||
| a06a065c52 | |||
| 4ffe3d32f0 | |||
| 1348e4ec67 | |||
| 5c620aa060 | |||
| 65b51ef436 | |||
| 6b95c7c714 | |||
| cf3eb729cb | |||
| 1d468570c2 | |||
| d64fa92eee | |||
| 77f7c83df6 | |||
| d75f86fe17 | |||
| 7fd20ffc27 | |||
| c1ab96af59 | |||
| dd682468f1 | |||
| 7fedd96eda | |||
| dade115522 | |||
| ef3c60e021 | |||
| ae7baf3c9f | |||
| 8baa9039ab | |||
| 9c21b3ba17 | |||
| a345c57b18 | |||
| df9a67b8a5 | |||
| d4412078e2 | |||
| 8923d6f2ef | |||
| 0f28bf440a | |||
| 0e66dce507 | |||
| 1beba61e80 | |||
| c1de9b3835 | |||
| 2a5c67dca4 | |||
| be95a3b985 | |||
| f49d624992 | |||
| 329725c21e | |||
| 9547082cdd | |||
| fd40e07271 | |||
| a722fe5493 | |||
| d949fd6801 | |||
| 0bc9ae17a2 | |||
| e5429f2831 | |||
| c688be33a3 | |||
| 82296e9420 | |||
| 1783fa7882 | |||
| ae002508f5 | |||
| 2329be1d56 | |||
| 6b8eb56fd8 | |||
| f82060b773 | |||
| 05441e21c7 | |||
| 1c8e047af5 | |||
| 453f78c48a | |||
| a2b0c71769 | |||
| 0302dd16a2 | |||
| dd643df554 | |||
| f533f30d3b | |||
| df10e4006a | |||
| 32d6f97428 | |||
| c7c1280a74 | |||
| ed8e9db1d3 | |||
| 6123fa55e2 | |||
| 5f5f44f2ca | |||
| 87be7803f8 | |||
| cc5c32ba1b | |||
| 7bfee7ef70 | |||
| f4da6d1a68 | |||
| 53acc89d7a | |||
| 0e3165f0b3 | |||
| 5adb6a957e | |||
| 60658f3b4a | |||
| 9acf96dcab | |||
| 3ae6a15959 | |||
| 7e9b1fe543 | |||
| d3675d3e88 | |||
| 73b0dc1f92 | |||
| 83230435e7 | |||
| e5c4305bdb | |||
| a8b365a33b | |||
| 4c2efc6857 | |||
| 305348b934 | |||
| 0bbd13319b | |||
| b1a42309af | |||
| 6bfb789b98 | |||
| 5f135b2df3 | |||
| c0a02a2467 | |||
| 5136621016 | |||
| 6148cd38c0 | |||
| 1378548e0a | |||
| 5b88e5ab72 | |||
| 1a2afd7fe4 | |||
| fc59f17616 | |||
| 330d80faa7 | |||
| 09b2103fde | |||
| da1db5d6d1 | |||
| eff0a78549 | |||
| 4b2323803a | |||
| 2890b9ee94 | |||
| e13983d257 | |||
| 142e29146e | |||
| b5bc0e315f | |||
| 3f16028620 | |||
| f5e04493c3 | |||
| e27ed6dc7a | |||
| 6e53ba123c | |||
| 2acc3bf43c | |||
| fa08851ccb | |||
| 5a282f931f | |||
| f656a642a2 | |||
| b4e8f59497 | |||
| 17ddfe2ecb | |||
| 32a5269449 | |||
| 67affb8216 | |||
| f978082766 | |||
| 011bffc2f5 | |||
| da5db34c27 | |||
| 6f56529070 | |||
| 95e211146a | |||
| 1f60fd4673 | |||
| b650870377 | |||
| 34e1fc9dc6 | |||
| a167f6dbca | |||
| b299f81736 | |||
| 0bdb88ae9e | |||
| 8c0aa1afc9 | |||
| 713a9da8b0 | |||
| dd83c1c09a | |||
| f7e8903a7f | |||
| 0728bb6609 | |||
| 872fc36cc4 | |||
| fe1f5425af | |||
| 67d24e7032 | |||
| caae1d5720 | |||
| 36493b1ac1 | |||
| c32438bc79 | |||
| f1e95a5749 | |||
| 99a219de52 | |||
| 3a713edefe | |||
| f0fde99c46 | |||
| 06df92cfa2 | |||
| 7808e616a5 | |||
| e89fb4e371 | |||
| a58f0efab6 | |||
| e1c848c189 | |||
| 2ea2956e8f | |||
| 437d6a0a02 | |||
| 26c7e57040 | |||
| 8b1c4127e3 | |||
| 69fce2b7e4 | |||
| 89eea57353 | |||
| d0a69c7642 | |||
| e49fc4d750 | |||
| 7830b1787a | |||
| 235bdcab3b | |||
| 8629444b93 | |||
| a7deebb231 | |||
| 0cfcfff4b8 | |||
| 29e2d74c96 | |||
| 247b2d95b5 | |||
| 0d25477253 | |||
| 1e0c48661b | |||
| 4850fd3039 | |||
| bfe4f1f987 | |||
| e409f30dcb | |||
| 1f2f2e7f71 | |||
| ed3851ff6b | |||
| b1bff07931 | |||
| a8f3b08f67 | |||
| 44e0e1a780 | |||
| c2d762866b | |||
| 25cac9b43b | |||
| 0686806ef3 | |||
| cadb42dc64 | |||
| 34e60be48e | |||
| 4e03ffe31d | |||
| 22287e4b1a | |||
| 2d9bddeebf | |||
| ac802ad804 | |||
| a379081818 | |||
| 550afb2a72 | |||
| 13ee4216e3 | |||
| 5f42fc85eb | |||
| aa9093c3ad | |||
| 3988c4c620 | |||
| e99b368c60 | |||
| 70d026f0f7 | |||
| 833f55cf11 | |||
| f36fd442e8 | |||
| 60564c02ab | |||
| b39f3bb7f9 | |||
| 31a3e0eaa9 | |||
| aac49cd591 | |||
| 557432b764 | |||
| 8683b4a049 | |||
| 49b29449af | |||
| a80b692453 | |||
| 5100441bba | |||
| ff5c46a912 | |||
| 860feade62 | |||
| e193924db6 | |||
| 3702195fb2 | |||
| 364a762fa7 | |||
| fa810327f7 | |||
| b25ed2333f | |||
| 6803fd0988 | |||
| 373ca27d34 | |||
| 03341aea9f | |||
| a7c5fad12d | |||
| 3132cc9d4f | |||
| 2eb32285d7 | |||
| 274d5ccc62 | |||
| a105edc53f | |||
| 582f2e673a | |||
| e9e6e0921d | |||
| 0a580ed98d | |||
| bb13942ace | |||
| 3c681534da | |||
| 036fc9ef9a | |||
| 03a901b1fb | |||
| 6390af2f48 | |||
| 078308b04a | |||
| c708bb044e | |||
| e001ebc339 | |||
| f9a69ed13c | |||
| f1f1738ca8 | |||
| 8bdec47d8e | |||
| dc662df705 | |||
| d5f21323ce | |||
| 608266dace | |||
| f8349fb154 | |||
| 3408c2366d | |||
| 320eee740a | |||
| ae877e4633 | |||
| 5881cd48de | |||
| 6d9979bd1b | |||
| 9360d678e5 | |||
| 5182889f53 | |||
| 918cc4f6c4 | |||
| 6862165efa | |||
| 052cf0a509 | |||
| 9556da6637 | |||
| 5af16f7c62 | |||
| 223e070e9c | |||
| 3e49f34657 | |||
| a73abacee5 | |||
| 604a1e0cdd | |||
| d2eb4436ab | |||
| 7632786af9 | |||
| 50e44173c8 | |||
| 8084242767 | |||
| e05e7093c9 | |||
| ebf862e0d3 | |||
| c94a1593e3 | |||
| d5cad3ef39 | |||
| 65da222994 | |||
| 3382df5406 | |||
| f9280e786b | |||
| 8d4df26b01 | |||
| 884a9b436c | |||
| bada40d4a4 | |||
| b0130619d6 | |||
| 207f222e80 | |||
| 8e959ef40d | |||
| 846ddd3979 | |||
| 5370fa4d1d | |||
| b23182d82d | |||
| 2048b6330c | |||
| 8c49fdeb1d | |||
| 37ec33f56c | |||
| e4c3310295 | |||
| 915c263266 | |||
| 0f332fb0ff | |||
| dd8a1647c3 | |||
| b56a9953cf | |||
| 5cb2949d50 | |||
| 75ad63dd97 | |||
| 2448dbd55f | |||
| 0222576f3f | |||
| 002e281598 | |||
| 17f2636bc2 | |||
| cff7c05eb8 | |||
| 3f9e294f21 | |||
| 687d6670b3 | |||
| 0690e7b230 | |||
| e80b3cf70a | |||
| 86de0a0c8d | |||
| 50a0403d37 | |||
| 59d0bb149d | |||
| 7b93ccde37 | |||
| 96dd901bea | |||
| 316507507b | |||
| e11305428c | |||
| 489eb64b4a | |||
| 555eef58af | |||
| 2e011c622f | |||
| a686188165 | |||
| 880314b565 | |||
| 638de36275 | |||
| bc72e1c107 | |||
| 23a8fdc812 | |||
| 6511e183f5 | |||
| 775858e46a | |||
| ff9fb97d72 | |||
| c00b4eda45 | |||
| ca12b96b77 | |||
| 2bc458c4f9 | |||
| 6945ab9db5 | |||
| 3768fa2928 | |||
| ad9a1630a1 | |||
| 3252c42592 | |||
| f41ee92e8e | |||
| 77937f2359 | |||
| e3ccaace4a | |||
| b612105230 | |||
| 157a8eb014 | |||
| 7d620f8fdc | |||
| ba75b60c85 | |||
| d70681c2f5 | |||
| 1bc6eba524 | |||
| 6200a07627 | |||
| ccdacb8443 | |||
| a84be34f83 | |||
| 5ec89b95ec | |||
| 189be87f2e | |||
| 61868b9510 | |||
| 13317f2598 | |||
| 54dfeb3d11 | |||
| 908784bca9 | |||
| 28c9a52455 | |||
| 86ee7eef07 | |||
| b4ed92a701 | |||
| 2ae416b6f5 | |||
| c5faeaee4c | |||
| d3c9ccba3c | |||
| 72c6f8882c | |||
| ec07626137 | |||
| 28f156895a | |||
| 7adddedf4f | |||
| 6ba03f474a | |||
| 44fbef243d | |||
| 7c19447345 | |||
| 5e946c1f4f | |||
| f793334a44 | |||
| 4c47af3dee | |||
| 3ab3c72aa7 | |||
| efee80655a | |||
| a4da1b6b4e | |||
| 36186e9af7 | |||
| 37c54215d1 | |||
| 5f99938670 | |||
| 92788c9c31 | |||
| 42cb48db5c | |||
| 3bb83daec9 | |||
| 34db25536e | |||
| ab57c200c5 | |||
| 3b72da69af | |||
| 358dd1dc29 | |||
| 3f436bb599 | |||
| d7431b36a2 | |||
| 159a155062 | |||
| 0c0b4b69c9 | |||
| a4b47235d6 | |||
| 90e25dfc45 | |||
| 3c7933178d | |||
| 738fb91dec | |||
| badca8a618 | |||
| 0fa3a9d6aa | |||
| 8b0edbf074 | |||
| 27db562110 | |||
| de4bcb1b4c | |||
| 47ce3408a1 | |||
| 1e2abd1466 | |||
| 1e5e225a86 | |||
| 774796cf77 | |||
| 9e72cc3998 | |||
| 928b6719dc | |||
| 878ef73e8c | |||
| 106c11ddde | |||
| 1c4022b77d | |||
| 158bff3dcb | |||
| 8642510801 | |||
| 36a0b0572f | |||
| 0fd0462bc6 | |||
| 362c9de6ad | |||
| 8d8941166c | |||
| 0d16e46646 | |||
| 4d04135186 | |||
| cb4e4c8d45 | |||
| bf58e69516 | |||
| cf51659f5a | |||
| 4a7f57ee8e | |||
| 1cc0453ccd | |||
| 9f589703a6 | |||
| ed4f0da1c9 | |||
| 923eb86a8c | |||
| 5886683a33 | |||
| 5ef0a36d3d | |||
| 07e4f55804 | |||
| dda4552e79 | |||
| c04a71b431 | |||
| 120beb8aaf | |||
| d08fd3c2af | |||
| 2b94d55524 | |||
| 057c314edc | |||
| d4690ea232 | |||
| 88763ab043 | |||
| 547ea9cfbf | |||
| cfaccbf953 | |||
| 11d0802ca6 | |||
| 1cd17816c5 | |||
| 8bcf6f016d | |||
| bd39c5df46 | |||
| 85f08eccd7 | |||
| e35c3f18e5 | |||
| 700ffe79a3 | |||
| 0bfc7f5abd | |||
| 39a16ed55a | |||
| 7414e469b8 | |||
| a55b9cfc70 | |||
| f4187d49da | |||
| cacdd7e63a | |||
| 08d6834166 | |||
| 740c3dafcc | |||
| b50f58ec0d | |||
| 3fd9808fc6 | |||
| 9155d32f36 | |||
| e6d7fcb2e0 | |||
| c5b69126d7 | |||
| 0f0904243d | |||
| 95d96c926b | |||
| e39521934c | |||
| c629f23da8 | |||
| 3f5b13cd49 | |||
| 019e03af94 | |||
| 8e477788f8 | |||
| b32b09d584 | |||
| 1a1033c601 | |||
| fcde530bc4 | |||
| 8117d73b98 | |||
| 6ca58162c6 | |||
| 7b348fa333 | |||
| a6587c7986 | |||
| 19b193784b | |||
| 6f44b00e42 | |||
| 90eb487eec | |||
| 2d111226fe | |||
| 832acc2639 | |||
| 7eb5190921 | |||
| 2e1e274ed4 | |||
| a217af7ae5 | |||
| 1e893d4b21 | |||
| de2ab7ccb7 | |||
| f6a31e8fdc | |||
| 7cf1291cc2 | |||
| f380071cb5 | |||
| 3055d89c46 | |||
| 4ef04a9d94 | |||
| a633683bb3 | |||
| 0c4b848a48 | |||
| 010daf38da | |||
| 038963316f | |||
| 94a34a82f7 | |||
| d8f2f5e6a0 | |||
| bdcc11503d | |||
| 35ca47e57a | |||
| 840c045bc1 | |||
| 7ca1574c28 | |||
| bae9d77604 | |||
| 91bdc56a5d | |||
| 00cd643c33 | |||
| 73118e777f | |||
| eb50526c35 | |||
| 8e952904e8 | |||
| 0df4a0d4ce | |||
| 56e76851cc | |||
| 6fd62e9bfc | |||
| 8d07409ff8 | |||
| 7c6aaf9d4b | |||
| e06a6ee9c8 | |||
| 92a8d0e9c0 | |||
| 26a16e4e27 | |||
| 824fe6ec83 |
@@ -51,7 +51,7 @@ jobs:
|
||||
chunk=$(((($count % $chunks)) + 1))
|
||||
echo "$testname $testfile" >> ./chunk_$chunk.txt
|
||||
done < <(grep "function test_" "${testfile}" | sed -r "s/^.*function (test_[a-zA-Z0-9_]+).*/\1/")
|
||||
done < <(find . -name "*_test.php")
|
||||
done < <(find . -name "*_test.php" -not -path "*/fixtures/*")
|
||||
# Generate the matrix to run tests.
|
||||
echo "matrix=$(ls -1 chunk_*.txt | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
|
||||
echo "$count individual tests collected in $chunks files"
|
||||
|
||||
+21
-20
@@ -56,6 +56,14 @@ jobs:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
postgres:
|
||||
image: ${{ matrix.db == 'pgsql' && 'postgres:15' || '' }}
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_PASSWORD: test
|
||||
ports:
|
||||
- 5432:5432
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -71,27 +79,20 @@ jobs:
|
||||
db: pgsql
|
||||
|
||||
steps:
|
||||
- name: Setting up DB mysql
|
||||
- name: Run MySQL Server
|
||||
if: ${{ matrix.db == 'mysqli' }}
|
||||
uses: moodlehq/mysql-action@v1
|
||||
with:
|
||||
collation server: utf8mb4_bin
|
||||
mysql version: 8.4
|
||||
mysql database: test
|
||||
mysql user: test
|
||||
mysql password: test
|
||||
use tmpfs: true
|
||||
tmpfs size: '1024M'
|
||||
extra conf: --skip-log-bin
|
||||
|
||||
- name: Setting up DB pgsql
|
||||
if: ${{ matrix.db == 'pgsql' }}
|
||||
uses: m4nu56/postgresql-action@v1
|
||||
with:
|
||||
postgresql version: 15
|
||||
postgresql db: test
|
||||
postgresql user: test
|
||||
postgresql password: test
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e MYSQL_DATABASE=test \
|
||||
-e MYSQL_USER=test \
|
||||
-e MYSQL_PASSWORD=test \
|
||||
-e MYSQL_ROOT_PASSWORD=test \
|
||||
-p 3306:3306 \
|
||||
-d \
|
||||
--tmpfs /var/lib/mysql:rw,noexec,nosuid,size=1024M \
|
||||
mysql:8.4 \
|
||||
--skip-log-bin \
|
||||
--collation-server=utf8mb4_bin
|
||||
|
||||
- name: Configuring git vars
|
||||
uses: rlespinasse/github-slug-action@v4
|
||||
|
||||
@@ -89,7 +89,7 @@ const templates = {
|
||||
|
||||
module.exports = {
|
||||
opts: {
|
||||
destination: "./jsdoc/",
|
||||
destination: "./public/jsdoc/",
|
||||
template: "node_modules/docdash",
|
||||
},
|
||||
plugins,
|
||||
|
||||
@@ -18,7 +18,7 @@ import inquirer from 'inquirer';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { createNote } from './note.mjs';
|
||||
import { getInitialValues } from './helpers.mjs';
|
||||
import { getInitialValues, getIssueNumberFromBranchName } from './helpers.mjs';
|
||||
import * as Prompts from './prompts.mjs';
|
||||
import logger from './logger.mjs';
|
||||
|
||||
@@ -29,7 +29,7 @@ export default async (options) => {
|
||||
// Fetch information.
|
||||
const messages = [];
|
||||
const { issueNumber } = await inquirer.prompt([
|
||||
Prompts.getIssuePrompt(),
|
||||
Prompts.getIssuePrompt(getIssueNumberFromBranchName()),
|
||||
], initialValues);
|
||||
|
||||
let selection = {};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import chalk from 'chalk';
|
||||
import branchName from 'current-git-branch';
|
||||
import { isStandardComponent, isCommunityComponent, rewritePlugintypeAsSubsystem } from './components.mjs';
|
||||
import { isValidNoteName } from './noteTypes.mjs';
|
||||
import logger from './logger.mjs';
|
||||
@@ -59,6 +60,19 @@ export const formatIssueNumber = (input) => {
|
||||
return input;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the issue number, based off current git branch name
|
||||
*
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export const getIssueNumberFromBranchName = () => {
|
||||
const issueNumber = String(branchName()).match(/^[a-zA-Z]*-\d+/);
|
||||
if (issueNumber) {
|
||||
return formatIssueNumber(issueNumber[0]);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate a component.
|
||||
*
|
||||
|
||||
@@ -43,8 +43,8 @@ module.exports = (grunt) => {
|
||||
|
||||
const componentData = fetchComponentData().components;
|
||||
for (const [thisPath, component] of Object.entries(componentData)) {
|
||||
jsconfigData.compilerOptions.paths[`${component}/*`] = [`public/${thisPath}/amd/src/*`];
|
||||
jsconfigData.include.push(`public/${thisPath}/amd/src/**/*`);
|
||||
jsconfigData.compilerOptions.paths[`${component}/*`] = [`${thisPath}/amd/src/*`];
|
||||
jsconfigData.include.push(`${thisPath}/amd/src/**/*`);
|
||||
}
|
||||
|
||||
grunt.file.write('jsconfig.json', JSON.stringify(jsconfigData, null, " ") + "\n");
|
||||
|
||||
@@ -49,6 +49,10 @@ module.exports = grunt => {
|
||||
|
||||
// Spawn the task in a child process so that it doesn't kill this one
|
||||
// if it failed.
|
||||
var args = [task, filesOption];
|
||||
if (grunt.option('force')) {
|
||||
args.push('--force');
|
||||
}
|
||||
grunt.util.spawn(
|
||||
{
|
||||
// Spawn with the grunt bin.
|
||||
@@ -58,7 +62,7 @@ module.exports = grunt => {
|
||||
cwd: grunt.moodleEnv.fullRunDir,
|
||||
stdio: 'inherit'
|
||||
},
|
||||
args: [task, filesOption]
|
||||
args: args
|
||||
},
|
||||
function(err, res, code) {
|
||||
if (code !== 0) {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
issueNumber: MDL-51152
|
||||
notes:
|
||||
enrol_manual:
|
||||
- message: >-
|
||||
The unused parameter 'roleid' has been removed from the external
|
||||
function `unenrol_users()`
|
||||
type: removed
|
||||
@@ -0,0 +1,8 @@
|
||||
issueNumber: MDL-75067
|
||||
notes:
|
||||
tool_behat:
|
||||
- message: >-
|
||||
The `behat_session_trait::ensure_element_[does_not_]exists(...)` methods
|
||||
now accept optional `$container` parameter to define the parent node to
|
||||
look within
|
||||
type: improved
|
||||
@@ -0,0 +1,8 @@
|
||||
issueNumber: MDL-85322
|
||||
notes:
|
||||
block_html:
|
||||
- message: >-
|
||||
Treat Dashboard (pagetype 'my-index') as trusted in web services so
|
||||
get_content_for_external preserves embedded HTML (e.g. iframes) on user
|
||||
Dashboard.
|
||||
type: changed
|
||||
@@ -0,0 +1,12 @@
|
||||
issueNumber: MDL-86524
|
||||
notes:
|
||||
core_question:
|
||||
- message: >-
|
||||
During restore of a question_set_reference, mapping of IDs in the
|
||||
filtercondition is now delegated to qbank plugins.
|
||||
If your qbank plugin defines a filter condition that uses database
|
||||
IDs, add an override of `restore_filtercondition()` to the `condition`
|
||||
class, which checks the condition's data and replaces the IDs with
|
||||
mapped values if required. See
|
||||
`qbank_managecategories\category_condition` for an example.
|
||||
type: improved
|
||||
@@ -0,0 +1,10 @@
|
||||
issueNumber: MDL-86839
|
||||
notes:
|
||||
core:
|
||||
- message: >
|
||||
"grunt watch" now accepts a force flag. Run "grunt watch -f" or "grunt
|
||||
watch --force" to prevent grunt from cancelling builds when errors
|
||||
occur. This is especially useful during development, because js build
|
||||
files will be built even if, for example, jslint errors are still
|
||||
present in the files.
|
||||
type: improved
|
||||
@@ -0,0 +1,7 @@
|
||||
issueNumber: MDL-87100
|
||||
notes:
|
||||
core:
|
||||
- message: >-
|
||||
When creating upgrade notes, the issue number will be inferred from the
|
||||
current Git branch name by default
|
||||
type: improved
|
||||
@@ -0,0 +1,7 @@
|
||||
issueNumber: MDL-87795
|
||||
notes:
|
||||
core_user:
|
||||
- message: >-
|
||||
Added new optional parameter `userid` to the `user_remove_user_device`
|
||||
function.
|
||||
type: improved
|
||||
@@ -0,0 +1,8 @@
|
||||
issueNumber: MDL-87993
|
||||
notes:
|
||||
core:
|
||||
- message: >-
|
||||
The `core/toast` JS module now accepts a `visuallyHidden` configuration
|
||||
parameter to render visually hidden toast messages for screen reader
|
||||
users.
|
||||
type: improved
|
||||
@@ -32,7 +32,7 @@ Moodle also offers hosting through both [MoodleCloud][11], and our [partner netw
|
||||
|
||||
## License
|
||||
|
||||
Moodle is provided freely as open source software, under version 3 of the GNU General Public License. For more information on our license see
|
||||
Moodle is provided freely as open source software, under version 3 of the GNU General Public License. See our [license page][12] for more information.
|
||||
|
||||
[1]: https://moodle.org
|
||||
[2]: https://moodle.com
|
||||
|
||||
+101
@@ -6,6 +6,107 @@ More detailed information on key changes can be found in the [Developer update n
|
||||
|
||||
The format of this change log follows the advice given at [Keep a CHANGELOG](https://keepachangelog.com).
|
||||
|
||||
## 5.1.3+
|
||||
|
||||
### core
|
||||
|
||||
#### Added
|
||||
|
||||
- "grunt watch" now accepts a force flag. Run "grunt watch -f" or "grunt watch --force" to prevent grunt from cancelling builds when errors occur. This is especially useful during development, because js build files will be built even if, for example, jslint errors are still present in the files.
|
||||
|
||||
For more information see [MDL-86839](https://tracker.moodle.org/browse/MDL-86839)
|
||||
- When creating upgrade notes, the issue number will be inferred from the current Git branch name by default
|
||||
|
||||
For more information see [MDL-87100](https://tracker.moodle.org/browse/MDL-87100)
|
||||
- The `core/toast` JS module now accepts a `visuallyHidden` configuration parameter to render visually hidden toast messages for screen reader users.
|
||||
|
||||
For more information see [MDL-87993](https://tracker.moodle.org/browse/MDL-87993)
|
||||
|
||||
### core_question
|
||||
|
||||
#### Added
|
||||
|
||||
- During restore of a question_set_reference, mapping of IDs in the filtercondition is now delegated to qbank plugins. If your qbank plugin defines a filter condition that uses database IDs, add an override of `restore_filtercondition()` to the `condition` class, which checks the condition's data and replaces the IDs with mapped values if required. See `qbank_managecategories\category_condition` for an example.
|
||||
|
||||
For more information see [MDL-86524](https://tracker.moodle.org/browse/MDL-86524)
|
||||
|
||||
### core_user
|
||||
|
||||
#### Added
|
||||
|
||||
- Added new optional parameter `userid` to the `user_remove_user_device` function.
|
||||
|
||||
For more information see [MDL-87795](https://tracker.moodle.org/browse/MDL-87795)
|
||||
|
||||
### block_html
|
||||
|
||||
#### Changed
|
||||
|
||||
- Treat Dashboard (pagetype 'my-index') as trusted in web services so get_content_for_external preserves embedded HTML (e.g. iframes) on user Dashboard.
|
||||
|
||||
For more information see [MDL-85322](https://tracker.moodle.org/browse/MDL-85322)
|
||||
|
||||
### enrol_manual
|
||||
|
||||
#### Removed
|
||||
|
||||
- The unused parameter 'roleid' has been removed from the external function `unenrol_users()`
|
||||
|
||||
For more information see [MDL-51152](https://tracker.moodle.org/browse/MDL-51152)
|
||||
|
||||
### tool_behat
|
||||
|
||||
#### Added
|
||||
|
||||
- The `behat_session_trait::ensure_element_[does_not_]exists(...)` methods now accept optional `$container` parameter to define the parent node to look within
|
||||
|
||||
For more information see [MDL-75067](https://tracker.moodle.org/browse/MDL-75067)
|
||||
|
||||
## 5.1.2
|
||||
|
||||
### core
|
||||
|
||||
#### Added
|
||||
|
||||
- There is a new Behat `toast_message` named selector to more easily assert the presence of Toast messages on the page
|
||||
|
||||
For more information see [MDL-87443](https://tracker.moodle.org/browse/MDL-87443)
|
||||
|
||||
#### Changed
|
||||
|
||||
- `\core\output\core_renderer::confirm()`'s `$displayoptions` parameter now also accepts a `headinglevel` option that developers can use to specify the heading level of the confirmation's heading. If not specified, the confirmation heading will be rendered in an `h4` tag.
|
||||
|
||||
For more information see [MDL-87694](https://tracker.moodle.org/browse/MDL-87694)
|
||||
|
||||
### core_customfield
|
||||
|
||||
#### Added
|
||||
|
||||
- Added new `\core_customfield\api::is_shortname_unique(...)` method to determine whether a shortname is available for use inside a given handler
|
||||
|
||||
For more information see [MDL-87059](https://tracker.moodle.org/browse/MDL-87059)
|
||||
|
||||
### core_question
|
||||
|
||||
#### Fixed
|
||||
|
||||
- In order to prevent re-use of question version numbers after a version is deleted, the `nextversion` column was added to `question_bank_entries`. This serves as a counter incremented each time a version is created.
|
||||
Do not query this field directly. Instead use `core_question\versions::get_next_version()` to read the value, which will initialise it based on the existing versions if it is not set yet. By default, it will increment the version number automatically, unless you pass `increment: false`. Because of this, it is advisable to call it inside a transaction, that is only committed after the version number is used in a `question_versions` record.
|
||||
|
||||
For more information see [MDL-86798](https://tracker.moodle.org/browse/MDL-86798)
|
||||
|
||||
### customfield_number
|
||||
|
||||
#### Changed
|
||||
|
||||
- In order to fully support shared custom field categories, additional component/area/itemid parameters have been added to the following:
|
||||
|
||||
* The `customfield_number_recalculate_value` external method
|
||||
* The abstract `\customfield_number\provider_base::recalculate()` method
|
||||
* The `\customfield_number\task\recalculate` helpers for queueing task instances
|
||||
|
||||
For more information see [MDL-87714](https://tracker.moodle.org/browse/MDL-87714)
|
||||
|
||||
## 5.1.1
|
||||
|
||||
### core
|
||||
|
||||
@@ -125,11 +125,6 @@ if ($options['list']) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (moodle_needs_upgrading()) {
|
||||
mtrace("Moodle upgrade pending, cannot manage tasks.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if ($disable = $options['disable']) {
|
||||
if (!$task = \core\task\manager::get_scheduled_task($disable)) {
|
||||
mtrace("Task '$disable' not found");
|
||||
@@ -157,6 +152,11 @@ if ($disable = $options['disable']) {
|
||||
exit(1);
|
||||
}
|
||||
} else if ($execute = $options['execute']) {
|
||||
if (moodle_needs_upgrading()) {
|
||||
mtrace("Moodle upgrade pending, cannot execute tasks.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (!$task = \core\task\manager::get_scheduled_task($execute)) {
|
||||
mtrace("Task '$execute' not found");
|
||||
exit(1);
|
||||
|
||||
+7
-1
@@ -2,8 +2,11 @@
|
||||
"name": "moodle/moodle",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"description": "Moodle - the world's open source learning platform",
|
||||
"type": "project",
|
||||
"type": "moodle-core",
|
||||
"homepage": "https://moodle.org",
|
||||
"provide": {
|
||||
"moodle/lms": "5.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11",
|
||||
"mikey179/vfsstream": "1.6.*",
|
||||
@@ -26,6 +29,9 @@
|
||||
"core_phpunit\\": "public/lib/phpunit/classes/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"haspublicdir": true
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
|
||||
Generated
+681
-432
File diff suppressed because it is too large
Load Diff
Generated
+325
@@ -19,6 +19,7 @@
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"current-git-branch": "^2.0.2",
|
||||
"docdash": "^2.0.2",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-jsdoc": "^48.0.4",
|
||||
@@ -2261,6 +2262,13 @@
|
||||
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@sec-ant/readable-stream": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz",
|
||||
@@ -2270,6 +2278,19 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/merge-streams": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
|
||||
"integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz",
|
||||
@@ -4134,6 +4155,16 @@
|
||||
"uuid": "^3.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/current-git-branch": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/current-git-branch/-/current-git-branch-2.0.2.tgz",
|
||||
"integrity": "sha512-uf6Yvp3WGEGlJbsoseXIun9pvfUyts3nbHm6bn6/JBHiBgDbVifd/0dsJXLCo6ndRJGBlLxXhK+YNPBVouFdyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-git-repository": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-now": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
|
||||
@@ -7386,6 +7417,16 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
|
||||
"integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -7975,6 +8016,222 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-git-repository/-/is-git-repository-3.0.0.tgz",
|
||||
"integrity": "sha512-iCIObmkwkU6b15KB7rltAb60rIRXN4qAjVmBkqry8EWdwcJ9B7KUDCYrLIV2HOHENHYy9bm/MsDHDisoAZrpUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"execa": "^9.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/execa": {
|
||||
"version": "9.6.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz",
|
||||
"integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/merge-streams": "^4.0.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"figures": "^6.1.0",
|
||||
"get-stream": "^9.0.0",
|
||||
"human-signals": "^8.0.1",
|
||||
"is-plain-obj": "^4.1.0",
|
||||
"is-stream": "^4.0.1",
|
||||
"npm-run-path": "^6.0.0",
|
||||
"pretty-ms": "^9.2.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"strip-final-newline": "^4.0.0",
|
||||
"yoctocolors": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/figures": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
|
||||
"integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-unicode-supported": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/get-stream": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
|
||||
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sec-ant/readable-stream": "^0.4.1",
|
||||
"is-stream": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/is-plain-obj": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/is-stream": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
|
||||
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/is-unicode-supported": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/npm-run-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
|
||||
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^4.0.0",
|
||||
"unicorn-magic": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/npm-run-path/node_modules/path-key": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/is-git-repository/node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
@@ -10283,6 +10540,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-ms": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
|
||||
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-passwd": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
|
||||
@@ -10606,6 +10876,22 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-ms": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
|
||||
"integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse-ms": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@@ -12059,6 +12345,19 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-final-newline": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
|
||||
"integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-indent": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz",
|
||||
@@ -12737,6 +13036,19 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/unicorn-magic": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
|
||||
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||
@@ -13251,6 +13563,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
|
||||
"integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yuglify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yuglify/-/yuglify-2.0.0.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"current-git-branch": "^2.0.2",
|
||||
"docdash": "^2.0.2",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-jsdoc": "^48.0.4",
|
||||
|
||||
@@ -119,8 +119,8 @@ foreach ($settingspage->children as $childpage) {
|
||||
$outputhtml .= html_writer::tag('div', '<!-- -->', array('class' => 'clearer'));
|
||||
$outputhtml .= $setting->output_html($data);
|
||||
if ($childpage->has_dependencies()) {
|
||||
$opts = ['dependencies' => $childpage->get_dependencies_for_javascript()];
|
||||
$PAGE->requires->js_call_amd('core/showhidesettings', 'init', [$opts]);
|
||||
$context = ['dependencies' => json_encode($childpage->get_dependencies_for_javascript())];
|
||||
echo $OUTPUT->render_from_template('core_admin/settings_showhide', $context);
|
||||
}
|
||||
}
|
||||
$outputhtml .= html_writer::end_tag('fieldset');
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core_admin\admin;
|
||||
|
||||
use admin_setting;
|
||||
|
||||
/**
|
||||
* Render a notification as part of other admin settings.
|
||||
*
|
||||
* @package core_admin
|
||||
* @subpackage admin
|
||||
* @copyright 2025 Matt Porritt <matt.porritt@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class admin_setting_notification extends admin_setting {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $name The name of the setting.
|
||||
* @param string $notification The notification to display.
|
||||
* @param string $type The type of notification.
|
||||
* @param bool $cancelable Whether the notification can be canceled.
|
||||
*/
|
||||
public function __construct(
|
||||
string $name,
|
||||
/** @var string The notification to display. */
|
||||
protected string $notification,
|
||||
/** @var string The type of notification. */
|
||||
protected string $type = 'info',
|
||||
/** @var bool Whether the notification can be canceled. */
|
||||
protected bool $cancelable = false
|
||||
) {
|
||||
$this->nosave = true;
|
||||
|
||||
parent::__construct($name, '', '', '');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function get_setting(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function get_defaultsetting(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function write_setting($data): string {
|
||||
// Do not write any setting.
|
||||
return '';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function output_html($data, $query = ''): string {
|
||||
global $OUTPUT;
|
||||
|
||||
return $OUTPUT->notification($this->notification, $this->type, $this->cancelable);
|
||||
}
|
||||
}
|
||||
@@ -282,13 +282,30 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
*
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
public function admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
|
||||
$cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
|
||||
$buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
|
||||
$themedesignermode = false, $devlibdir = false, $mobileconfigured = false,
|
||||
$overridetossl = false, $invalidforgottenpasswordurl = false, $croninfrequent = false,
|
||||
$showcampaigncontent = false, bool $showfeedbackencouragement = false, bool $showservicesandsupport = false,
|
||||
$xmlrpcwarning = '') {
|
||||
public function admin_notifications_page(
|
||||
$maturity,
|
||||
$insecuredataroot,
|
||||
$errorsdisplayed,
|
||||
$cronoverdue,
|
||||
$dbproblems,
|
||||
$maintenancemode,
|
||||
$availableupdates,
|
||||
$availableupdatesfetch,
|
||||
$buggyiconvnomb,
|
||||
$registered,
|
||||
array $cachewarnings = [],
|
||||
$eventshandlers = 0,
|
||||
$themedesignermode = false,
|
||||
$devlibdir = false,
|
||||
$mobileconfigured = false,
|
||||
$overridetossl = false,
|
||||
$invalidforgottenpasswordurl = false,
|
||||
$croninfrequent = false,
|
||||
$showcampaigncontent = false,
|
||||
bool $showfeedbackencouragement = false,
|
||||
bool $showservicesandsupport = false,
|
||||
$xmlrpcwarning = ''
|
||||
) {
|
||||
|
||||
global $CFG;
|
||||
$output = '';
|
||||
@@ -313,6 +330,8 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
$output .= $this->mobile_configuration_warning($mobileconfigured);
|
||||
$output .= $this->forgotten_password_url_warning($invalidforgottenpasswordurl);
|
||||
$output .= $this->mnet_deprecation_warning($xmlrpcwarning);
|
||||
$output .= $this->moodlenet_removal_warning();
|
||||
$output .= $this->marketplace_integration_notice();
|
||||
$output .= $this->userfeedback_encouragement($showfeedbackencouragement);
|
||||
$output .= $this->services_and_support_content($showservicesandsupport);
|
||||
$output .= $this->campaign_content($showcampaigncontent);
|
||||
@@ -2326,6 +2345,33 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
return $this->warning($xmlrpcwarning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a warning about the removal of MoodleNet integration.
|
||||
*
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
protected function moodlenet_removal_warning(): string {
|
||||
$moodlenetenabled = get_config('tool_moodlenet', 'enablemoodlenet');
|
||||
if (!empty($moodlenetenabled)) {
|
||||
$moodlenetwarning = get_string('moodlenetremovalwarning', 'admin');
|
||||
return $this->warning($moodlenetwarning);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a notice about Moodle Marketplace integration.
|
||||
*
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
protected function marketplace_integration_notice(): string {
|
||||
$installer = tool_installaddon_installer::instance();
|
||||
$url = $installer->get_marketplace_url();
|
||||
$notice = get_string('marketplaceavailablenotice', 'admin', $url->out());
|
||||
return $this->warning($notice, 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the theme selector list.
|
||||
*
|
||||
|
||||
@@ -32,7 +32,6 @@ $adminroot = admin_get_root(); // need all settings here
|
||||
$adminroot->search = $query; // So we can reference it in search boxes later in this invocation
|
||||
$statusmsg = '';
|
||||
$errormsg = '';
|
||||
$focus = '';
|
||||
|
||||
// now we'll deal with the case that the admin has submitted the form with changed settings
|
||||
if ($data = data_submitted() and confirm_sesskey() and isset($data->action) and $data->action == 'save-settings') {
|
||||
@@ -41,7 +40,7 @@ if ($data = data_submitted() and confirm_sesskey() and isset($data->action) and
|
||||
if (!empty($adminroot->errors)) {
|
||||
$errormsg = get_string('errorwithsettings', 'admin');
|
||||
$firsterror = reset($adminroot->errors);
|
||||
$focus = $firsterror->id;
|
||||
$PAGE->set_focuscontrol($firsterror->id);
|
||||
} else {
|
||||
// No errors. Did we change any setting? If so, then redirect with success.
|
||||
if ($count) {
|
||||
@@ -55,7 +54,7 @@ $PAGE->set_primary_active_tab('siteadminnode');
|
||||
|
||||
// and finally, if we get here, then there are matching settings and we have to print a form
|
||||
// to modify them
|
||||
echo $OUTPUT->header($focus);
|
||||
echo $OUTPUT->header();
|
||||
|
||||
// Display a warning if site is not registered.
|
||||
if (empty($query)) {
|
||||
|
||||
@@ -72,6 +72,7 @@ if ($data = data_submitted() and confirm_sesskey() and isset($data->action) and
|
||||
} else {
|
||||
$errormsg = get_string('errorwithsettings', 'admin');
|
||||
$firsterror = reset($adminroot->errors);
|
||||
$PAGE->set_focuscontrol($firsterror->id);
|
||||
}
|
||||
$settingspage = $adminroot->locate($section, true);
|
||||
}
|
||||
@@ -165,10 +166,8 @@ if (empty($SITE->fullname)) {
|
||||
$PAGE->requires->js_call_amd('core_form/changechecker', 'watchFormById', ['adminsettings']);
|
||||
|
||||
if ($settingspage->has_dependencies()) {
|
||||
$opts = [
|
||||
'dependencies' => $settingspage->get_dependencies_for_javascript()
|
||||
];
|
||||
$PAGE->requires->js_call_amd('core/showhidesettings', 'init', [$opts]);
|
||||
$context = ['dependencies' => json_encode($settingspage->get_dependencies_for_javascript())];
|
||||
echo $OUTPUT->render_from_template('core_admin/settings_showhide', $context);
|
||||
}
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
@@ -32,19 +32,29 @@ if ($hassiteconfig) {
|
||||
$providers->add(new admin_setting_heading('availableproviders',
|
||||
get_string('availableproviders', 'core_ai'),
|
||||
get_string('availableproviders_desc', 'core_ai')));
|
||||
// Add call to action to add a new provider.
|
||||
$providers->add(new \core_admin\admin\admin_setting_template_render(
|
||||
name: 'addnewprovider',
|
||||
templatename: 'core_ai/admin_add_provider',
|
||||
context: ['addnewproviderurl' => new moodle_url('/ai/configure.php')]
|
||||
));
|
||||
|
||||
$providers->add(new \core_ai\admin\admin_setting_provider_manager(
|
||||
if (!empty(core_plugin_manager::instance()->get_plugins_of_type("aiprovider"))) {
|
||||
// Add call to action to add a new provider.
|
||||
$providers->add(new \core_admin\admin\admin_setting_template_render(
|
||||
name: 'addnewprovider',
|
||||
templatename: 'core_ai/admin_add_provider',
|
||||
context: ['addnewproviderurl' => new moodle_url('/ai/configure.php')]
|
||||
));
|
||||
|
||||
$providers->add(new \core_ai\admin\admin_setting_provider_manager(
|
||||
'aiprovider',
|
||||
\core_ai\table\aiprovider_management_table::class,
|
||||
'manageaiproviders',
|
||||
new lang_string('manageaiproviders', 'core_ai'),
|
||||
));
|
||||
));
|
||||
} else {
|
||||
$providers->add(new \core_admin\admin\admin_setting_notification(
|
||||
name:'noproviderplugins',
|
||||
notification: get_string('noproviderplugins', 'core_ai'),
|
||||
type: 'danger'
|
||||
));
|
||||
}
|
||||
|
||||
$ADMIN->add('ai', $providers);
|
||||
|
||||
// Add settings page for AI placement settings.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_admin/settings_showhide
|
||||
|
||||
Passes the necessary data to show/hide dependant settings.
|
||||
|
||||
Context variables required for this template:
|
||||
* dependencies - JSON-serialized data structure describing the dependencies between settings
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"dependencies": {
|
||||
"s__setting1":{
|
||||
"eq":[
|
||||
[
|
||||
"s__setting0"
|
||||
]
|
||||
]
|
||||
},
|
||||
"s__setting2":{
|
||||
"eq":[
|
||||
[
|
||||
"s__setting1"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
<span id="settings-showhide-{{uniqid}}" data-dependencies="{{dependencies}}"></span>
|
||||
|
||||
{{#js}}
|
||||
require(['core/showhidesettings'], function(ShowHideSettings) {
|
||||
ShowHideSettings.init("settings-showhide-{{uniqid}}");
|
||||
});
|
||||
{{/js}}
|
||||
@@ -1,5 +1,13 @@
|
||||
# tool_behat Upgrade notes
|
||||
|
||||
## 5.1.3+
|
||||
|
||||
### Added
|
||||
|
||||
- The `behat_session_trait::ensure_element_[does_not_]exists(...)` methods now accept optional `$container` parameter to define the parent node to look within
|
||||
|
||||
For more information see [MDL-75067](https://tracker.moodle.org/browse/MDL-75067)
|
||||
|
||||
## 5.0
|
||||
|
||||
### Added
|
||||
|
||||
@@ -224,6 +224,11 @@ if (empty($parallelrun)) {
|
||||
$cmds['singlerun'] = $runtestscommand;
|
||||
|
||||
echo "Running single behat site:" . PHP_EOL;
|
||||
// The inner PHP process is not marked as having an interactive terminal even if it's passed
|
||||
// through from this one which does, so we need to pass it through as an environment variable.
|
||||
if (function_exists('posix_isatty') && posix_isatty(STDOUT)) {
|
||||
putenv('MOODLE_BEHAT_RUNNING_IN_TTY=1');
|
||||
}
|
||||
passthru("php $runtestscommand", $status);
|
||||
$exitcodes['singlerun'] = $status;
|
||||
chdir($cwd);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
@tool @tool_dataprivacy
|
||||
Feature: Authorized users can request others personal data
|
||||
In order to export or access another users data
|
||||
As a designated role
|
||||
I need the correct permissions
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| user1 | User1 | One | user1@example.com |
|
||||
| user2 | User2 | Two | user2@example.com |
|
||||
| officer1 | Officer1 | One | officer1@example.com |
|
||||
# Create Privacy Officer Role.
|
||||
And the following "role" exists:
|
||||
| shortname | privacyofficer |
|
||||
| name | Privacy Officer |
|
||||
| context_system | 1 |
|
||||
| tool/dataprivacy:managedataregistry | allow |
|
||||
| tool/dataprivacy:managedatarequests | allow |
|
||||
| tool/dataprivacy:makedatarequestsforchildren | allow |
|
||||
| moodle/site:configview | allow |
|
||||
| moodle/category:viewhiddencategories | allow |
|
||||
| moodle/course:viewhiddencourses | allow |
|
||||
| moodle/course:viewhiddenactivities | allow |
|
||||
| moodle/course:view | allow |
|
||||
# Create Parent Role.
|
||||
And the following "role" exists:
|
||||
| shortname | parentrole |
|
||||
| name | Parent Role |
|
||||
| context_user | 1 |
|
||||
| moodle/user:viewdetails | allow |
|
||||
| moodle/user:viewalldetails | allow |
|
||||
| moodle/user:readuserblogs | allow |
|
||||
| moodle/user:readuserposts | allow |
|
||||
| moodle/user:viewuseractivitiesreport | allow |
|
||||
| moodle/user:editprofile | allow |
|
||||
| tool/policy:acceptbehalf | allow |
|
||||
| tool/dataprivacy:makedatarequestsforchildren | allow |
|
||||
# Add permission to allow parent to make requests on behalf of child user.
|
||||
And the following config values are set as admin:
|
||||
| contactdataprotectionofficer | 1 | tool_dataprivacy |
|
||||
And I log in as "admin"
|
||||
|
||||
@javascript
|
||||
Scenario: Privacy officer can request for other user's personal data
|
||||
Given I navigate to "Users > Permissions > Assign system roles" in site administration
|
||||
# Assign Privacy Officer role to officer1.
|
||||
And I follow "Privacy Officer"
|
||||
And I set the field "addselect_searchtext" to "Officer1"
|
||||
And I set the field "addselect" to "Officer1 One (officer1@example.com)"
|
||||
And I press "Add"
|
||||
# Navigate to home in order to navigate properly to Privacy settings.
|
||||
And I am on site homepage
|
||||
# Select Privacy officer in the Orivacy officer role mapping setting.
|
||||
And I navigate to "Users > Privacy and policies > Privacy settings" in site administration
|
||||
And I click on "Privacy Officer" "checkbox"
|
||||
And I press "Save changes"
|
||||
And I log in as "officer1"
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
# Create a new request as the designated privacy officer.
|
||||
When I follow "New request"
|
||||
And I set the field "User" to "User1 One"
|
||||
And I set the field "Comment" to "User One data"
|
||||
And I press "Save changes"
|
||||
# Confirm that the new data request is successfully created for selected user with status "Awaiting approval".
|
||||
Then the following should exist in the "generaltable" table:
|
||||
| Type | User | Requested by | Status | Message |
|
||||
| Export | User1 One | Officer1 One | Awaiting approval | User One data |
|
||||
|
||||
@javascript
|
||||
Scenario: Parent user can request data on behalf of child user
|
||||
Given I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I follow "User1 One"
|
||||
And I click on "Preferences" "link" in the ".profile_tree" "css_element"
|
||||
# Assign user2 as parent for user1.
|
||||
And I follow "Assign roles relative to this user"
|
||||
And I follow "Parent"
|
||||
And I set the field "Potential users" to "User2 Two (user2@example.com)"
|
||||
And I click on "Add" "button" in the "#page-content" "css_element"
|
||||
And I log in as "user2"
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Data requests"
|
||||
# As parent, create a data request for a child user.
|
||||
And I follow "New request"
|
||||
And I click on "User" "field"
|
||||
When I type "User1 One"
|
||||
# Confirm that only the parent's child users can be searched and selected.
|
||||
Then I should see "User1 One"
|
||||
And I type "User2 Two"
|
||||
And I should see "No suggestions"
|
||||
And I type "Officer1 One"
|
||||
And I should see "No suggestions"
|
||||
And I set the field "Search" to "User1"
|
||||
And I set the field "Comment" to "This is a comment"
|
||||
And I press "Save changes"
|
||||
# Confirm that data request was successfully made by parent on behalf of child user.
|
||||
And I should see "Your request has been submitted to the privacy officer"
|
||||
And the following should exist in the "generaltable" table:
|
||||
| Type | Requested by | Status | Message |
|
||||
| Export all of my personal data (User1 One) | User2 Two | Awaiting approval | This is a comment |
|
||||
@@ -0,0 +1,3 @@
|
||||
define("tool_installaddon/footer",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.footerClickListener=void 0;_exports.footerClickListener=()=>null}));
|
||||
|
||||
//# sourceMappingURL=footer.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"footer.min.js","sources":["../src/footer.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Activity chooser footer handlers for tool_installaddon.\n *\n * @module tool_installaddon/footer\n * @copyright 2026 Safat Shahin <safat.shahin@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// The footer is a plain link, so the chooser does not need extra click handling.\nexport const footerClickListener = () => null;\n"],"names":[],"mappings":"2LAwBmC,IAAM"}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Activity chooser footer handlers for tool_installaddon.
|
||||
*
|
||||
* @module tool_installaddon/footer
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
// The footer is a plain link, so the chooser does not need extra click handling.
|
||||
export const footerClickListener = () => null;
|
||||
@@ -70,6 +70,25 @@ class tool_installaddon_installer {
|
||||
$url = 'https://moodle.org/plugins/get.php';
|
||||
}
|
||||
|
||||
return $this->get_external_service_url($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URL to Moodle Marketplace.
|
||||
*
|
||||
* @return moodle_url
|
||||
*/
|
||||
public function get_marketplace_url(): moodle_url {
|
||||
return $this->get_external_service_url('https://marketplace.moodle.com/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URL to external service with optional encoded site information.
|
||||
*
|
||||
* @param string $url The external service base URL.
|
||||
* @return moodle_url
|
||||
*/
|
||||
protected function get_external_service_url(string $url): moodle_url {
|
||||
if (!$this->should_send_site_info()) {
|
||||
return new moodle_url($url);
|
||||
}
|
||||
|
||||
+9
-6
@@ -16,20 +16,23 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tool_moodlenet\task;
|
||||
namespace tool_installaddon\task;
|
||||
|
||||
/**
|
||||
* Ad-hoc task to perform post install tasks.
|
||||
* We use this to set the active activity chooser footer plugin to tool_moodlenet.
|
||||
* We use this to set the active activity chooser footer plugin to tool_installaddon.
|
||||
* We couldn't do this directly in install.php, because there is an admin_apply_default_settings() call after all plugins are
|
||||
* installed and that would reset whatever value we had set earlier to 'hidden'.
|
||||
*
|
||||
* @package tool_moodlenet
|
||||
* @copyright 2022 Shamim Rezaie <shamim@moodle.com>
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class post_install extends \core\task\adhoc_task {
|
||||
public function execute() {
|
||||
set_config('activitychooseractivefooter', 'tool_moodlenet');
|
||||
/**
|
||||
* Sets the active activity chooser footer plugin after install defaults are applied.
|
||||
*/
|
||||
public function execute(): void {
|
||||
set_config('activitychooseractivefooter', 'tool_installaddon');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Install script for tool_installaddon.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Perform the post-install procedures.
|
||||
*/
|
||||
function xmldb_tool_installaddon_install(): void {
|
||||
// Use an ad-hoc task to set the active activity chooser footer plugin to tool_installaddon.
|
||||
// We couldn't do this in admin/settings/courses.php for 2 reasons:
|
||||
// - First, because it would be a breach of component communications principles to do so there.
|
||||
// - Second, because we can't call get_plugins_with_function() during install and upgrade (or it will return []).
|
||||
// We couldn't do this directly here either, because there is an admin_apply_default_settings() call after all plugins are
|
||||
// installed and that would reset whatever value we set here to 'hidden'.
|
||||
$postinstall = new tool_installaddon\task\post_install();
|
||||
core\task\manager::queue_adhoc_task($postinstall);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Upgrade script for tool_installaddon.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Upgrade the plugin.
|
||||
*
|
||||
* @param int $oldversion
|
||||
* @return bool always true
|
||||
*/
|
||||
function xmldb_tool_installaddon_upgrade(int $oldversion): bool {
|
||||
if ($oldversion < 2025100601) {
|
||||
// Set the activity chooser active footer to include marketplace regardless of the previous setting.
|
||||
// We are deliberately setting this to increase awareness of marketplace.
|
||||
set_config('activitychooseractivefooter', 'tool_installaddon');
|
||||
|
||||
upgrade_plugin_savepoint(true, 2025100601, 'tool', 'installaddon');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['acknowledgement'] = 'Acknowledgement';
|
||||
$string['acknowledgementtext'] = 'I understand that it is my responsibility to have full backups of this site prior to installing additional plugins. I accept and understand that plugins (especially but not only those originating in unofficial sources) may contain security holes, can make the site unavailable, or cause private data leaks or loss.';
|
||||
$string['activitychooserfootertext'] = 'Browse more activities on';
|
||||
$string['featuredisabled'] = 'The plugin installer is disabled on this site.';
|
||||
$string['installaddon'] = 'Install plugin!';
|
||||
$string['installaddons'] = 'Install plugins';
|
||||
@@ -44,6 +45,8 @@ $string['installfromzipsubmit'] = 'Install plugin from the ZIP file';
|
||||
$string['installfromziptype'] = 'Plugin type';
|
||||
$string['installfromziptype_help'] = 'For plugins that correctly declare their component name, the installer is able to detect the plugin type automatically. If the auto-detection fails, choose the correct type of plugin manually. Warning: The installation procedure can fail badly if an incorrect plugin type is specified.';
|
||||
$string['installfromziptype_link'] = 'Development:Plugins';
|
||||
$string['marketplaceadminlinktext'] = 'Browse new plugins';
|
||||
$string['marketplacelink'] = 'Moodle Marketplace';
|
||||
$string['permcheck'] = 'Make sure the plugin type root location is writable by the web server process.';
|
||||
$string['permcheckerror'] = 'Error while checking for write permission';
|
||||
$string['permcheckprogress'] = 'Checking for write permission ...';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Public API for tool_installaddon.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use core_course\local\entity\activity_chooser_footer;
|
||||
|
||||
/**
|
||||
* Build activity chooser footer content for Marketplace.
|
||||
*
|
||||
* @param int $courseid The selected course id.
|
||||
* @param int $sectionid The selected section id.
|
||||
* @return activity_chooser_footer
|
||||
*/
|
||||
function tool_installaddon_custom_chooser_footer(int $courseid, int $sectionid): activity_chooser_footer {
|
||||
global $OUTPUT;
|
||||
|
||||
$installer = tool_installaddon_installer::instance();
|
||||
$marketplaceurl = $installer->get_marketplace_url();
|
||||
|
||||
$renderedfooter = $OUTPUT->render_from_template('tool_installaddon/chooser_footer', [
|
||||
'url' => $marketplaceurl->out(false),
|
||||
]);
|
||||
|
||||
return new activity_chooser_footer(
|
||||
'tool_installaddon/footer',
|
||||
$renderedfooter
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,12 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
if ($hassiteconfig and empty($CFG->disableupdateautodeploy)) {
|
||||
$installer = tool_installaddon_installer::instance();
|
||||
$ADMIN->add('modules', new admin_externalpage(
|
||||
'tool_installaddon_marketplace',
|
||||
get_string('marketplaceadminlinktext', 'tool_installaddon'),
|
||||
$installer->get_marketplace_url()->out(false)
|
||||
), 'modsettings');
|
||||
|
||||
$ADMIN->add('modules', new admin_externalpage('tool_installaddon_index',
|
||||
get_string('installaddons', 'tool_installaddon'),
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template tool_installaddon/chooser_footer
|
||||
|
||||
Activity chooser footer link to Marketplace.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"url": "https://marketplace.moodle.com/?site=hash"
|
||||
}
|
||||
}}
|
||||
<div class="w-100 d-flex px-2">
|
||||
<span class="my-auto me-1">{{#str}} activitychooserfootertext, tool_installaddon {{/str}}</span>
|
||||
{{> tool_installaddon/marketplace_link }}
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template tool_installaddon/marketplace_link
|
||||
|
||||
Reusable link to Moodle Marketplace.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"url": "https://marketplace.moodle.com/?site=hash"
|
||||
}
|
||||
}}
|
||||
<a class="d-inline my-auto" href="{{url}}" target="_blank" rel="noopener noreferrer">
|
||||
{{#str}} marketplacelink, tool_installaddon {{/str}}
|
||||
<i
|
||||
class="fa fa-external-link"
|
||||
role="img"
|
||||
aria-label="{{#str}} opensinnewwindow, core {{/str}}"
|
||||
title="{{#str}} opensinnewwindow, core {{/str}}"
|
||||
>
|
||||
</i>
|
||||
</a>
|
||||
@@ -0,0 +1,37 @@
|
||||
@tool @tool_installaddon @javascript
|
||||
Feature: Marketplace activity chooser footer
|
||||
In order to browse plugins from the activity chooser
|
||||
As a teacher
|
||||
I need to see the Marketplace footer link when installaddon is the active footer plugin
|
||||
|
||||
Background:
|
||||
Given the following config values are set as admin:
|
||||
| activitychooseractivefooter | tool_installaddon |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category | format |
|
||||
| Course 1 | C1 | 0 | topics |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
|
||||
Scenario: Activity chooser footer includes Marketplace link
|
||||
Given I log in as "teacher1"
|
||||
When I am on "Course 1" course homepage with editing mode on
|
||||
And I open the activity chooser
|
||||
Then "Browse more activities on" "text" should exist in the "Add an activity or resource" "dialogue"
|
||||
And "Marketplace" "link" should exist in the "Add an activity or resource" "dialogue"
|
||||
|
||||
Scenario: Back button works from activity help when the Marketplace footer is active
|
||||
Given I log in as "teacher1"
|
||||
When I am on "Course 1" course homepage with editing mode on
|
||||
And I open the activity chooser
|
||||
And I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
|
||||
Then "help" "core_course > Activity chooser screen" should be visible
|
||||
When I click on "Back" "button" in the "Add an activity or resource" "dialogue"
|
||||
Then "modules" "core_course > Activity chooser screen" should be visible
|
||||
And "help" "core_course > Activity chooser screen" should not be visible
|
||||
And "Marketplace" "link" should exist in the "Add an activity or resource" "dialogue"
|
||||
And "Back" "button" should not be visible
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Provides a mock testable_tool_installaddon_installer_without_site_info class.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @subpackage fixtures
|
||||
* @category test
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once(__DIR__ . '/testable_installer.php');
|
||||
|
||||
/**
|
||||
* Testable subclass with site-info sharing disabled.
|
||||
*
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class testable_tool_installaddon_installer_without_site_info extends testable_tool_installaddon_installer {
|
||||
/**
|
||||
* Disable site info sharing.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function should_send_site_info() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,14 @@
|
||||
namespace tool_installaddon;
|
||||
|
||||
use testable_tool_installaddon_installer;
|
||||
use testable_tool_installaddon_installer_without_site_info;
|
||||
use tool_installaddon_installer;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once(__DIR__.'/fixtures/testable_installer.php');
|
||||
require_once(__DIR__ . '/fixtures/testable_installer.php');
|
||||
require_once(__DIR__ . '/fixtures/testable_installer_without_site_info.php');
|
||||
|
||||
/**
|
||||
* Unit tests for the {@link tool_installaddon_installer} class
|
||||
@@ -32,16 +34,14 @@ require_once(__DIR__.'/fixtures/testable_installer.php');
|
||||
* @category test
|
||||
* @copyright 2013 David Mudrak <david@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \tool_installaddon_installer
|
||||
*/
|
||||
final class installer_test extends \advanced_testcase {
|
||||
|
||||
public function test_get_addons_repository_url(): void {
|
||||
$installer = testable_tool_installaddon_installer::instance();
|
||||
$url = $installer->get_addons_repository_url();
|
||||
$query = parse_url($url, PHP_URL_QUERY);
|
||||
$this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches));
|
||||
$site = rawurldecode($matches[1]);
|
||||
$site = json_decode(base64_decode($site), true);
|
||||
$site = $this->decode_site_info_from_url($url);
|
||||
$this->assertIsArray($site);
|
||||
$this->assertEquals(3, count($site));
|
||||
$this->assertSame('Nasty site', $site['fullname']);
|
||||
@@ -49,6 +49,34 @@ final class installer_test extends \advanced_testcase {
|
||||
$this->assertSame("2.5'; DROP TABLE mdl_user; --", $site['majorversion']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that marketplace URL includes expected host, scheme and site payload.
|
||||
*/
|
||||
public function test_get_marketplace_url(): void {
|
||||
$installer = testable_tool_installaddon_installer::instance();
|
||||
$addonsurl = $installer->get_addons_repository_url();
|
||||
$marketplaceurl = $installer->get_marketplace_url();
|
||||
|
||||
$this->assertSame('marketplace.moodle.com', parse_url($marketplaceurl, PHP_URL_HOST));
|
||||
$this->assertSame('https', parse_url($marketplaceurl, PHP_URL_SCHEME));
|
||||
$this->assertSame(
|
||||
$this->decode_site_info_from_url($addonsurl),
|
||||
$this->decode_site_info_from_url($marketplaceurl),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that marketplace URL has no query string when site info is unavailable.
|
||||
*/
|
||||
public function test_get_marketplace_url_without_site_info(): void {
|
||||
$installer = testable_tool_installaddon_installer_without_site_info::instance();
|
||||
$marketplaceurl = $installer->get_marketplace_url();
|
||||
|
||||
$this->assertSame('marketplace.moodle.com', parse_url($marketplaceurl, PHP_URL_HOST));
|
||||
$this->assertSame('https', parse_url($marketplaceurl, PHP_URL_SCHEME));
|
||||
$this->assertEmpty(parse_url($marketplaceurl, PHP_URL_QUERY));
|
||||
}
|
||||
|
||||
public function test_decode_remote_request(): void {
|
||||
$installer = testable_tool_installaddon_installer::instance();
|
||||
|
||||
@@ -132,7 +160,7 @@ $plugin->version = 2014121300;
|
||||
$versionphp = file_get_contents($fixtures.'/github/moodle-repository_mahara-master/version.php');
|
||||
$this->assertEquals('repository_mahara', $installer->testable_detect_plugin_component_from_versionphp($versionphp));
|
||||
|
||||
$versionphp = file_get_contents($fixtures.'/nocomponent/baz/version.php');
|
||||
$versionphp = file_get_contents($fixtures . '/nocomponent/baz/version.php');
|
||||
$this->assertFalse($installer->testable_detect_plugin_component_from_versionphp($versionphp));
|
||||
}
|
||||
|
||||
@@ -143,15 +171,28 @@ $plugin->version = 2014121300;
|
||||
$storage1 = $installer->make_installfromzip_storage();
|
||||
$this->assertTrue(is_dir($storage1));
|
||||
$this->assertTrue(is_writable($storage1));
|
||||
file_put_contents($storage1.'/hello.txt', 'Find me if you can!');
|
||||
file_put_contents($storage1 . '/hello.txt', 'Find me if you can!');
|
||||
|
||||
// Check we get unique directory on each call.
|
||||
$storage2 = $installer->make_installfromzip_storage();
|
||||
$this->assertTrue(is_dir($storage2));
|
||||
$this->assertTrue(is_writable($storage2));
|
||||
$this->assertFalse(file_exists($storage2.'/hello.txt'));
|
||||
$this->assertFalse(file_exists($storage2 . '/hello.txt'));
|
||||
|
||||
// Check both are in the same parent directory.
|
||||
$this->assertEquals(dirname($storage1), dirname($storage2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the encoded 'site' URL query parameter.
|
||||
*
|
||||
* @param moodle_url $url
|
||||
* @return array
|
||||
*/
|
||||
private function decode_site_info_from_url(\moodle_url $url): array {
|
||||
$query = parse_url($url, PHP_URL_QUERY);
|
||||
$this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches));
|
||||
$site = rawurldecode($matches[1]);
|
||||
return json_decode(base64_decode($site), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Unit tests for tool_installaddon lib.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_installaddon;
|
||||
|
||||
use core_course\local\entity\activity_chooser_footer;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/admin/tool/installaddon/lib.php');
|
||||
|
||||
/**
|
||||
* Test installaddon lib functions.
|
||||
*/
|
||||
final class lib_test extends \advanced_testcase {
|
||||
/**
|
||||
* Tests chooser footer generation for marketplace link content.
|
||||
*
|
||||
* @covers ::tool_installaddon_custom_chooser_footer
|
||||
*/
|
||||
public function test_tool_installaddon_custom_chooser_footer(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$footer = \tool_installaddon_custom_chooser_footer(1, 1);
|
||||
|
||||
$this->assertInstanceOf(activity_chooser_footer::class, $footer);
|
||||
$this->assertSame('tool_installaddon/footer', $footer->get_footer_js_file());
|
||||
$this->assertStringContainsString('https://marketplace.moodle.com/', $footer->get_footer_template());
|
||||
$this->assertStringContainsString('site=', $footer->get_footer_template());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tool_installaddon\task;
|
||||
|
||||
/**
|
||||
* Unit tests for the post install task.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @category test
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \tool_installaddon\task\post_install
|
||||
*/
|
||||
final class post_install_test extends \advanced_testcase {
|
||||
/**
|
||||
* Tests that the task sets the activity chooser footer plugin to tool_installaddon.
|
||||
*/
|
||||
public function test_execute_sets_activitychooseractivefooter_config(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
set_config('activitychooseractivefooter', 'hidden');
|
||||
|
||||
$task = new post_install();
|
||||
$task->execute();
|
||||
|
||||
$this->assertSame(
|
||||
'tool_installaddon',
|
||||
get_config('core', 'activitychooseractivefooter'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,6 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'tool_installaddon';
|
||||
$plugin->version = 2025100600;
|
||||
$plugin->version = 2025100601;
|
||||
$plugin->requires = 2025092600;
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
|
||||
@@ -598,6 +598,10 @@ class manager {
|
||||
$urls = [
|
||||
new \moodle_url('/login/logout.php'),
|
||||
new \moodle_url('/admin/tool/mfa/guide.php'),
|
||||
// Allow email self-registration confirmation to complete so that
|
||||
// auth_email can restore wantsurl from the auth_email_wantsurl user
|
||||
// preference before MFA intercepts on the next request.
|
||||
new \moodle_url('/login/confirm.php'),
|
||||
];
|
||||
foreach ($factors as $factor) {
|
||||
$urls = array_merge($urls, $factor->get_no_redirect_urls());
|
||||
@@ -605,8 +609,9 @@ class manager {
|
||||
|
||||
// Allow forced redirection exclusions.
|
||||
if ($exclusions = get_config('tool_mfa', 'redir_exclusions')) {
|
||||
foreach (explode("\n", $exclusions) as $exclusion) {
|
||||
$urls[] = new \moodle_url($exclusion);
|
||||
$exclusions = preg_split('/\n|\r/', $exclusions, -1, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($exclusions as $exclusion) {
|
||||
$urls[] = new \moodle_url(trim($exclusion));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,3 +38,8 @@ input.tool-mfa-verification-code,
|
||||
.mfa-verify-form .form-group.fitem > span > input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#mfalist .icon-circle .icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace tool_mfa;
|
||||
* Tests for MFA manager class.
|
||||
*
|
||||
* @package tool_mfa
|
||||
* @covers \tool_mfa\manager
|
||||
* @author Peter Burnett <peterburnett@catalyst-au.net>
|
||||
* @copyright Catalyst IT
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
@@ -30,9 +31,6 @@ final class manager_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Tests getting the factor total weight
|
||||
*
|
||||
* @covers ::get_total_weight
|
||||
* @covers ::setup_user_factor
|
||||
*/
|
||||
public function test_get_total_weight(): void {
|
||||
$this->resetAfterTest(true);
|
||||
@@ -76,8 +74,6 @@ final class manager_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Tests getting the factor status
|
||||
*
|
||||
* @covers ::get_status
|
||||
*/
|
||||
public function test_get_status(): void {
|
||||
$this->resetAfterTest(true);
|
||||
@@ -117,8 +113,6 @@ final class manager_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Tests checking if passed enough factors
|
||||
*
|
||||
* @covers ::passed_enough_factors
|
||||
*/
|
||||
public function test_passed_enough_factors(): void {
|
||||
$this->resetAfterTest(true);
|
||||
@@ -151,6 +145,30 @@ final class manager_test extends \advanced_testcase {
|
||||
$this->assertEquals(true, \tool_mfa\manager::passed_enough_factors());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test reading no redirect URLs configuration
|
||||
*/
|
||||
public function test_get_no_redirect_urls(): void {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Ensure all EOL markers are covered.
|
||||
$config = "/user/profile.php\r" .
|
||||
"/course/index.php\n" .
|
||||
"/admin/settings.php\r\n" .
|
||||
"/mod/bar/view.php ";
|
||||
|
||||
set_config('redir_exclusions', $config, 'tool_mfa');
|
||||
|
||||
// Cast URLs to string for simpler comparison.
|
||||
$noredirecturls = array_map(fn(\moodle_url $url) => (string) $url, manager::get_no_redirect_urls());
|
||||
|
||||
$this->assertContains((string) (new \moodle_url('/user/profile.php')), $noredirecturls);
|
||||
$this->assertContains((string) (new \moodle_url('/course/index.php')), $noredirecturls);
|
||||
$this->assertContains((string) (new \moodle_url('/admin/settings.php')), $noredirecturls);
|
||||
$this->assertContains((string) (new \moodle_url('/mod/bar/view.php')), $noredirecturls);
|
||||
}
|
||||
|
||||
/**
|
||||
* The data provider for whether urls should be redirected or not
|
||||
*
|
||||
@@ -178,7 +196,6 @@ final class manager_test extends \advanced_testcase {
|
||||
/**
|
||||
* Tests whether it should require mfa
|
||||
*
|
||||
* @covers ::should_require_mfa
|
||||
* @param string $urlstring
|
||||
* @param string $webroot
|
||||
* @param bool $status
|
||||
@@ -197,8 +214,6 @@ final class manager_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Tests whether it should require the mfa checks
|
||||
*
|
||||
* @covers ::should_require_mfa
|
||||
*/
|
||||
public function test_should_require_mfa_checks(): void {
|
||||
// Setup test and user.
|
||||
@@ -285,8 +300,6 @@ final class manager_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Tests should require the mfa redirection loop
|
||||
*
|
||||
* @covers ::should_require_mfa
|
||||
*/
|
||||
public function test_should_require_mfa_redirection_loop(): void {
|
||||
// Setup test and user.
|
||||
@@ -341,9 +354,6 @@ final class manager_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Tests checking for possible setup factor
|
||||
*
|
||||
* @covers ::possible_factor_setup
|
||||
* @covers ::setup_user_factor
|
||||
*/
|
||||
public function test_possible_factor_setup(): void {
|
||||
// Setup test and user.
|
||||
@@ -377,8 +387,6 @@ final class manager_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Tests checking if a factor is ready
|
||||
*
|
||||
* @covers ::is_ready
|
||||
*/
|
||||
public function test_is_ready(): void {
|
||||
// Setup test and user.
|
||||
@@ -419,9 +427,6 @@ final class manager_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Tests core hooks
|
||||
*
|
||||
* @covers ::mfa_config_hook_test
|
||||
* @covers ::mfa_login_hook_test
|
||||
*/
|
||||
public function test_core_hooks(): void {
|
||||
// Setup test and user.
|
||||
@@ -438,9 +443,31 @@ final class manager_test extends \advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests circular redirect auth
|
||||
* Tests that /login/confirm.php is excluded from MFA redirection.
|
||||
*
|
||||
* When a user follows an email self-registration confirmation link, MFA must
|
||||
* not intercept the request before auth_email::user_confirm() has had a chance
|
||||
* to restore the wantsurl from the auth_email_wantsurl user preference.
|
||||
*
|
||||
* @covers ::should_require_mfa
|
||||
* @covers ::get_no_redirect_urls
|
||||
*/
|
||||
public function test_confirm_url_no_redirect(): void {
|
||||
$this->resetAfterTest(true);
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user);
|
||||
|
||||
$confirmurl = new \moodle_url('/login/confirm.php');
|
||||
$this->assertEquals(
|
||||
\tool_mfa\manager::NO_REDIRECT,
|
||||
\tool_mfa\manager::should_require_mfa($confirmurl, false),
|
||||
'/login/confirm.php must not trigger an MFA redirect so that auth_email can ' .
|
||||
'restore wantsurl from the auth_email_wantsurl user preference first.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests circular redirect auth
|
||||
*/
|
||||
public function test_circular_redirect_auth(): void {
|
||||
// Setup test and user.
|
||||
|
||||
@@ -28,12 +28,5 @@ declare(strict_types=1);
|
||||
* Perform the post-install procedures.
|
||||
*/
|
||||
function xmldb_tool_moodlenet_install() {
|
||||
// Use an ad-hoc task to set the active activity chooser footer plugin to tool_moodlenet.
|
||||
// We couldn't do this in admin/settings/courses.php for 2 reasons:
|
||||
// - First, because it would be a breach of component communications principles to do so there.
|
||||
// - Second, because we can't call get_plugins_with_function() during install and upgrade (or it will return []).
|
||||
// We couldn't do this directly here either, because there is an admin_apply_default_settings() call after all plugins are
|
||||
// installed and that would reset whatever value we set here to 'hidden'.
|
||||
$postinstall = new tool_moodlenet\task\post_install();
|
||||
core\task\manager::queue_adhoc_task($postinstall);
|
||||
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ $string['defaultmoodlenet_desc'] = 'The URL of the MoodleNet instance available
|
||||
$string['defaultmoodlenetname'] = "MoodleNet instance name";
|
||||
$string['defaultmoodlenetnamevalue'] = 'MoodleNet Central';
|
||||
$string['defaultmoodlenetname_desc'] = 'The name of the MoodleNet instance available via the activity chooser.';
|
||||
$string['removalwarning_feature'] = 'If you need to continue using MoodleNet, contact your site administrator about setting up a self-hosted MoodleNet instance.';
|
||||
$string['removalwarning_service'] = 'After the date, you will no longer be able to browse or add content from MoodleNet Central.';
|
||||
$string['removalwarning_title'] = 'The MoodleNet service will be shut down on 20 April 2026.';
|
||||
$string['enablemoodlenet'] = 'Enable MoodleNet integration (inbound)';
|
||||
$string['enablemoodlenet_desc'] = 'If enabled, a user with the capability to create and manage activities can browse MoodleNet via the activity chooser and import MoodleNet resources into their course. In addition, a user with the capability to restore backups can select a backup file on MoodleNet and restore it into Moodle.';
|
||||
$string['errorduringdownload'] = 'An error occurred while downloading the file: {$a}';
|
||||
|
||||
@@ -27,8 +27,16 @@
|
||||
<div class="content text-start mb-5 px-5 py-4" data-region="chooser-option-summary-content-container">
|
||||
<div data-region="moodle-net">
|
||||
<div class="overlay-icon-container z-index-1 d-none" data-region="spinner"></div>
|
||||
<img class="w-25 mb-4" aria-hidden="true" src="{{{img}}}">
|
||||
<img class="w-25 mb-4" alt="" aria-hidden="true" src="{{{img}}}">
|
||||
<p>{{#str}} instancedescription, tool_moodlenet {{/str}}</p>
|
||||
|
||||
{{! Removal warning - always visible when MoodleNet integration is enabled }}
|
||||
<div class="alert alert-warning w-75 mx-auto mb-3" role="alert">
|
||||
<strong>{{#str}}removalwarning_title, tool_moodlenet{{/str}}</strong>
|
||||
<p class="mb-1">{{#str}}removalwarning_service, tool_moodlenet{{/str}}</p>
|
||||
<p class="mb-0">{{#str}}removalwarning_feature, tool_moodlenet{{/str}}</p>
|
||||
</div>
|
||||
|
||||
<p class="w-75 mx-auto mb-1 mt-5">{{#str}} connectandbrowse, tool_moodlenet {{/str}}</p>
|
||||
<a class="btn btn-secondary d-block w-75 mx-auto mb-4"
|
||||
data-action="browse"
|
||||
|
||||
@@ -61,7 +61,8 @@ Feature: Basic OAuth2 functionality
|
||||
And I should see "device_authorization_endpoint"
|
||||
And I navigate to "Server > OAuth 2 services" in site administration
|
||||
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
|
||||
And I should see "firstname" in the "givenname" "table_row"
|
||||
And I should see "firstname" in the "given_name" "table_row"
|
||||
And I should see "lastname" in the "family_name" "table_row"
|
||||
And I should see "idnumber" in the "sub" "table_row"
|
||||
And I should see "email" in the "email" "table_row"
|
||||
And I should see "lang" in the "locale" "table_row"
|
||||
|
||||
@@ -49,8 +49,10 @@ function xmldb_tool_recyclebin_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v5.0.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2025041401) {
|
||||
// Automatically generated Moodle v5.1.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2025100601) {
|
||||
// Changing precision of field shortname on table tool_recyclebin_category to (1333).
|
||||
$table = new xmldb_table('tool_recyclebin_category');
|
||||
$field = new xmldb_field('shortname', XMLDB_TYPE_CHAR, '1333', null, XMLDB_NOTNULL, null, null, 'categoryid');
|
||||
@@ -73,11 +75,8 @@ function xmldb_tool_recyclebin_upgrade($oldversion) {
|
||||
$dbman->change_field_precision($table, $field);
|
||||
|
||||
// Recyclebin savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2025041401, 'tool', 'recyclebin');
|
||||
upgrade_plugin_savepoint(true, 2025100601, 'tool', 'recyclebin');
|
||||
}
|
||||
|
||||
// Automatically generated Moodle v5.1.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2025100600; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2025100601; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2025092600; // Requires this Moodle version.
|
||||
$plugin->component = 'tool_recyclebin'; // Full name of the plugin (used for diagnostics).
|
||||
|
||||
@@ -49,7 +49,7 @@ function tool_task_mtrace_wrapper(string $message, string $eol = ''): void {
|
||||
|
||||
// We autolink urls and emails here but can't use format_text as it does
|
||||
// more than we need and has side effects which are not useful in this context.
|
||||
$urlpattern = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/';
|
||||
$urlpattern = '~\b(?:https?|ftps?)://[a-z0-9-]+(?:\.[a-z0-9-]+)*(?::\d+)?(?:/[^\s<]*)?~i';
|
||||
$message = preg_replace_callback($urlpattern, function($matches) {
|
||||
$url = $matches[0];
|
||||
return html_writer::link($url, $url, ['target' => '_blank']);
|
||||
|
||||
@@ -58,7 +58,7 @@ class tool_task_renderer extends plugin_renderer_base {
|
||||
get_string('nextruntime', 'tool_task'),
|
||||
];
|
||||
|
||||
$table->attributes['class'] = 'admintable table generaltable table-hover';
|
||||
$table->attributes['class'] = 'admintable table generaltable table-striped table-hover';
|
||||
$table->colclasses = [];
|
||||
|
||||
// For each task entry (row) show action buttons/logs link depending on the user permissions.
|
||||
@@ -388,7 +388,7 @@ class tool_task_renderer extends plugin_renderer_base {
|
||||
get_string('default', 'tool_task'),
|
||||
];
|
||||
|
||||
$table->attributes['class'] = 'admintable table generaltable table-hover';
|
||||
$table->attributes['class'] = 'admintable table generaltable table-striped table-hover';
|
||||
$table->colclasses = [];
|
||||
|
||||
if (!$showloglink) {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace tool_task;
|
||||
|
||||
/**
|
||||
* Test for the lib class.
|
||||
*
|
||||
* @package tool_task
|
||||
* @copyright 2026 Brendan Heywood <brendan@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class lib_test extends \advanced_testcase {
|
||||
/**
|
||||
* Data provider for mtrace
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function tool_task_mtrace_wrapper_provider(): array {
|
||||
return [
|
||||
[
|
||||
'A url http://moodle.com',
|
||||
'A url <a target="_blank" href="http://moodle.com">http://moodle.com</a>',
|
||||
],
|
||||
[
|
||||
'A url https://moodle.com',
|
||||
'A url <a target="_blank" href="https://moodle.com">https://moodle.com</a>',
|
||||
],
|
||||
[
|
||||
'A url https://moodle.com post text',
|
||||
'A url <a target="_blank" href="https://moodle.com">https://moodle.com</a> post text',
|
||||
],
|
||||
[
|
||||
'A url https://moodle.com. In a paragraph',
|
||||
'A url <a target="_blank" href="https://moodle.com">https://moodle.com</a>. In a paragraph',
|
||||
],
|
||||
[
|
||||
'A url https://localhost post text',
|
||||
'A url <a target="_blank" href="https://localhost">https://localhost</a> post text',
|
||||
],
|
||||
[
|
||||
'A url https://main.localhost post text',
|
||||
'A url <a target="_blank" href="https://main.localhost">https://main.localhost</a> post text',
|
||||
],
|
||||
[
|
||||
'email info@moodle.com after',
|
||||
'email <a href="mailto:info@moodle.com">info@moodle.com</a> after',
|
||||
],
|
||||
[
|
||||
'A sentence that ends in info@moodle.com. With another sentence.',
|
||||
'A sentence that ends in <a href="mailto:info@moodle.com">info@moodle.com</a>. With another sentence.',
|
||||
],
|
||||
];
|
||||
}
|
||||
/**
|
||||
* Test validations for minute field.
|
||||
* @dataProvider tool_task_mtrace_wrapper_provider
|
||||
* @param string $output task output
|
||||
* @param string $expected html
|
||||
* @covers ::tool_task_mtrace_wrapper
|
||||
*/
|
||||
public function test_tool_task_mtrace_wrapper(string $output, string $expected): void {
|
||||
global $CFG;
|
||||
require_once("{$CFG->dirroot}/{$CFG->admin}/tool/task/lib.php");
|
||||
|
||||
$this->expectOutputString($expected);
|
||||
$result = tool_task_mtrace_wrapper($output);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
@tool @tool_uploaduser @_file_upload
|
||||
Feature: Admin can suspend user course enrolment via CSV upload
|
||||
In order to manage enrolments in bulk
|
||||
As an administrator
|
||||
I need to be able to enrol and suspend users using CSV upload
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
| Course 2 | C2 |
|
||||
| Course 3 | C3 |
|
||||
|
||||
@javascript
|
||||
Scenario: Admin uploads enrol and suspend CSVs and verifies enrolment status
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Users > Accounts > Upload users" in site administration
|
||||
When I upload "lib/tests/fixtures/QA_user_enrol.txt" file to "File" filemanager
|
||||
And I press "Upload users"
|
||||
And I press "Upload users"
|
||||
And I press "Continue"
|
||||
And I upload "lib/tests/fixtures/QA_user_suspend.txt" file to "File" filemanager
|
||||
And I press "Upload users"
|
||||
And I set the field "Upload type" to "Update existing users only"
|
||||
And I press "Upload users"
|
||||
And I press "Continue"
|
||||
And I am on the "Course 1" "enrolled users" page
|
||||
Then the following should exist in the "participants" table:
|
||||
| First name | Status |
|
||||
| Learner One | Active |
|
||||
| Learner Two | Active |
|
||||
And I am on the "Course 2" "enrolled users" page
|
||||
And the following should exist in the "participants" table:
|
||||
| First name | Status |
|
||||
| Learner One | Active |
|
||||
| Learner Two | Suspended |
|
||||
And I am on the "Course 3" "enrolled users" page
|
||||
And the following should exist in the "participants" table:
|
||||
| First name | Status |
|
||||
| Learner One | Suspended |
|
||||
| Learner Two | Active |
|
||||
@@ -35,8 +35,6 @@ if (isset($newsettings['frontpagesettings'])) {
|
||||
$newsettingshtml = implode($newsettings);
|
||||
unset($newsettings);
|
||||
|
||||
$focus = '';
|
||||
|
||||
if (empty($adminroot->errors) and $newsettingshtml === '') {
|
||||
// there must be either redirect without message or continue button or else upgrade would be sometimes broken
|
||||
if ($return == 'site') {
|
||||
@@ -48,12 +46,12 @@ if (empty($adminroot->errors) and $newsettingshtml === '') {
|
||||
|
||||
if (!empty($adminroot->errors)) {
|
||||
$firsterror = reset($adminroot->errors);
|
||||
$focus = $firsterror->id;
|
||||
$PAGE->set_focuscontrol($firsterror->id);
|
||||
}
|
||||
|
||||
// and finally, if we get here, then there are new settings and we have to print a form
|
||||
// to modify them
|
||||
echo $OUTPUT->header($focus);
|
||||
echo $OUTPUT->header();
|
||||
|
||||
if (!empty($SITE->fullname) and !empty($SITE->shortname)) {
|
||||
echo $OUTPUT->box(get_string('upgradesettingsintro','admin'), 'generalbox');
|
||||
|
||||
@@ -196,8 +196,12 @@ class external_service_functions_form extends moodleform {
|
||||
//we add the descriptions to the functions
|
||||
foreach ($functions as $functionid => $functionname) {
|
||||
//retrieve full function information (including the description)
|
||||
$function = \core_external\external_api::external_function_info($functionname);
|
||||
if (empty($function->deprecated)) {
|
||||
try {
|
||||
$function = \core_external\external_api::external_function_info($functionname);
|
||||
} catch (Throwable $exception) {
|
||||
$function = null;
|
||||
}
|
||||
if ($function !== null && empty($function->deprecated)) {
|
||||
$functions[$functionid] = $function->name . ':' . $function->description;
|
||||
} else {
|
||||
// Exclude the deprecated ones.
|
||||
|
||||
@@ -72,6 +72,12 @@ $PAGE->set_pagelayout('admin');
|
||||
$PAGE->set_title($title);
|
||||
$PAGE->set_heading($title);
|
||||
|
||||
// Explode if there are no provider plugins installed.
|
||||
$plugins = core_plugin_manager::instance()->get_plugins_of_type('aiprovider');
|
||||
if (empty($plugins)) {
|
||||
throw new moodle_exception('noproviderplugins', 'core_ai');
|
||||
}
|
||||
|
||||
// Provider instance form processing.
|
||||
$mform = new \core_ai\form\ai_provider_form(customdata: $data);
|
||||
if ($mform->is_cancelled()) {
|
||||
|
||||
@@ -143,7 +143,7 @@ abstract class abstract_processor extends process_base {
|
||||
$errormessage = $response->getReasonPhrase();
|
||||
} else {
|
||||
$bodyobj = json_decode($response->getBody()->getContents());
|
||||
$errormessage = $bodyobj->error->message;
|
||||
$errormessage = $bodyobj->error;
|
||||
}
|
||||
|
||||
return \core_ai\error\factory::create($status, $errormessage)->get_error_details();
|
||||
|
||||
@@ -55,7 +55,7 @@ class provider extends \core_ai\provider {
|
||||
|
||||
#[\Override]
|
||||
public function add_authentication_headers(RequestInterface $request): RequestInterface {
|
||||
if (empty($this->config['basicauthenabled'])) {
|
||||
if (empty($this->config['enablebasicauth'])) {
|
||||
return $request;
|
||||
} else {
|
||||
// Add the Authorization header for basic auth.
|
||||
|
||||
@@ -151,12 +151,21 @@ final class process_explain_text_test extends \advanced_testcase {
|
||||
$responses = [
|
||||
500 => new Response(500, ['Content-Type' => 'application/json']),
|
||||
503 => new Response(503, ['Content-Type' => 'application/json']),
|
||||
401 => new Response(401, ['Content-Type' => 'application/json'],
|
||||
'{"error": {"message": "Invalid Authentication"}}'),
|
||||
404 => new Response(404, ['Content-Type' => 'application/json'],
|
||||
'{"error": {"message": "You must be a member of an organization to use the API"}}'),
|
||||
429 => new Response(429, ['Content-Type' => 'application/json'],
|
||||
'{"error": {"message": "Rate limit reached for requests"}}'),
|
||||
401 => new Response(
|
||||
401,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => 'Invalid Authentication']),
|
||||
),
|
||||
404 => new Response(
|
||||
404,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => 'You must be a member of an organization to use the API']),
|
||||
),
|
||||
429 => new Response(
|
||||
429,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => 'Rate limit reached for requests']),
|
||||
),
|
||||
];
|
||||
|
||||
$processor = new process_explain_text($this->provider, $this->action);
|
||||
@@ -317,7 +326,7 @@ final class process_explain_text_test extends \advanced_testcase {
|
||||
$mock->append(new Response(
|
||||
401,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => ['message' => 'Invalid Authentication']]),
|
||||
json_encode(['error' => 'Invalid Authentication']),
|
||||
));
|
||||
|
||||
$processor = new process_explain_text($this->provider, $this->action);
|
||||
|
||||
@@ -153,17 +153,17 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
401 => new Response(
|
||||
401,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => ['message' => 'Invalid Authentication']]),
|
||||
json_encode(['error' => 'Invalid Authentication']),
|
||||
),
|
||||
404 => new Response(
|
||||
404,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => ['message' => 'You must be a member of an organization to use the API']]),
|
||||
json_encode(['error' => 'You must be a member of an organization to use the API']),
|
||||
),
|
||||
429 => new Response(
|
||||
429,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => ['message' => 'Rate limit reached for requests']]),
|
||||
json_encode(['error' => 'Rate limit reached for requests']),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -326,7 +326,7 @@ final class process_generate_text_test extends \advanced_testcase {
|
||||
$mock->append(new Response(
|
||||
401,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => ['message' => 'Invalid Authentication']]),
|
||||
json_encode(['error' => 'Invalid Authentication']),
|
||||
));
|
||||
|
||||
$processor = new process_generate_text($this->provider, $this->action);
|
||||
|
||||
@@ -151,12 +151,21 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
$responses = [
|
||||
500 => new Response(500, ['Content-Type' => 'application/json']),
|
||||
503 => new Response(503, ['Content-Type' => 'application/json']),
|
||||
401 => new Response(401, ['Content-Type' => 'application/json'],
|
||||
'{"error": {"message": "Invalid Authentication"}}'),
|
||||
404 => new Response(404, ['Content-Type' => 'application/json'],
|
||||
'{"error": {"message": "You must be a member of an organization to use the API"}}'),
|
||||
429 => new Response(429, ['Content-Type' => 'application/json'],
|
||||
'{"error": {"message": "Rate limit reached for requests"}}'),
|
||||
401 => new Response(
|
||||
401,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => 'Invalid Authentication']),
|
||||
),
|
||||
404 => new Response(
|
||||
404,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => 'You must be a member of an organization to use the API']),
|
||||
),
|
||||
429 => new Response(
|
||||
429,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => 'Rate limit reached for requests']),
|
||||
),
|
||||
];
|
||||
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
@@ -317,7 +326,7 @@ final class process_summarise_text_test extends \advanced_testcase {
|
||||
$mock->append(new Response(
|
||||
401,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode(['error' => ['message' => 'Invalid Authentication']]),
|
||||
json_encode(['error' => 'Invalid Authentication']),
|
||||
));
|
||||
|
||||
$processor = new process_summarise_text($this->provider, $this->action);
|
||||
|
||||
@@ -171,7 +171,7 @@ class auth_plugin_email extends auth_plugin_base {
|
||||
* @param string $confirmsecret
|
||||
*/
|
||||
function user_confirm($username, $confirmsecret) {
|
||||
global $DB, $SESSION;
|
||||
global $DB;
|
||||
$user = get_complete_user_data('username', $username);
|
||||
|
||||
if (!empty($user)) {
|
||||
@@ -179,17 +179,15 @@ class auth_plugin_email extends auth_plugin_base {
|
||||
return AUTH_CONFIRM_ERROR;
|
||||
|
||||
} else if ($user->secret === $confirmsecret && $user->confirmed) {
|
||||
// Clean up stale wantsurl preference if user clicks confirmation link again.
|
||||
unset_user_preference('auth_email_wantsurl', $user);
|
||||
return AUTH_CONFIRM_ALREADY;
|
||||
|
||||
} else if ($user->secret === $confirmsecret) { // They have provided the secret key to get in
|
||||
$DB->set_field("user", "confirmed", 1, array("id"=>$user->id));
|
||||
|
||||
if ($wantsurl = get_user_preferences('auth_email_wantsurl', false, $user)) {
|
||||
// Ensure user gets returned to page they were trying to access before signing up.
|
||||
$SESSION->wantsurl = $wantsurl;
|
||||
unset_user_preference('auth_email_wantsurl', $user);
|
||||
}
|
||||
|
||||
// Clean up the wantsurl preference regardless of how confirmation was triggered
|
||||
// (e.g. /login/confirm.php, admin single confirm, bulk confirm, web service).
|
||||
unset_user_preference('auth_email_wantsurl', $user);
|
||||
return AUTH_CONFIRM_OK;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace auth_email;
|
||||
|
||||
/**
|
||||
* Tests for email authentication plugin.
|
||||
*
|
||||
* @package auth_email
|
||||
* @copyright 2026 Moodle Pty Ltd
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \auth_plugin_email
|
||||
*/
|
||||
final class auth_test extends \advanced_testcase {
|
||||
/**
|
||||
* Test that user_confirm() cleans up the auth_email_wantsurl preference
|
||||
* when confirming a user for the first time (AUTH_CONFIRM_OK).
|
||||
*/
|
||||
public function test_user_confirm_cleans_up_wantsurl_preference(): void {
|
||||
global $DB;
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Create an unconfirmed user with the email auth method.
|
||||
$user = $this->getDataGenerator()->create_user([
|
||||
'auth' => 'email',
|
||||
'confirmed' => 0,
|
||||
]);
|
||||
$secret = random_string(15);
|
||||
$DB->set_field('user', 'secret', $secret, ['id' => $user->id]);
|
||||
|
||||
// Simulate the wantsurl preference saved at signup time.
|
||||
set_user_preference('auth_email_wantsurl', 'https://example.com/course/view.php?id=42', $user);
|
||||
$this->assertTrue(
|
||||
$DB->record_exists('user_preferences', ['userid' => $user->id, 'name' => 'auth_email_wantsurl']),
|
||||
'Preference should exist in DB before confirmation.'
|
||||
);
|
||||
|
||||
$auth = get_auth_plugin('email');
|
||||
$result = $auth->user_confirm($user->username, $secret);
|
||||
|
||||
$this->assertEquals(AUTH_CONFIRM_OK, $result);
|
||||
$this->assertFalse(
|
||||
$DB->record_exists('user_preferences', ['userid' => $user->id, 'name' => 'auth_email_wantsurl']),
|
||||
'auth_email_wantsurl preference must be removed from DB after successful confirmation.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that user_confirm() cleans up the auth_email_wantsurl preference
|
||||
* even when the user is already confirmed (AUTH_CONFIRM_ALREADY).
|
||||
*
|
||||
* This covers the edge case where a user clicks the confirmation link
|
||||
* a second time — the stale preference should still be cleaned up.
|
||||
*/
|
||||
public function test_user_confirm_already_confirmed_cleans_up_wantsurl_preference(): void {
|
||||
global $DB;
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Create an already-confirmed user with the email auth method.
|
||||
$user = $this->getDataGenerator()->create_user([
|
||||
'auth' => 'email',
|
||||
'confirmed' => 1,
|
||||
]);
|
||||
$secret = random_string(15);
|
||||
$DB->set_field('user', 'secret', $secret, ['id' => $user->id]);
|
||||
|
||||
// Simulate a stale wantsurl preference left over from signup.
|
||||
set_user_preference('auth_email_wantsurl', 'https://example.com/course/view.php?id=42', $user);
|
||||
$this->assertTrue(
|
||||
$DB->record_exists('user_preferences', ['userid' => $user->id, 'name' => 'auth_email_wantsurl']),
|
||||
'Preference should exist in DB before re-confirmation.'
|
||||
);
|
||||
|
||||
$auth = get_auth_plugin('email');
|
||||
$result = $auth->user_confirm($user->username, $secret);
|
||||
|
||||
$this->assertEquals(AUTH_CONFIRM_ALREADY, $result);
|
||||
$this->assertFalse(
|
||||
$DB->record_exists('user_preferences', ['userid' => $user->id, 'name' => 'auth_email_wantsurl']),
|
||||
'auth_email_wantsurl preference must be removed from DB even when user is already confirmed.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -197,9 +197,7 @@ class api {
|
||||
|
||||
$data->link = $confirmationurl->out(false);
|
||||
$message = get_string('confirmlinkedloginemail', 'auth_oauth2', $data);
|
||||
|
||||
$data->link = $confirmationurl->out();
|
||||
$messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false, true);
|
||||
$messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false);
|
||||
|
||||
$user->mailformat = 1; // Always send HTML version as well.
|
||||
|
||||
@@ -339,9 +337,7 @@ class api {
|
||||
|
||||
$data->link = $confirmationurl->out(false);
|
||||
$message = get_string('confirmaccountemail', 'auth_oauth2', $data);
|
||||
|
||||
$data->link = $confirmationurl->out();
|
||||
$messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false, true);
|
||||
$messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false);
|
||||
|
||||
$user->mailformat = 1; // Always send HTML version as well.
|
||||
|
||||
|
||||
@@ -467,18 +467,31 @@ class auth extends \auth_plugin_base {
|
||||
$mappeduser = get_complete_user_data('id', $linkedlogin->get('userid'));
|
||||
|
||||
if ($mappeduser && $mappeduser->suspended) {
|
||||
$failurereason = AUTH_LOGIN_SUSPENDED;
|
||||
$event = \core\event\user_login_failed::create([
|
||||
'userid' => $mappeduser->id,
|
||||
'other' => [
|
||||
'username' => $userinfo['username'],
|
||||
'reason' => $failurereason
|
||||
]
|
||||
]);
|
||||
$event->trigger();
|
||||
$SESSION->loginerrormsg = get_string('invalidlogin');
|
||||
$client->log_out();
|
||||
redirect(new moodle_url('/login/index.php'));
|
||||
// Check if there's another user with the same email that is not suspended.
|
||||
$moodleuser = \core_user::get_user_by_email($userinfo['email'], '*', null, IGNORE_MULTIPLE);
|
||||
if ($moodleuser->id == $mappeduser->id) {
|
||||
$failurereason = AUTH_LOGIN_SUSPENDED;
|
||||
$event = \core\event\user_login_failed::create([
|
||||
'userid' => $mappeduser->id,
|
||||
'other' => [
|
||||
'username' => $userinfo['username'],
|
||||
'reason' => $failurereason,
|
||||
],
|
||||
]);
|
||||
$event->trigger();
|
||||
$SESSION->loginerrormsg = get_string('invalidlogin');
|
||||
$client->log_out();
|
||||
redirect(new moodle_url('/login/index.php'));
|
||||
} else if ($moodleuser && !$moodleuser->suspended) {
|
||||
// Update the OAuth2 linked login to point to the active user account.
|
||||
$linkedlogin->set('userid', $moodleuser->id);
|
||||
$linkedlogin->set('timemodified', time());
|
||||
$linkedlogin->update();
|
||||
|
||||
// Update user fields and continue with login.
|
||||
$userinfo = $this->update_user($userinfo, $moodleuser);
|
||||
$userwasmapped = true;
|
||||
}
|
||||
} else if ($mappeduser && ($mappeduser->confirmed || !$issuer->get('requireconfirmation'))) {
|
||||
// Update user fields.
|
||||
$userinfo = $this->update_user($userinfo, $mappeduser);
|
||||
@@ -508,7 +521,6 @@ class auth extends \auth_plugin_base {
|
||||
redirect(new moodle_url('/login/index.php'));
|
||||
}
|
||||
|
||||
|
||||
if (!$issuer->is_valid_login_domain($oauthemail)) {
|
||||
// Trigger login failed event.
|
||||
$failurereason = AUTH_LOGIN_UNAUTHORISED;
|
||||
@@ -524,8 +536,24 @@ class auth extends \auth_plugin_base {
|
||||
|
||||
if (!$userwasmapped) {
|
||||
// No defined mapping - we need to see if there is an existing account with the same email.
|
||||
$moodleuser = \core_user::get_user_by_email($userinfo['email'], '*', null, IGNORE_MULTIPLE);
|
||||
|
||||
// Ensure we don't link a login for a suspended user.
|
||||
if (!empty($moodleuser) && $moodleuser->suspended) {
|
||||
$failurereason = AUTH_LOGIN_SUSPENDED;
|
||||
$event = \core\event\user_login_failed::create([
|
||||
'userid' => $moodleuser->id,
|
||||
'other' => [
|
||||
'username' => $userinfo['email'],
|
||||
'reason' => $failurereason,
|
||||
],
|
||||
]);
|
||||
$event->trigger();
|
||||
$SESSION->loginerrormsg = get_string('invalidlogin');
|
||||
$client->log_out();
|
||||
redirect(new moodle_url('/login/index.php'));
|
||||
}
|
||||
|
||||
$moodleuser = \core_user::get_user_by_email($userinfo['email']);
|
||||
if (!empty($moodleuser)) {
|
||||
if ($issuer->get('requireconfirmation')) {
|
||||
$PAGE->set_url('/auth/oauth2/confirm-link-login.php');
|
||||
|
||||
@@ -27,19 +27,14 @@ $string['auth_oauth2description'] = 'OAuth 2 standards based authentication';
|
||||
$string['auth_oauth2settings'] = 'OAuth 2 authentication settings.';
|
||||
$string['confirmaccountemail'] = 'Hi {$a->firstname},
|
||||
|
||||
A new account has been requested at \'{$a->sitename}\'
|
||||
using your email address.
|
||||
A new account has been requested at \'{$a->sitename}\' using your email address.
|
||||
|
||||
To confirm your new account, please go to this web address:
|
||||
To confirm your new account, please click the link below:
|
||||
|
||||
{$a->link}
|
||||
<a href="{$a->link}">Confirm your account</a>
|
||||
|
||||
In most mail programs, this should appear as a blue link
|
||||
which you can just click on. If that doesn\'t work,
|
||||
then cut and paste the address into the address
|
||||
line at the top of your web browser window.
|
||||
|
||||
If you need help, please contact the site administrator,
|
||||
If you need help, please contact the site administrator.
|
||||
{$a->admin}
|
||||
|
||||
If you did not do this, someone else could be trying to compromise your account.
|
||||
@@ -53,16 +48,12 @@ A request has been made to link the {$a->issuername} login
|
||||
{$a->linkedemail} to your account at \'{$a->sitename}\'
|
||||
using your email address.
|
||||
|
||||
To confirm this request and link these logins, please go to this web address:
|
||||
To confirm this request and link these logins, please click the link below:
|
||||
|
||||
{$a->link}
|
||||
<a href="{$a->link}">Link your accounts</a>
|
||||
|
||||
In most mail programs, this should appear as a blue link
|
||||
which you can just click on. If that doesn\'t work,
|
||||
then cut and paste the address into the address
|
||||
line at the top of your web browser window.
|
||||
|
||||
If you need help, please contact the site administrator,
|
||||
If you need help, please contact the site administrator.
|
||||
{$a->admin}
|
||||
|
||||
If you did not do this, someone else could be trying to compromise your account.
|
||||
|
||||
@@ -34,11 +34,13 @@ Feature: Test if the login form provides the correct feedback
|
||||
Scenario: Set logo for loginpage
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Appearance > Logos" in site administration
|
||||
And I upload "course/tests/fixtures/image.jpg" file to "Logo" filemanager
|
||||
And I upload "lib/tests/fixtures/1.jpg" file to "Logo" filemanager
|
||||
And I upload "lib/tests/fixtures/2.jpg" file to "Favicon" filemanager
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
And I follow "Log in"
|
||||
Then "//img[@id='logoimage']" "xpath_element" should exist
|
||||
Then "//img[@id='logoimage' and contains(@src, '/1.jpg')]" "xpath_element" should exist
|
||||
And "//link[@rel='shortcut icon' and contains(@href, '/2.jpg')]" "xpath_element" should exist
|
||||
|
||||
Scenario: Add a custom welcome message
|
||||
Given the following config values are set as admin:
|
||||
|
||||
+8
-8
@@ -32,7 +32,7 @@ Feature: Confirm that availability_completion works with previous activity setti
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
|
||||
And I click on "Displayed if student doesn't meet this condition • Click to hide" "link"
|
||||
And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link"
|
||||
And I set the field "Activity or resource" to "Previous activity with completion"
|
||||
And I press "Save and return to course"
|
||||
Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
|
||||
@@ -58,7 +58,7 @@ Feature: Confirm that availability_completion works with previous activity setti
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
|
||||
And I click on "Displayed if student doesn't meet this condition • Click to hide" "link"
|
||||
And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link"
|
||||
And I set the field "Activity or resource" to "Previous activity with completion"
|
||||
And I press "Save and return to course"
|
||||
Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
|
||||
@@ -83,7 +83,7 @@ Feature: Confirm that availability_completion works with previous activity setti
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
|
||||
And I click on "Displayed if student doesn't meet this condition • Click to hide" "link"
|
||||
And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link"
|
||||
And I set the field "Activity or resource" to "Previous activity with completion"
|
||||
And I press "Save and return to course"
|
||||
Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
|
||||
@@ -112,7 +112,7 @@ Feature: Confirm that availability_completion works with previous activity setti
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
|
||||
And I click on "Displayed if student doesn't meet this condition • Click to hide" "link"
|
||||
And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link"
|
||||
And I set the field "Activity or resource" to "Previous activity with completion"
|
||||
And I press "Save changes"
|
||||
Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
|
||||
@@ -138,7 +138,7 @@ Feature: Confirm that availability_completion works with previous activity setti
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
|
||||
And I click on "Displayed if student doesn't meet this condition • Click to hide" "link"
|
||||
And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link"
|
||||
Then the "Activity or resource" select box should not contain "Previous activity with completion"
|
||||
|
||||
# Set Page2 restriction to Previous Activity with completion and delete Page1.
|
||||
@@ -148,7 +148,7 @@ Feature: Confirm that availability_completion works with previous activity setti
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
|
||||
And I click on "Displayed if student doesn't meet this condition • Click to hide" "link"
|
||||
And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link"
|
||||
And I set the field "Activity or resource" to "Previous activity with completion"
|
||||
And I press "Save and return to course"
|
||||
Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
|
||||
@@ -170,7 +170,7 @@ Feature: Confirm that availability_completion works with previous activity setti
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
|
||||
And I click on "Displayed if student doesn't meet this condition • Click to hide" "link"
|
||||
And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link"
|
||||
And I set the field "Activity or resource" to "Previous activity with completion"
|
||||
And I press "Save changes"
|
||||
Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
|
||||
@@ -184,7 +184,7 @@ Feature: Confirm that availability_completion works with previous activity setti
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
|
||||
And I click on "Displayed if student doesn't meet this condition • Click to hide" "link"
|
||||
And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link"
|
||||
And I set the field "Activity or resource" to "Previous activity with completion"
|
||||
And I press "Save changes"
|
||||
And I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
|
||||
|
||||
@@ -184,7 +184,8 @@ class condition extends \core_availability\condition {
|
||||
*/
|
||||
private static function get_cached_grade_name($courseid, $gradeitemid) {
|
||||
global $DB, $CFG;
|
||||
require_once($CFG->libdir . '/gradelib.php');
|
||||
|
||||
require_once("{$CFG->dirroot}/grade/lib.php");
|
||||
|
||||
// Get all grade item names from cache, or using db query.
|
||||
$cache = \cache::make('availability_grade', 'items');
|
||||
|
||||
@@ -180,4 +180,4 @@ Feature: Display availability for activities and sections
|
||||
And I should not see "Date" in the "Restrict access" "fieldset"
|
||||
And I press "Add restriction..."
|
||||
And I click on "Grade" "button" in the "Add restriction..." "dialogue"
|
||||
And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student"
|
||||
And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Item name displayed"
|
||||
|
||||
@@ -89,13 +89,13 @@ Feature: edit_availability
|
||||
And I should see "Date" in the "Restrict access" "fieldset"
|
||||
And ".availability-item .availability-eye img" "css_element" should be visible
|
||||
And ".availability-item .availability-delete img" "css_element" should be visible
|
||||
And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student"
|
||||
And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Item name displayed"
|
||||
|
||||
# Toggle the eye icon.
|
||||
When I click on ".availability-item .availability-eye img" "css_element"
|
||||
Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Hidden entirely"
|
||||
When I click on ".availability-item .availability-eye img" "css_element"
|
||||
Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student"
|
||||
Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Item name displayed"
|
||||
|
||||
# Click the delete button.
|
||||
When I click on ".availability-item .availability-delete img" "css_element"
|
||||
|
||||
@@ -2649,6 +2649,7 @@ class backup_questions_structure_step extends backup_structure_step {
|
||||
'questioncategoryid',
|
||||
'idnumber',
|
||||
'ownerid',
|
||||
'nextversion',
|
||||
]);
|
||||
|
||||
$questionversions = new backup_nested_element('question_version');
|
||||
@@ -2725,7 +2726,7 @@ class backup_questions_structure_step extends backup_structure_step {
|
||||
WHERE bi.backupid = ?
|
||||
AND bi.itemname = 'question_categoryfinal'", [backup::VAR_BACKUPID]);
|
||||
|
||||
// Add all question bank entries from "complete" categories, plus annotated question bank entires
|
||||
// Add all question bank entries from "complete" categories, plus annotated question bank entires and their children
|
||||
// from "partial" categories.
|
||||
$questionbankentry->set_source_sql(
|
||||
"
|
||||
@@ -2737,8 +2738,11 @@ class backup_questions_structure_step extends backup_structure_step {
|
||||
UNION
|
||||
SELECT qbe.*
|
||||
FROM {question_bank_entries} qbe
|
||||
JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
|
||||
JOIN {question} q ON q.id = qv.questionid
|
||||
LEFT JOIN {question_versions} parentqv ON parentqv.questionid = q.parent
|
||||
JOIN {question_category_partial_temp} qcp ON qcp.itemid = qbe.questioncategoryid
|
||||
JOIN {backup_ids_temp} biq ON biq.itemid = qbe.id
|
||||
JOIN {backup_ids_temp} biq ON biq.itemid = qbe.id OR biq.itemid = parentqv.questionbankentryid
|
||||
WHERE qcp.itemid = ?
|
||||
AND qcp.backupid = ?
|
||||
AND biq.backupid = ?
|
||||
|
||||
@@ -78,6 +78,9 @@ class restore_root_task extends restore_task {
|
||||
// Unconditionally, load create all the needed outcomes
|
||||
$this->add_step(new restore_outcomes_structure_step('create_scales', 'outcomes.xml'));
|
||||
|
||||
// If we haven't preloaded information, load all the question banks to temp_ids_table.
|
||||
$this->add_step(new \core\backup\restore_load_questionbanks('load_questionbanks'));
|
||||
|
||||
// If we haven't preloaded information, load all the needed categories and questions (reduced) to temp_ids_table
|
||||
$this->add_step(new restore_load_categories_and_questions('load_categories_and_questions'));
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core_question\local\bank\question_version_status;
|
||||
use core_question\versions;
|
||||
|
||||
/**
|
||||
* delete old directories and conditionally create backup_temp_ids table
|
||||
*/
|
||||
@@ -5289,6 +5292,15 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
$this->set_mapping('question_bank_entry', $this->latestqbe->oldid, $this->latestqbe->newid);
|
||||
}
|
||||
|
||||
if (
|
||||
($data->qtype === 'random')
|
||||
&& ($this->latestversion->status == \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN)
|
||||
) {
|
||||
// Ensure that this newly created question is considered by
|
||||
// \qtype_random\task\remove_unused_questions.
|
||||
$this->latestversion->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_DRAFT;
|
||||
}
|
||||
|
||||
// Now store the question.
|
||||
$newitemid = $DB->insert_record('question', $data);
|
||||
$this->set_mapping('question', $oldid, $newitemid);
|
||||
@@ -5301,9 +5313,55 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
$oldqvid = $this->latestversion->id;
|
||||
$this->latestversion->questionbankentryid = $this->latestqbe->newid;
|
||||
$this->latestversion->questionid = $newitemid;
|
||||
// In case the backed up version was deleted and a new one created in its place, increase the version numbers of
|
||||
// conflicting versions to make room for this one.
|
||||
$transaction = $DB->start_delegated_transaction();
|
||||
if (
|
||||
$DB->record_exists(
|
||||
'question_versions',
|
||||
[
|
||||
'questionbankentryid' => $this->latestversion->questionbankentryid,
|
||||
'version' => $this->latestversion->version,
|
||||
],
|
||||
)
|
||||
) {
|
||||
// We'll update each higher version and any references one-at-a-time, starting with the highest, to avoid
|
||||
// creating a duplicate questionbankentryid-version combination in question_versions.
|
||||
$moveversions = $DB->get_records_select(
|
||||
'question_versions',
|
||||
'questionbankentryid = :questionbankentryid AND version >= :oldversion',
|
||||
[
|
||||
'questionbankentryid' => $this->latestversion->questionbankentryid,
|
||||
'oldversion' => $this->latestversion->version,
|
||||
],
|
||||
'version DESC',
|
||||
);
|
||||
foreach ($moveversions as $moveversion) {
|
||||
$DB->set_field(
|
||||
'question_versions',
|
||||
'version',
|
||||
$moveversion->version + 1,
|
||||
[
|
||||
'questionbankentryid' => $moveversion->questionbankentryid,
|
||||
'version' => $moveversion->version,
|
||||
]
|
||||
);
|
||||
$DB->set_field(
|
||||
'question_references',
|
||||
'version',
|
||||
$moveversion->version + 1,
|
||||
[
|
||||
'questionbankentryid' => $moveversion->questionbankentryid,
|
||||
'version' => $moveversion->version,
|
||||
]
|
||||
);
|
||||
}
|
||||
// Ensure the nextversion value has been initialised, and increment it to account for the additional version.
|
||||
versions::get_next_version($this->latestversion->questionbankentryid);
|
||||
}
|
||||
$newqvid = $DB->insert_record('question_versions', $this->latestversion);
|
||||
$this->set_mapping('question_versions', $oldqvid, $newqvid);
|
||||
|
||||
$transaction->allow_commit();
|
||||
} else {
|
||||
// By performing this set_mapping() we make get_old/new_parentid() to work for all the
|
||||
// children elements of the 'question' one (so qtype plugins will know the question they belong to).
|
||||
@@ -5311,6 +5369,18 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
|
||||
// Also create the question_bank_entry and version mappings, if required.
|
||||
$newquestionversion = $DB->get_record('question_versions', ['questionid' => $questionmapping->newitemid]);
|
||||
// Restore the version to ready state if it has been hidden.
|
||||
if (
|
||||
$newquestionversion->status == question_version_status::QUESTION_STATUS_HIDDEN
|
||||
&& $this->latestversion->status == question_version_status::QUESTION_STATUS_READY
|
||||
) {
|
||||
$DB->set_field(
|
||||
'question_versions',
|
||||
'status',
|
||||
question_version_status::QUESTION_STATUS_READY,
|
||||
['questionid' => $questionmapping->newitemid],
|
||||
);
|
||||
}
|
||||
$this->set_mapping('question_versions', $this->latestversion->id, $newquestionversion->id);
|
||||
if (empty($this->latestqbe->newid)) {
|
||||
$this->latestqbe->oldid = $this->latestqbe->id;
|
||||
@@ -5402,12 +5472,18 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
}
|
||||
$tagcontextid = $this->cachedcategory->contextid;
|
||||
// Add the tag to the question.
|
||||
core_tag_tag::add_item_tag('core_question',
|
||||
$taginstanceid = core_tag_tag::add_item_tag(
|
||||
'core_question',
|
||||
'question',
|
||||
$newquestion,
|
||||
context::instance_by_id($tagcontextid),
|
||||
$tagname
|
||||
$tagname,
|
||||
);
|
||||
$tagid = $DB->get_field('tag_instance', 'tagid', ['id' => $taginstanceid]);
|
||||
if ($tagid != $data->id) {
|
||||
// The tag didn't exist already, map the new ID.
|
||||
$this->set_mapping('tag', $data->id, $tagid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5485,7 +5561,11 @@ class restore_move_module_questions_categories extends restore_execution_step {
|
||||
// but if that context still exists on the site and the user has access then point question references
|
||||
// to the originals.
|
||||
$originalcontext = context::instance_by_id($contextid, IGNORE_MISSING);
|
||||
if ($originalcontext && has_capability('mod/qbank:view', $originalcontext)) {
|
||||
if (
|
||||
$this->task->is_samesite()
|
||||
&& $originalcontext
|
||||
&& has_capability('mod/qbank:view', $originalcontext)
|
||||
) {
|
||||
$originalquestions = get_questions_category(question_get_top_category($contextid), false);
|
||||
$targetcoursecontext = context_course::instance($this->get_courseid());
|
||||
foreach ($originalquestions as $originalquestion) {
|
||||
@@ -5608,13 +5688,19 @@ class restore_move_module_questions_categories extends restore_execution_step {
|
||||
// We need to check all the question_set_references belonging to this context_module.
|
||||
$references = $DB->get_records('question_set_references', ['usingcontextid' => $newcontext->newitemid]);
|
||||
foreach ($references as $reference) {
|
||||
$filtercondition = json_decode($reference->filtercondition);
|
||||
if (!empty($filtercondition->questioncategoryid) &&
|
||||
in_array($filtercondition->questioncategoryid, $categoryids)) {
|
||||
// This is one of ours, update the questionscontextid.
|
||||
$DB->set_field('question_set_references',
|
||||
'questionscontextid', $newcontext->newitemid,
|
||||
['id' => $reference->id]);
|
||||
$filtercondition = json_decode($reference->filtercondition, true);
|
||||
if (!array_key_exists('filter', $filtercondition)) {
|
||||
$filtercondition = \core_question\question_reference_manager::convert_legacy_set_reference_filter_condition(
|
||||
$filtercondition,
|
||||
);
|
||||
}
|
||||
$questioncategoryid = $filtercondition['filter']['category']['values'][0];
|
||||
if (in_array($questioncategoryid, $categoryids)) {
|
||||
// This is one of ours, update the questionscontextid and filtercondition fields.
|
||||
$reference->questionscontextid = $newcontext->newitemid;
|
||||
$filtercondition['cat'] = "{$questioncategoryid},{$newcontext->newitemid}";
|
||||
$reference->filtercondition = json_encode($filtercondition);
|
||||
$DB->update_record('question_set_references', $reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5629,20 +5715,20 @@ class restore_move_module_questions_categories extends restore_execution_step {
|
||||
);
|
||||
}
|
||||
}
|
||||
// Remove any remaining course-level question categories from the restored course.
|
||||
// Remove any remaining course-level question categories and their questions from the restored course.
|
||||
$coursecatsql = "
|
||||
SELECT qc.id AS categoryid
|
||||
SELECT qc.id AS id, qc.contextid AS contextid
|
||||
FROM {question_categories} qc
|
||||
JOIN {context} c ON c.id = qc.contextid
|
||||
WHERE c.contextlevel = :courselevel AND c.instanceid = :courseid
|
||||
";
|
||||
$DB->delete_records_subquery(
|
||||
'question_categories',
|
||||
'id',
|
||||
'categoryid',
|
||||
$categories = $DB->get_records_sql(
|
||||
$coursecatsql,
|
||||
['courselevel' => context_course::LEVEL, 'courseid' => $this->task->get_courseid()]
|
||||
['courselevel' => context_course::LEVEL, 'courseid' => $this->task->get_courseid()],
|
||||
);
|
||||
foreach ($categories as $category) {
|
||||
question_category_delete_safe($category);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6421,50 +6507,47 @@ trait restore_question_set_reference_data_trait {
|
||||
public function process_question_set_reference($data) {
|
||||
global $DB;
|
||||
$data = (object) $data;
|
||||
$owncontext = $data->usingcontextid == $data->questionscontextid;
|
||||
$data->usingcontextid = $this->get_mappingid('context', $data->usingcontextid);
|
||||
$data->itemid = $this->get_new_parentid('quiz_question_instance');
|
||||
|
||||
$originalbankinbackup = (bool) restore_dbops::get_backup_ids_record(
|
||||
$this->get_restoreid(),
|
||||
'questionbank',
|
||||
$data->questionscontextid,
|
||||
);
|
||||
|
||||
if ($context = $this->get_mappingid('context', $data->questionscontextid)) {
|
||||
$data->questionscontextid = $context;
|
||||
} else {
|
||||
$this->log(
|
||||
"question_set_reference with old id {$data->id} referenced question context "
|
||||
. "{$data->questionscontextid} which was not included in the backup. Therefore, this has been "
|
||||
. "restored with the old questionscontextid.",
|
||||
backup::LOG_WARNING,
|
||||
);
|
||||
}
|
||||
|
||||
$filtercondition = json_decode($data->filtercondition, true);
|
||||
|
||||
if (!isset($filtercondition['filter'])) {
|
||||
// Pre-4.3, convert the old filtercondition format to the new format.
|
||||
// Don't map tags to new IDs, the plugin will do that below.
|
||||
$filtercondition = \core_question\question_reference_manager::convert_legacy_set_reference_filter_condition(
|
||||
$filtercondition);
|
||||
$filtercondition,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
// Map category id used for category filter condition and corresponding context id.
|
||||
$oldcategoryid = $filtercondition['filter']['category']['values'][0];
|
||||
// Decide if we're going to refer back to the original category, or to the new category.
|
||||
// Are we restoring to a different site?
|
||||
// Has the original context or category been deleted?
|
||||
// Did the old category belong to the same context as the original set reference?
|
||||
// Are we allowed to use its questions?
|
||||
$questionscontext = context::instance_by_id($data->questionscontextid, IGNORE_MISSING);
|
||||
if (
|
||||
!$this->get_task()->is_samesite()
|
||||
|| !$questionscontext
|
||||
|| !$DB->record_exists('question_categories', ['id' => $oldcategoryid])
|
||||
|| $owncontext
|
||||
|| !has_capability('moodle/question:useall', $questionscontext)
|
||||
) {
|
||||
$newcategoryid = $this->get_mappingid('question_category', $oldcategoryid);
|
||||
$filtercondition['filter']['category']['values'][0] = $newcategoryid;
|
||||
$qbankfeatureclasses = \core\component::get_plugin_list_with_class('qbank', 'plugin_feature');
|
||||
|
||||
if ($context = $this->get_mappingid('context', $data->questionscontextid)) {
|
||||
$data->questionscontextid = $context;
|
||||
} else {
|
||||
$this->log('question_set_reference with old id ' . $data->id .
|
||||
' referenced question context ' . $data->questionscontextid .
|
||||
' which was not included in the backup. Therefore, this has been ' .
|
||||
' restored with the old questionscontextid.', backup::LOG_WARNING);
|
||||
foreach ($qbankfeatureclasses as $qbankfeatureclass) {
|
||||
$qbankfeature = new $qbankfeatureclass();
|
||||
$filters = $qbankfeature->get_question_filters();
|
||||
foreach ($filters as $filter) {
|
||||
$filtercondition = $filter->restore_filtercondition($filtercondition, $data, $this, $originalbankinbackup);
|
||||
}
|
||||
}
|
||||
|
||||
$filtercondition['cat'] = implode(',', [
|
||||
$filtercondition['filter']['category']['values'][0],
|
||||
$data->questionscontextid,
|
||||
]);
|
||||
|
||||
$data->filtercondition = json_encode($filtercondition);
|
||||
|
||||
$DB->insert_record('question_set_references', $data);
|
||||
|
||||
@@ -107,17 +107,12 @@ final class async_backup_test extends \advanced_testcase {
|
||||
$asynctask = new \core\task\asynchronous_backup_task();
|
||||
$asynctask->set_custom_data(['backupid' => $backupid]);
|
||||
$asynctask->set_userid($USER->id);
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// We are expecting trace output during this test.
|
||||
$this->expectOutputRegex("/$backupid/");
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$this->assertInstanceOf('\\core\\task\\asynchronous_backup_task', $task);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
$asynctask->execute();
|
||||
|
||||
$postbackuprec = $DB->get_record('backup_controllers', ['backupid' => $backupid]);
|
||||
|
||||
@@ -151,13 +146,9 @@ final class async_backup_test extends \advanced_testcase {
|
||||
// Create the adhoc task.
|
||||
$asynctask = new \core\task\asynchronous_backup_task();
|
||||
$asynctask->set_custom_data(['backupid' => $backupid]);
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
$asynctask->execute();
|
||||
|
||||
$postbackuprec = $DB->get_record('backup_controllers', ['backupid' => $backupid]);
|
||||
|
||||
@@ -241,19 +232,11 @@ final class async_backup_test extends \advanced_testcase {
|
||||
// Now queue an adhoc task and check it handles and completes gracefully.
|
||||
$asynctask = new \core\task\asynchronous_backup_task();
|
||||
$asynctask->set_custom_data(array('backupid' => $backupid));
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// We are expecting a specific message output during this test.
|
||||
$this->expectOutputRegex('/invalid controller/');
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$this->assertInstanceOf('\\core\\task\\asynchronous_backup_task', $task);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
|
||||
// Check the task record is removed.
|
||||
$this->assertEquals(0, $DB->count_records('task_adhoc'));
|
||||
$asynctask->execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,20 +223,12 @@ final class async_restore_test extends \advanced_testcase {
|
||||
// Create the adhoc task.
|
||||
$asynctask = new \core\task\asynchronous_restore_task();
|
||||
$asynctask->set_custom_data(['backupid' => $restoreid]);
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// We are expecting a specific message output during this test.
|
||||
$this->expectOutputRegex('/invalid controller/');
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
|
||||
// Check the task record is removed.
|
||||
$this->assertEquals(0, $DB->count_records('task_adhoc'));
|
||||
$asynctask->execute();
|
||||
|
||||
// Now delete the record and confirm an entirely missing controller is handled.
|
||||
$DB->delete_records('backup_controllers');
|
||||
@@ -244,19 +236,11 @@ final class async_restore_test extends \advanced_testcase {
|
||||
// Create the adhoc task.
|
||||
$asynctask = new \core\task\asynchronous_restore_task();
|
||||
$asynctask->set_custom_data(['backupid' => $restoreid]);
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// We are expecting a specific message output during this test.
|
||||
$this->expectOutputRegex('/Unable to find restore controller/');
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
|
||||
// Check the task record is removed.
|
||||
$this->assertEquals(0, $DB->count_records('task_adhoc'));
|
||||
$asynctask->execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,14 @@ use core_backup\hook\fixtures\copy_course_hook_callbacks;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class copy_course_hook_test extends advanced_testcase {
|
||||
/**
|
||||
* Load required test libraries
|
||||
*/
|
||||
public static function setUpBeforeClass(): void {
|
||||
global $CFG;
|
||||
require_once("{$CFG->dirroot}/backup/util/includes/backup_includes.php");
|
||||
parent::setUpBeforeClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the hook.
|
||||
|
||||
@@ -464,6 +464,39 @@ abstract class restore_dbops {
|
||||
$xmlparser->process();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store ids associated with any activity in the backup that supports FEATURE_PUBLISHES_QUESTIONS.
|
||||
*
|
||||
* @param string $restoreid The restore ID.
|
||||
* @param string $activitiespath The path to the `activities` folder in the backup being restored.
|
||||
*/
|
||||
public static function load_questionbanks_to_tempids(string $restoreid, string $activitiespath): void {
|
||||
if (!is_dir($activitiespath)) {
|
||||
return;
|
||||
}
|
||||
// Get modules that publish questions.
|
||||
$qmodules = array_filter(
|
||||
array_keys(core\component::get_all_plugins_list('mod')),
|
||||
fn($module) => plugin_supports('mod', $module, FEATURE_PUBLISHES_QUESTIONS),
|
||||
);
|
||||
foreach (scandir($activitiespath) as $activitydir) {
|
||||
[$modname] = explode('_', $activitydir);
|
||||
if (!in_array($modname, $qmodules)) {
|
||||
continue;
|
||||
}
|
||||
$activityfile = "{$activitiespath}/{$activitydir}/{$modname}.xml";
|
||||
if (!file_exists($activityfile)) { // Shouldn't happen ever, but...
|
||||
throw new backup_helper_exception('missing_moodle_backup_xml_file', $activityfile);
|
||||
}
|
||||
// Parse each activity's file, storing the relevant data in the database.
|
||||
$xmlparser = new progressive_parser();
|
||||
$xmlparser->set_file($activityfile);
|
||||
$xmlprocessor = new restore_questionbanks_parser_processor($restoreid);
|
||||
$xmlparser->set_processor($xmlprocessor);
|
||||
$xmlparser->process();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all the included categories and questions, deciding the action to perform
|
||||
* for each one (mapping / creation) and returning one array of problems in case
|
||||
@@ -609,9 +642,18 @@ abstract class restore_dbops {
|
||||
$topcats = 0;
|
||||
// get categories in context (bank)
|
||||
$categories = self::restore_get_question_categories($restoreid, $contextid, $contextlevel);
|
||||
|
||||
// cache permissions if $targetcontext is found
|
||||
if ($targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel)) {
|
||||
if (
|
||||
$contextlevel == \core\context\module::LEVEL
|
||||
&& self::get_backup_ids_record($restoreid, 'questionbank', $contextid)
|
||||
) {
|
||||
// Don't look for an existing module context, we have the original context in the backup,
|
||||
// so we'll put the categories in the course context for now and move them once the activity is restored.
|
||||
$targetcontext = core\context\course::instance($courseid);
|
||||
} else {
|
||||
$targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel);
|
||||
}
|
||||
if ($targetcontext) {
|
||||
// Cache permissions if $targetcontext is found.
|
||||
$canmanagecategory = has_capability('moodle/question:managecategory', $targetcontext, $userid);
|
||||
$canadd = has_capability('moodle/question:add', $targetcontext, $userid);
|
||||
}
|
||||
|
||||
@@ -411,7 +411,7 @@ abstract class backup_helper {
|
||||
|
||||
}
|
||||
|
||||
$selectmenu = new \core\output\select_menu('coursereusetype', $menuarray, $activeurl);
|
||||
$selectmenu = new \core\output\select_menu('coursereusetype', $menuarray, $activeurl, true);
|
||||
$selectmenu->set_label(get_string('coursereusenavigationmenu'), ['class' => 'visually-hidden']);
|
||||
$options = \html_writer::tag(
|
||||
'div',
|
||||
|
||||
@@ -173,6 +173,7 @@ abstract class restore_prechecks_helper {
|
||||
$progress->progress($majorstep++);
|
||||
|
||||
// Check we are able to restore and the categories and questions
|
||||
restore_dbops::load_questionbanks_to_tempids($restoreid, $controller->get_plan()->get_basepath() . '/activities');
|
||||
$file = $controller->get_plan()->get_basepath() . '/questions.xml';
|
||||
restore_dbops::load_categories_and_questions_to_tempids($restoreid, $file);
|
||||
if ($problems = restore_dbops::precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite)) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
defined('MOODLE_INTERNAL' || die());
|
||||
|
||||
require_once($CFG->dirroot . '/backup/util/xml/parser/processors/grouped_parser_processor.class.php');
|
||||
|
||||
/**
|
||||
* Parse and store activity data for activities that publish questions.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2025 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class restore_questionbanks_parser_processor extends grouped_parser_processor {
|
||||
/**
|
||||
* Store the restore ID and register paths.
|
||||
*
|
||||
* @param string $restoreid ID of the backup being restored.
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var string ID of the backup being restored */
|
||||
protected string $restoreid,
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->add_path('/activity');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function dispatch_chunk($data): void {
|
||||
// Recieved one chunk, store the context ID as that's what we will match question categories against.
|
||||
$itemid = $data['tags']['contextid'];
|
||||
restore_dbops::set_backup_ids_record($this->restoreid, 'questionbank', $itemid);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function notify_path_start($path) {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function notify_path_end($path) {
|
||||
// Nothing to do.
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ require_once($CFG->dirroot . '/backup/util/helper/backup_anonymizer_helper.class
|
||||
require_once($CFG->dirroot . '/backup/util/helper/backup_file_manager.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/copy_helper.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/restore_prechecks_helper.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/restore_questionbanks_parser_processor.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/restore_moodlexml_parser_processor.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/restore_inforef_parser_processor.class.php');
|
||||
require_once($CFG->dirroot . '/backup/util/helper/restore_users_parser_processor.class.php');
|
||||
|
||||
@@ -217,7 +217,7 @@ if ($award && data_submitted() && has_capability('moodle/badges:awardbadge', $co
|
||||
$users = $existingselector->get_selected_users();
|
||||
|
||||
foreach ($users as $user) {
|
||||
if (!process_manual_revoke($user->id, $USER->id, $issuerrole->roleid, $badgeid)) {
|
||||
if (!process_manual_revoke($user->id, 0, $issuerrole->roleid, $badgeid)) {
|
||||
echo $OUTPUT->error_text(get_string('error:cannotrevokebadge', 'badges'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ class award_criteria_course extends award_criteria {
|
||||
$params['courseid'] = $this->courseid;
|
||||
|
||||
// Add by date parameter.
|
||||
if (isset($param['bydate'])) {
|
||||
if (isset($coursecriteria['bydate'])) {
|
||||
$where .= ' AND cc.timecompleted <= :completebydate';
|
||||
$params['completebydate'] = $coursecriteria['bydate'];
|
||||
}
|
||||
|
||||
@@ -211,7 +211,10 @@ class award_criteria_courseset extends award_criteria {
|
||||
$check_grade = ($grade->grade >= $param['grade']);
|
||||
}
|
||||
|
||||
if (!$filtered && isset($param['bydate'])) {
|
||||
// When the user list is not filtered, or the aggregation method is ANY,
|
||||
// ensure the course completion date is before the bydate (if set).
|
||||
$comparedates = !$filtered || $this->method == BADGE_CRITERIA_AGGREGATION_ANY;
|
||||
if ($comparedates && isset($param['bydate'])) {
|
||||
$cparams = array(
|
||||
'userid' => $userid,
|
||||
'course' => $course->id,
|
||||
@@ -254,9 +257,16 @@ class award_criteria_courseset extends award_criteria {
|
||||
$params = array();
|
||||
|
||||
if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) {
|
||||
$coursedata = [];
|
||||
foreach ($this->params as $param) {
|
||||
$coursedata[] = " cc.course = :completedcourse{$param['course']} ";
|
||||
$conditionsql = " cc.course = :completedcourse{$param['course']} ";
|
||||
$params["completedcourse{$param['course']}"] = $param['course'];
|
||||
|
||||
if (isset($param['bydate'])) {
|
||||
$conditionsql .= " AND cc.timecompleted <= :completedbydate{$param['course']} ";
|
||||
$params["completedbydate{$param['course']}"] = $param['bydate'];
|
||||
}
|
||||
$coursedata[] = "({$conditionsql})";
|
||||
}
|
||||
if (!empty($coursedata)) {
|
||||
$extraon = implode(' OR ', $coursedata);
|
||||
@@ -272,6 +282,11 @@ class award_criteria_courseset extends award_criteria {
|
||||
cc{$param['course']}.timecompleted > 0 ";
|
||||
$where .= " AND cc{$param['course']}.course IS NOT NULL ";
|
||||
$params["completedcourse{$param['course']}"] = $param['course'];
|
||||
|
||||
if (isset($param['bydate'])) {
|
||||
$where .= " AND cc{$param['course']}.timecompleted <= :completedbydate{$param['course']} ";
|
||||
$params["completedbydate{$param['course']}"] = $param['bydate'];
|
||||
}
|
||||
}
|
||||
return array($join, $where, $params);
|
||||
}
|
||||
|
||||
@@ -290,27 +290,27 @@ function process_manual_award($recipientid, $issuerid, $issuerrole, $badgeid) {
|
||||
/**
|
||||
* Manually revoke awarded badges.
|
||||
*
|
||||
* @param int $recipientid
|
||||
* @param int $issuerid
|
||||
* @param int $issuerrole
|
||||
* @param int $badgeid
|
||||
* @param int $recipientid User ID of the recipient
|
||||
* @param int $issuerid User ID of the issuer (if 0, issuer will be ignored)
|
||||
* @param int $issuerrole Role of the issuer
|
||||
* @param int $badgeid ID of the badge
|
||||
* @return bool
|
||||
*/
|
||||
function process_manual_revoke($recipientid, $issuerid, $issuerrole, $badgeid) {
|
||||
global $DB;
|
||||
$params = array(
|
||||
'badgeid' => $badgeid,
|
||||
'issuerid' => $issuerid,
|
||||
'issuerrole' => $issuerrole,
|
||||
'recipientid' => $recipientid
|
||||
);
|
||||
$params = [
|
||||
'badgeid' => $badgeid,
|
||||
'issuerrole' => $issuerrole,
|
||||
'recipientid' => $recipientid,
|
||||
];
|
||||
if (!empty($issuerid)) {
|
||||
$params['issuerid'] = $issuerid;
|
||||
}
|
||||
if ($DB->record_exists('badge_manual_award', $params)) {
|
||||
if ($DB->delete_records('badge_manual_award', array('badgeid' => $badgeid,
|
||||
'issuerid' => $issuerid,
|
||||
'recipientid' => $recipientid))
|
||||
&& $DB->delete_records('badge_issued', array('badgeid' => $badgeid,
|
||||
'userid' => $recipientid))) {
|
||||
|
||||
if (
|
||||
$DB->delete_records('badge_manual_award', $params) &&
|
||||
$DB->delete_records('badge_issued', ['badgeid' => $badgeid, 'userid' => $recipientid])
|
||||
) {
|
||||
// Trigger event, badge revoked.
|
||||
$badge = new \badge($badgeid);
|
||||
$eventparams = array(
|
||||
|
||||
@@ -11,11 +11,13 @@ Feature: Award badges
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| teacher2 | Teacher | 2 | teacher2@example.com |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
| student2 | Student | 2 | student2@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| teacher2 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
And the following "activity" exists:
|
||||
@@ -371,14 +373,24 @@ Feature: Award badges
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
Then I should see "Recipients (2)"
|
||||
And I select "Recipients (2)" from the "jump" singleselect
|
||||
And I press "Award badge"
|
||||
And I set the field "existingrecipients[]" to "Student 2 (student2@example.com)"
|
||||
And I press "Revoke badge"
|
||||
And I set the field "existingrecipients[]" to "Student 1 (student1@example.com)"
|
||||
When I press "Revoke badge"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
Then I should see "Recipients (0)"
|
||||
Then I should see "Recipients (1)"
|
||||
And I log out
|
||||
# Now attempt to revoke a badge as another teacher.
|
||||
And I am on the "Course 1" "course" page logged in as "teacher2"
|
||||
And I navigate to "Badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I select "Recipients (1)" from the "jump" singleselect
|
||||
And I press "Award badge"
|
||||
And I set the field "existingrecipients[]" to "Student 1 (student1@example.com)"
|
||||
And I press "Revoke badge"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I should see "Recipients (0)"
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
@mod @mod_quiz @core_badges @core_completion @javascript
|
||||
|
||||
Feature: Award badges based on course completion
|
||||
In order to ensure a student has learned the required material
|
||||
As a teacher
|
||||
I need a badge to be awarded only when the student completes the course or a set of courses.
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
| student2 | Student | 2 | student2@example.com |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category | enablecompletion |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
| Course 2 | C2 | 0 | 1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
| teacher1 | C2 | editingteacher |
|
||||
| student1 | C2 | student |
|
||||
| student2 | C2 | student |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
| Course | C2 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | truefalse | First question | Answer the first question |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber | attempts | gradepass | completion | completionpassgrade | completionusegrade |
|
||||
| quiz | Test quiz name C1 | C1 | quiz1 | 2 | 5.00 | 2 | 1 | 1 |
|
||||
| quiz | Test quiz name C2 | C2 | quiz1 | 2 | 5.00 | 2 | 1 | 1 |
|
||||
And quiz "Test quiz name C1" contains the following questions:
|
||||
| question | page |
|
||||
| First question | 1 |
|
||||
And quiz "Test quiz name C2" contains the following questions:
|
||||
| question | page |
|
||||
| First question | 1 |
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| completionstatus | Course | C1 | course-view-* | side-pre |
|
||||
| completionstatus | Course | C2 | course-view-* | side-pre |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Course completion" in current page administration
|
||||
And I expand all fieldsets
|
||||
And I set the field "Test quiz name C1" to "1"
|
||||
And I press "Save changes"
|
||||
And I am on the "Course 2" course page logged in as teacher1
|
||||
And I navigate to "Course completion" in current page administration
|
||||
And I expand all fieldsets
|
||||
And I set the field "Test quiz name C2" to "1"
|
||||
And I press "Save changes"
|
||||
|
||||
Scenario Outline: Badge awarded depending on the course completion by-date
|
||||
Given I am on the "Course 1" course page
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Course Badge 1 |
|
||||
| status | 0 |
|
||||
| type | 2 |
|
||||
| course | C1 |
|
||||
| description | Course badge 1 description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
And I navigate to "Badges" in current page administration
|
||||
And I click on "Course Badge 1" "link"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Course completion"
|
||||
And I set the field "Enable" to "1"
|
||||
And I set the field "Day" to "1"
|
||||
And I set the field "Month" to "January"
|
||||
And I set the field "Year" to "<year>"
|
||||
And I press "Save"
|
||||
And I press "Enable access"
|
||||
And I click on "Enable" "button" in the "Confirm" "dialogue"
|
||||
And user "student1" has attempted "Test quiz name C1" with responses:
|
||||
| slot | response |
|
||||
| 1 | True |
|
||||
# Completion cron won't mark the whole course completed unless the
|
||||
# individual criteria was marked completed more than a second ago. So
|
||||
# run it twice, first to mark the criteria and second for the course.
|
||||
And I run the scheduled task "core\task\completion_regular_task"
|
||||
And I wait "1" seconds
|
||||
And I run the scheduled task "core\task\completion_regular_task"
|
||||
When I run the scheduled task "core\task\badges_cron_task"
|
||||
And I run all adhoc tasks
|
||||
And I navigate to "Badges" in current page administration
|
||||
And I click on "Course Badge 1" "link"
|
||||
Then I should see "Recipients (<count>)"
|
||||
And I select "Recipients (<count>)" from the "jump" singleselect
|
||||
And I <student1_visibility> see "Student 1"
|
||||
And I should not see "Student 2"
|
||||
|
||||
Examples:
|
||||
| year | count | student1_visibility |
|
||||
| ## +1 year ## %Y ## | 1 | should |
|
||||
| ## -1 year ## %Y ## | 0 | should not |
|
||||
|
||||
Scenario Outline: Badge awarded depending on the courseset completion by-date
|
||||
Given I am logged in as "admin"
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Site Badge |
|
||||
| status | 0 |
|
||||
| description | Site badge description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I press "Edit" action in the "Site Badge" report row
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Completing a set of courses"
|
||||
And I set the field "courses" to "Course 1, Course 2"
|
||||
And I press "Add courses"
|
||||
And I expand all fieldsets
|
||||
# Set a very high grade requirement to ensure that the course completion criteria won't be marked completed just by passing the quiz.
|
||||
And I set the field with xpath "(//input[contains(@name, 'grade')])[1]" to "200"
|
||||
And I set the field with xpath "(//input[contains(@name, 'enabled')])[2]" to "1"
|
||||
And I set the field with xpath "(//select[contains(@name, 'day')])[2]" to "1"
|
||||
And I set the field with xpath "(//select[contains(@name, 'month')])[2]" to "January"
|
||||
And I set the field with xpath "(//select[contains(@name, 'year')])[2]" to "<year>"
|
||||
And I click on "Any of the selected courses is complete" "radio"
|
||||
And I press "Save"
|
||||
And I press "Enable access"
|
||||
And I click on "Enable" "button" in the "Confirm" "dialogue"
|
||||
And user "student1" has attempted "Test quiz name C1" with responses:
|
||||
| slot | response |
|
||||
| 1 | True |
|
||||
And user "student1" has attempted "Test quiz name C2" with responses:
|
||||
| slot | response |
|
||||
| 1 | True |
|
||||
# Completion cron won't mark the whole course completed unless the
|
||||
# individual criteria was marked completed more than a second ago. So
|
||||
# run it twice, first to mark the criteria and second for the course.
|
||||
And I run the scheduled task "core\task\completion_regular_task"
|
||||
And I wait "1" seconds
|
||||
And I run the scheduled task "core\task\completion_regular_task"
|
||||
When I run the scheduled task "core\task\badges_cron_task"
|
||||
And I run all adhoc tasks
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I follow "Site Badge"
|
||||
Then I should see "Recipients (<count>)"
|
||||
And I select "Recipients (<count>)" from the "jump" singleselect
|
||||
And I <student1_visibility> see "Student 1"
|
||||
And I should not see "Student 2"
|
||||
|
||||
Examples:
|
||||
| year | count | student1_visibility |
|
||||
| ## +1 year ## %Y ## | 1 | should |
|
||||
| ## -1 year ## %Y ## | 0 | should not |
|
||||
@@ -104,6 +104,9 @@ class block_completionstatus extends block_base {
|
||||
// Flag to set if current completion data is inconsistent with what is stored in the database.
|
||||
$pending_update = false;
|
||||
|
||||
// Get activities visible to the user that have completion enabled.
|
||||
$visibleactivities = $info->get_user_activities_with_completion($USER->id);
|
||||
|
||||
// Loop through course criteria.
|
||||
foreach ($completions as $completion) {
|
||||
$criteria = $completion->get_criteria();
|
||||
@@ -115,6 +118,11 @@ class block_completionstatus extends block_base {
|
||||
|
||||
// Activities are a special case, so cache them and leave them till last.
|
||||
if ($criteria->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
|
||||
// Only include activities that are visible to the user.
|
||||
if (!isset($visibleactivities[$criteria->moduleinstance])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$activities[$criteria->moduleinstance] = $complete;
|
||||
|
||||
if ($complete) {
|
||||
|
||||
@@ -119,10 +119,21 @@ $pendingupdate = false;
|
||||
// Load criteria to display.
|
||||
$completions = $info->get_completions($user->id);
|
||||
|
||||
// Get activities visible to the user that have completion enabled.
|
||||
$visibleactivities = $info->get_user_activities_with_completion($user->id);
|
||||
|
||||
// Loop through course criteria.
|
||||
foreach ($completions as $completion) {
|
||||
$criteria = $completion->get_criteria();
|
||||
|
||||
// Skip display of activity completion criteria for activities the user cannot see.
|
||||
if (
|
||||
$criteria->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY &&
|
||||
!isset($visibleactivities[$criteria->moduleinstance])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$pendingupdate && $criteria->is_pending($completion)) {
|
||||
$pendingupdate = true;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user