Compare commits
578 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2c2f2a0c5 | |||
| 7a7f029149 | |||
| 6cfefde94b | |||
| be8d3d004f | |||
| d42c5d9311 | |||
| 1c0e0f55bf | |||
| 6b4d33a98f | |||
| 506a4a33d4 | |||
| afe3c2b88f | |||
| 1a8415f1f1 | |||
| caab968624 | |||
| f3748c1006 | |||
| 72f49e4c76 | |||
| 4e8e5ca6ca | |||
| 3cf9457a36 | |||
| 31d96fb54f | |||
| 43874f1138 | |||
| e5269918b5 | |||
| 857c90735e | |||
| 1d95966c57 | |||
| f9a2bb1125 | |||
| cba59e58ec | |||
| 6d92262ecb | |||
| f3227bee2e | |||
| b1ba8e672e | |||
| 1ecd11e72e | |||
| 3e2dbf557b | |||
| 50c2251ef2 | |||
| 4dce7cdcd0 | |||
| 1241a17d4b | |||
| d3ec08f146 | |||
| c2bd37c0ec | |||
| 417c399bf6 | |||
| ffb5bfee38 | |||
| 6f84540075 | |||
| c6d921e53a | |||
| 890849b3e1 | |||
| f3b0bb1896 | |||
| a528f9ca2e | |||
| 1768ae93e8 | |||
| aa24806a89 | |||
| 156d7a5845 | |||
| 6bf10930c7 | |||
| adfde92245 | |||
| 641639c0a7 | |||
| f4254a24ad | |||
| cbeefb6854 | |||
| 5567567e5b | |||
| ec6551ce80 | |||
| 9118477079 | |||
| 53f41838e7 | |||
| 1b56df014c | |||
| 2edd3e1155 | |||
| a68bc9319a | |||
| e4c595aac8 | |||
| d3eaf24089 | |||
| 39012d0b09 | |||
| 3dcfe6520b | |||
| d4c569d95e | |||
| 35ad89a715 | |||
| f557352f22 | |||
| 894f2f161e | |||
| f6c8f4a098 | |||
| 93045eb621 | |||
| 5bff1a62b1 | |||
| e195cb3905 | |||
| 3c3f130d2c | |||
| 99809ba681 | |||
| 9c9b31658d | |||
| 23ad73ee56 | |||
| 9ce4d3a2de | |||
| b17cba4ef7 | |||
| f0c84172fb | |||
| c9913935bf | |||
| 72fb26ef25 | |||
| 8e40b622bd | |||
| f009e8e887 | |||
| e46b6a53ed | |||
| 1646ab8620 | |||
| 4290c0db02 | |||
| 61dc480cfa | |||
| c13c75342f | |||
| e394149084 | |||
| b275637a13 | |||
| 771e06bb2f | |||
| e3a4f45aa7 | |||
| 794b06cf63 | |||
| f833fba135 | |||
| 8fec0dc9e6 | |||
| df9acfa2ec | |||
| 52ef39651d | |||
| c03ad2f40d | |||
| 9e2daa9d6c | |||
| 6eb5c80e75 | |||
| d807b51210 | |||
| 6880fa0c12 | |||
| 31c83f8aa5 | |||
| 1ef32165a0 | |||
| ede9478779 | |||
| c083984fd0 | |||
| 7049a8334c | |||
| 2025178343 | |||
| 0620c07639 | |||
| 39096ac3d1 | |||
| d404adf500 | |||
| dd9b7e8deb | |||
| b357491d00 | |||
| 6139a04b64 | |||
| 9ebe2f8676 | |||
| a2aaba5f5d | |||
| 4112aa8957 | |||
| 39026940b3 | |||
| 0e0bdbdd76 | |||
| 5a40f5524e | |||
| 806b3f2d69 | |||
| 3d812fe6c3 | |||
| 367a2a7678 | |||
| 70ba060e57 | |||
| f5fc38f35a | |||
| dcd55b53a7 | |||
| a45cfb7c7b | |||
| c300545e9e | |||
| 48c9857449 | |||
| 70c14d4d22 | |||
| 66cf248632 | |||
| 4cff8eeaa2 | |||
| edfb86e638 | |||
| 54d4e61c7f | |||
| 66a22969e1 | |||
| 6bc1159c89 | |||
| 2268d3d2f7 | |||
| 7a4926411e | |||
| 187195c9e1 | |||
| 6dead7e7cb | |||
| 377a2c30f4 | |||
| 4fcf6aa947 | |||
| 7a8f504640 | |||
| 8aececb06e | |||
| 4b36673cd5 | |||
| be8600ab99 | |||
| 5bac9da882 | |||
| 5f854f895d | |||
| 7e8bef4959 | |||
| 02a0f72a8d | |||
| e7cf3f225a | |||
| 8a6832f499 | |||
| 0d0d8eed03 | |||
| 5cc31e17d4 | |||
| 7d789a5a4a | |||
| 65605f10b0 | |||
| e5bc55a7e4 | |||
| 78a1885ed9 | |||
| 1d51a21af3 | |||
| 0df615a945 | |||
| f5a401c0a5 | |||
| f5f42602f2 | |||
| c80a2a4009 | |||
| cdff966713 | |||
| c4fa1575a2 | |||
| 2737ed2183 | |||
| 5caa509d87 | |||
| a94f9cd373 | |||
| e0a5d727dd | |||
| 795b2a90c5 | |||
| 6aeb97c555 | |||
| 22cd43751f | |||
| c6e762b243 | |||
| a485e9e125 | |||
| f237faf8fd | |||
| 97dea969f5 | |||
| 31bbed6697 | |||
| e800f30163 | |||
| f308518c76 | |||
| 0ceef6ded6 | |||
| b646b9fde2 | |||
| 391b88f628 | |||
| 7c7d31011e | |||
| 1e1eb4f592 | |||
| df3a81046d | |||
| ea4c9193df | |||
| 5c8f1e4bf9 | |||
| 22c0f4e15f | |||
| 0f5d368eb2 | |||
| c86e79b0b7 | |||
| 209aba758e | |||
| 1edd0e6a65 | |||
| f18f512e23 | |||
| 1f16a6fa95 | |||
| a3d38b036a | |||
| c90320d471 | |||
| 357864383e | |||
| 233a9bc2ac | |||
| 011fee9440 | |||
| a7813201d1 | |||
| ab6a201b2c | |||
| e2f1a37ab5 | |||
| 9229bd7fac | |||
| 90a486a697 | |||
| da267624c5 | |||
| dfc522ccbb | |||
| 0fa4c468c9 | |||
| 946340aa07 | |||
| 162c0597d5 | |||
| d900d4f97b | |||
| 66c3ff069a | |||
| e44a13957c | |||
| 40b64c14be | |||
| 9341e5c73f | |||
| 2ee07eeeb5 | |||
| b71683f20b | |||
| df9eaaff5b | |||
| c39ab02a05 | |||
| 81b9c1e625 | |||
| 655ca834be | |||
| cd23b1d465 | |||
| 7a075f6f4f | |||
| c2e068cfc6 | |||
| 8e9c4bfdab | |||
| 13222eb585 | |||
| 54b3164035 | |||
| 1a7975c762 | |||
| 3f33e07952 | |||
| 1999f2020b | |||
| a838a2a97e | |||
| 8cd0734f14 | |||
| 46fddd22f0 | |||
| d28895b921 | |||
| 3248bea893 | |||
| 688a742017 | |||
| 5dc927426b | |||
| b56963d92d | |||
| 10cef5e611 | |||
| 86bac36c84 | |||
| bc5e19b626 | |||
| bc607ceb0b | |||
| a637333419 | |||
| 55417a7ef6 | |||
| 9b3855222d | |||
| 279c1925e1 | |||
| 981b69b0ca | |||
| bcf8806a82 | |||
| 5e9e3cfd8b | |||
| 700724363e | |||
| 9fd9388e2a | |||
| 7562615cd2 | |||
| d42486ced8 | |||
| 0113f57641 | |||
| 826ed6ace0 | |||
| 8a371c2bc8 | |||
| 0c9ad85a06 | |||
| 9b6573ad64 | |||
| e1142f89ea | |||
| 37e24ebcaf | |||
| 812a0873b9 | |||
| 451d088cac | |||
| b6ef7fdfe0 | |||
| 9288446a93 | |||
| a26a02d746 | |||
| 4b739bdcbf | |||
| b22b1dbd31 | |||
| 72dc3343bb | |||
| ccd7e02f0e | |||
| 1cd558bc1a | |||
| 7653430489 | |||
| 20acf53b3d | |||
| fa4f770b87 | |||
| 95f16b20ec | |||
| 3015d5b74e | |||
| 8fc0b91868 | |||
| a313647a92 | |||
| 59433cc60e | |||
| d2cf28d3c8 | |||
| db78394b90 | |||
| 198039e1c6 | |||
| 1e6639141f | |||
| 60e6d71df8 | |||
| 3eac771791 | |||
| 0530955961 | |||
| d73a6d2c1a | |||
| c7ea0da70d | |||
| 4a7f3166a4 | |||
| 7b6856793f | |||
| 48913b2b0e | |||
| 53a9a3f108 | |||
| 8ed65942cb | |||
| a3c6263c2e | |||
| a89db0244b | |||
| 20e1d63562 | |||
| dc8d401481 | |||
| df4189d1fd | |||
| 52b76e2fcd | |||
| 2bf213cfb2 | |||
| b945d89a41 | |||
| 0dd1c8137f | |||
| cfe63e8642 | |||
| f7879992dd | |||
| 0b7b9b453e | |||
| 923ce84983 | |||
| 3c5c9f37ff | |||
| 1ff7230f76 | |||
| 9053f4c192 | |||
| e6ff7d02d8 | |||
| 1c155f2fa0 | |||
| d89332888e | |||
| 5bf35c535e | |||
| 86799dab96 | |||
| d1321ff0d5 | |||
| e25fbec380 | |||
| 8464ca1b72 | |||
| c476ebe695 | |||
| 03429740d2 | |||
| 32897778cd | |||
| 1e2dcb145f | |||
| df9109fa9e | |||
| ecc53ae6a1 | |||
| e37eadb84b | |||
| d893d639d0 | |||
| 8d365edffe | |||
| 7a5a3f261e | |||
| a8ecc443fe | |||
| 0b32ae0b46 | |||
| 519a09da8b | |||
| beb129d286 | |||
| 2a2d293f3f | |||
| 1849cca694 | |||
| 628bb639ae | |||
| 8e409341bd | |||
| f856c7ba24 | |||
| 7a281ba10c | |||
| d0a9dba00d | |||
| 37d10d8350 | |||
| 9c3ea97d9b | |||
| 62ae269490 | |||
| 9f97900524 | |||
| e778f6fbbd | |||
| ee439d8fa8 | |||
| de0150412b | |||
| d1a9ba5386 | |||
| d1a3d40f65 | |||
| 7b0eac6677 | |||
| 1044306ce9 | |||
| 8648dc6046 | |||
| f7f1a4562d | |||
| 30b02b20ef | |||
| 94d260a4f8 | |||
| 63c2d91bc5 | |||
| 4bbb5edf35 | |||
| bcf0384d05 | |||
| e7cf1c6130 | |||
| 4fdc0b6768 | |||
| 9883338e8c | |||
| 08ccef4aec | |||
| cc69e589a4 | |||
| 29857c9cde | |||
| 2f6ec1d60a | |||
| 34ce92a046 | |||
| e96ec9501a | |||
| 76997fbf33 | |||
| 8135ded9e3 | |||
| c8f0320b24 | |||
| ab4c436f10 | |||
| 9cf08382ae | |||
| 85d0a666a3 | |||
| 0ea18ecce6 | |||
| 38a820017a | |||
| 4f6526ddb1 | |||
| bf9b563340 | |||
| 02c4d8c217 | |||
| 321a86a858 | |||
| 2d2fd03b2c | |||
| 9ce91a9588 | |||
| b2293b37f3 | |||
| afab4c9d25 | |||
| f1e8b839f8 | |||
| b7a3d429e2 | |||
| 10b28ebb08 | |||
| dfaa45838e | |||
| af0713a6b7 | |||
| 5ee8e97b61 | |||
| 91c8edfbef | |||
| ebdd862f3c | |||
| eb664d6d41 | |||
| c4f1af803c | |||
| 0418c85bb4 | |||
| d5bad6b64d | |||
| c922b363c4 | |||
| 38be896581 | |||
| 6efd33e7b3 | |||
| 0a87d4507b | |||
| 907a3442e9 | |||
| 5e20e1d8b8 | |||
| 7c6d7a9e1f | |||
| c131932352 | |||
| c7d4f457d7 | |||
| 4e09d70272 | |||
| 05d0c9b002 | |||
| e309724b41 | |||
| de7c1fdbe1 | |||
| 8c5c5eaee4 | |||
| e19a406db8 | |||
| 39d9131ad7 | |||
| d9a7733e16 | |||
| a11ccf13b6 | |||
| dc3598bcb4 | |||
| 095ce67f60 | |||
| 81a0fbee7f | |||
| ce4b7b94e0 | |||
| 5d8db4b548 | |||
| 4b9ad6b9fd | |||
| b10dacbc75 | |||
| 9d9d52a404 | |||
| ee4dd60780 | |||
| a923f3724b | |||
| 641528e9d3 | |||
| 7d24bb77b3 | |||
| d5521c1871 | |||
| 6bbceaa405 | |||
| f6c66d2be9 | |||
| bb18341e72 | |||
| c93be20aa5 | |||
| 0214ea3e19 | |||
| 3b847d4860 | |||
| 44dbf84bcb | |||
| a3b82724be | |||
| 4ded9db847 | |||
| ebeb8b6af2 | |||
| bbda6fabe0 | |||
| 850b45a996 | |||
| edbd895f60 | |||
| 01834f4bc1 | |||
| c9ee1c582d | |||
| 00970d6e59 | |||
| 170a6056de | |||
| ffc6111f4e | |||
| a8e32ae7f3 | |||
| ea7b951d3c | |||
| c0cd20aa61 | |||
| a0ffa26132 | |||
| f27faa1305 | |||
| ebc1084cdc | |||
| 0357a43862 | |||
| 85a77b7a6c | |||
| e42e6b6408 | |||
| 8e52bbe341 | |||
| 3204204ad9 | |||
| cc335884de | |||
| 30c73fa641 | |||
| 44f9fb5150 | |||
| b9af7918c8 | |||
| b9a6b71e3c | |||
| 9f330b6b40 | |||
| dcc7e81ac2 | |||
| 54e319351a | |||
| bfeda89d8c | |||
| 84306938ac | |||
| a46f6637cc | |||
| e756ec3228 | |||
| 1c8e7f1c7b | |||
| e6c202c420 | |||
| f666a0d059 | |||
| dfa3176e03 | |||
| 8d5cec0164 | |||
| 7636570179 | |||
| ae21155775 | |||
| b637072b76 | |||
| f10d3c78e0 | |||
| aab8839ab6 | |||
| 7b6463fbba | |||
| 42a7c6df26 | |||
| a3968e0b73 | |||
| aec7468f67 | |||
| ee36d500c9 | |||
| 6cf9ed0f40 | |||
| d0be2a57ce | |||
| 2653626ca0 | |||
| ace600fe84 | |||
| 28fe42c4ec | |||
| c27fde3701 | |||
| a9cf380584 | |||
| 21cb028390 | |||
| d8cbfc285a | |||
| bc3f269ac8 | |||
| 7a6e74a9fe | |||
| c489b7ac0a | |||
| 69c42304f0 | |||
| dea5950384 | |||
| 6aec3fe946 | |||
| 72fe24b91a | |||
| a7c0d3605a | |||
| d53466d1c6 | |||
| cc10e8e245 | |||
| 65bddce33c | |||
| 4bb0555b62 | |||
| 63448b6f98 | |||
| bc472d3ea1 | |||
| ed9673fe1f | |||
| 004cc09738 | |||
| a41dc037c9 | |||
| 4158ea095b | |||
| 337346e3d1 | |||
| b04e0bd518 | |||
| 028728b561 | |||
| b33729ab1d | |||
| f69c22ecda | |||
| ca2c397895 | |||
| 7dfc4481d4 | |||
| dc74787166 | |||
| a6146d1c58 | |||
| ca050231fb | |||
| e4e7d72571 | |||
| 938dbde96c | |||
| 15f30a7314 | |||
| 3bb18b29e0 | |||
| c69a91921e | |||
| bb56dbdc74 | |||
| dc3c3ca98a | |||
| 15bc48978d | |||
| a93254f43c | |||
| 77beaab116 | |||
| ce3a869c07 | |||
| e76ec78b37 | |||
| fe0f5baa2d | |||
| caf61abd94 | |||
| 179848ea1f | |||
| b4c7d832b7 | |||
| a061fd95bd | |||
| c9661b9a52 | |||
| af96e46e56 | |||
| c1bdb76d18 | |||
| a97e536341 | |||
| 0e1de8d591 | |||
| 51587e9909 | |||
| 43767635b7 | |||
| 68776525be | |||
| 41728c3d87 | |||
| fcdcd9d7e6 | |||
| 7e3e2077f0 | |||
| 2395392c99 | |||
| aef8d9e6f1 | |||
| e74c4acec4 | |||
| 74c9f95c4c | |||
| 5ce61f4be4 | |||
| c9a677dc7c | |||
| e9b11f36d5 | |||
| e6bae7d01f | |||
| 9359d08bd6 | |||
| 7b1af357ff | |||
| ff4c249e39 | |||
| 106fae466d | |||
| 2be96308d3 | |||
| 2bf704c153 | |||
| e07cf88f4d | |||
| 31ce71608d | |||
| 84858b01cf | |||
| 969b1a7f98 | |||
| 33035402a4 | |||
| 943801d2d9 | |||
| 59aef0a010 | |||
| 20e708cf73 | |||
| 47fb08fc6d | |||
| a576b9db5d | |||
| 9cd0fe59a3 | |||
| 66db7639e9 | |||
| 168d27f0b3 | |||
| cc7a7d2b2c | |||
| 98659fad08 | |||
| cbd7cd61fa | |||
| 9f56af660a | |||
| 63cc9efdbc | |||
| 17eb0624ab | |||
| 202fe57da2 | |||
| e3411249d6 | |||
| 8cce7a8a82 | |||
| 4390e42ea2 | |||
| 57e7f3ae94 | |||
| ac30d9e451 | |||
| e3ccdbee8a | |||
| 14d365dcf0 |
@@ -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"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
issueNumber: MDL-87443
|
||||
notes:
|
||||
core:
|
||||
- message: >-
|
||||
There is a new Behat `toast_message` named selector to more easily
|
||||
assert the presence of Toast messages on the page
|
||||
type: improved
|
||||
@@ -6,6 +6,94 @@ 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).
|
||||
|
||||
## 4.5.9
|
||||
|
||||
### core
|
||||
|
||||
#### 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)
|
||||
|
||||
## 4.5.8
|
||||
|
||||
### core
|
||||
|
||||
#### Changed
|
||||
|
||||
- The Hook Manager now uses localcache instead of caching via MUC.
|
||||
|
||||
For more information see [MDL-87107](https://tracker.moodle.org/browse/MDL-87107)
|
||||
|
||||
#### Fixed
|
||||
|
||||
- `restore_qtype_plugin::unset_excluded_fields` now returns the modified questiondata structure,
|
||||
in order to support structures that contain arrays.
|
||||
If your qtype plugin overrides `restore_qtype_plugin::remove_excluded_question_data` without
|
||||
calling the parent method, you may need to modify your overridden method to use the returned
|
||||
value.
|
||||
|
||||
For more information see [MDL-85975](https://tracker.moodle.org/browse/MDL-85975)
|
||||
- When responding to pcntl signals, call existing signal handlers.
|
||||
|
||||
For more information see [MDL-87079](https://tracker.moodle.org/browse/MDL-87079)
|
||||
|
||||
### mod_glossary
|
||||
|
||||
#### Added
|
||||
|
||||
- Function mod_glossary_rating_can_see_item_ratings is now implemented for checking permissions to view ratings.
|
||||
|
||||
For more information see [MDL-86960](https://tracker.moodle.org/browse/MDL-86960)
|
||||
|
||||
## 4.5.7
|
||||
|
||||
### core
|
||||
|
||||
#### Added
|
||||
|
||||
- The Behat `::execute()` method now accepts an array-style callable in addition to the string `classname::method` format.
|
||||
|
||||
The following formats are now accepted:
|
||||
|
||||
```php
|
||||
// String format:
|
||||
$this->execute('behat_general::i_click_on', [...]);
|
||||
|
||||
// Array format:
|
||||
$this->execute([behat_general::class,' i_click_on'], [...]);
|
||||
```
|
||||
|
||||
For more information see [MDL-86231](https://tracker.moodle.org/browse/MDL-86231)
|
||||
- The `\externallib_advanced_testcase` has been replaced by `\core_external\tests\externallib_testcase` and is now autoloadable.
|
||||
|
||||
For more information see [MDL-86283](https://tracker.moodle.org/browse/MDL-86283)
|
||||
|
||||
### core_badges
|
||||
|
||||
#### Added
|
||||
|
||||
- A number of new static methods have been added to `core_badges\backpack_api` to support the new Canvas Credentials backpack provider. These methods allow you to retrieve lists of providers and regions, check if Canvas Credentials fields should be displayed, and get a region URL or API URL based on a given region ID. The new methods include `get_providers`, `get_regions`, `display_canvas_credentials_fields`, `get_region_url`, `get_region_api_url`, `get_regionid_from_url`, and `is_canvas_credentials_region`.
|
||||
|
||||
For more information see [MDL-86174](https://tracker.moodle.org/browse/MDL-86174)
|
||||
|
||||
### core_grades
|
||||
|
||||
#### Added
|
||||
|
||||
- New 'is_gradable()' function has been created to return whether the item has any gradeitem that is GRADE_TYPE_VALUE or GRADE_TYPE_SCALE.
|
||||
|
||||
For more information see [MDL-85837](https://tracker.moodle.org/browse/MDL-85837)
|
||||
|
||||
### core_message
|
||||
|
||||
#### Added
|
||||
|
||||
- The `contexturl` property to `\core\message\message` instances can now contain `\core\url` values in addition to plain strings
|
||||
|
||||
For more information see [MDL-83080](https://tracker.moodle.org/browse/MDL-83080)
|
||||
|
||||
## 4.5.6
|
||||
|
||||
### core
|
||||
|
||||
@@ -69,7 +69,7 @@ if ($options['enablelater']) {
|
||||
}
|
||||
|
||||
$time = time() + ($options['enablelater']*60);
|
||||
set_config('maintenance_later', $time);
|
||||
set_config('maintenance_later', $time, null, true);
|
||||
|
||||
echo get_string('clistatusenabledlater', 'admin', userdate($time))."\n";
|
||||
return 0;
|
||||
@@ -77,23 +77,32 @@ if ($options['enablelater']) {
|
||||
} else if ($options['enable']) {
|
||||
if (file_exists("$CFG->dataroot/climaintenance.html")) {
|
||||
// The maintenance is already enabled, nothing to do.
|
||||
} else {
|
||||
enable_cli_maintenance_mode();
|
||||
exit(0);
|
||||
}
|
||||
enable_cli_maintenance_mode();
|
||||
set_config('maintenance_enabled', 'cli mode', null, true);
|
||||
|
||||
if (isset($CFG->maintenance_later)) {
|
||||
unset_config('maintenance_later', null, true);
|
||||
}
|
||||
set_config('maintenance_enabled', 0);
|
||||
unset_config('maintenance_later');
|
||||
echo get_string('sitemaintenanceoncli', 'admin')."\n";
|
||||
exit(0);
|
||||
|
||||
} else if ($options['enableold']) {
|
||||
set_config('maintenance_enabled', 1);
|
||||
unset_config('maintenance_later');
|
||||
set_config('maintenance_enabled', 1, null, true);
|
||||
if (isset($CFG->maintenance_later)) {
|
||||
unset_config('maintenance_later', null, true);
|
||||
}
|
||||
echo get_string('sitemaintenanceon', 'admin')."\n";
|
||||
exit(0);
|
||||
|
||||
} else if ($options['disable']) {
|
||||
set_config('maintenance_enabled', 0);
|
||||
unset_config('maintenance_later');
|
||||
if ($CFG->maintenance_enabled !== '0') {
|
||||
set_config('maintenance_enabled', 0, null, true);
|
||||
}
|
||||
if (isset($CFG->maintenance_later)) {
|
||||
unset_config('maintenance_later', null, true);
|
||||
}
|
||||
if (file_exists("$CFG->dataroot/climaintenance.html")) {
|
||||
unlink("$CFG->dataroot/climaintenance.html");
|
||||
}
|
||||
|
||||
@@ -158,13 +158,13 @@ if ($options['plugins']) {
|
||||
if (is_null($plugin)) {
|
||||
cli_writeln('Unknown plugin: ' . $component);
|
||||
} else {
|
||||
$pluginstring = $plugin->component . "\t" . $plugin->displayname;
|
||||
$pluginstring = $component . "\t" . $plugin->displayname;
|
||||
|
||||
if ($pluginman->can_uninstall_plugin($plugin->component)) {
|
||||
if ($pluginman->can_uninstall_plugin($component)) {
|
||||
if ($options['run']) {
|
||||
cli_writeln('Uninstalling: ' . $pluginstring);
|
||||
$progress = new progress_trace_buffer(new text_progress_trace(), true);
|
||||
$pluginman->uninstall_plugin($plugin->component, $progress);
|
||||
$pluginman->uninstall_plugin($component, $progress);
|
||||
$progress->finished();
|
||||
cli_write($progress->get_buffer());
|
||||
} else {
|
||||
|
||||
@@ -3835,6 +3835,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="optional"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -4411,6 +4412,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -4607,6 +4609,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -4802,6 +4805,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -4997,6 +5001,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -5100,4 +5105,208 @@
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="5.2" requires="4.4">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="10.11.0" />
|
||||
<VENDOR name="mysql" version="8.4" />
|
||||
<VENDOR name="postgres" version="16" />
|
||||
<VENDOR name="mssql" version="15.0" />
|
||||
<VENDOR name="auroramysql" version="8.0" />
|
||||
</DATABASE>
|
||||
<PHP version="8.3.0" level="required">
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="pcreunicodewarning" />
|
||||
</FEEDBACK>
|
||||
</PCREUNICODE>
|
||||
<PHP_EXTENSIONS>
|
||||
<PHP_EXTENSION name="iconv" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="iconvrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="mbstring" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="mbstringrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="curl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="curlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="openssl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="opensslrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="tokenizer" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="ctype" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ctyperequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zip" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ziprequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zlib" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="gd" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="gdrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="simplexml" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="simplexmlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="spl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="splrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="pcre" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="dom" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xml" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlreader" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="intl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="intlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="json" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="settingmemorylimit" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="file_uploads" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingfileuploads" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="opcache.enable" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="opcacherecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="zend.exception_ignore_args" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingzendexceptionignoreargs" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
</PHP_SETTINGS>
|
||||
<CUSTOM_CHECKS>
|
||||
<CUSTOM_CHECK function="\core\environment::check_composer_dependencies_installed" level="optional" />
|
||||
<CUSTOM_CHECK function="\core\environment::check_composer_developer_dependencies_not_installed" level="optional" />
|
||||
<CUSTOM_CHECK function="\core\environment::check_composer_dependencies_optimised" level="optional" />
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbstorageengine" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="quizattemptsupgradedmessage" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="slashargumentswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unsupporteddbtablerowformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unoconvwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="libcurlwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfileformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfilepertable" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddblargeprefix" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="ishttpswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="incompleteunicodesupport" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="sixtyfourbitsrequired" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_admin_dir_usage" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="iscustomadminwarnings" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mod_assignment" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_db_prefix_length" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_async_backup" level="recommended">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_aurora_version" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
+32
-8
@@ -916,8 +916,14 @@ if (empty($CFG->disabledevlibdirscheck) && (is_dir($CFG->dirroot.'/vendor') || i
|
||||
// Check if the site is being foced onto ssl.
|
||||
$overridetossl = !empty($CFG->overridetossl);
|
||||
|
||||
// Check if moodle campaign content setting is enabled or not.
|
||||
$showcampaigncontent = !isset($CFG->showcampaigncontent) || $CFG->showcampaigncontent;
|
||||
if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) {
|
||||
// We need to add this in order to be able to verify the showcampaigncontent setting behaviour during behat tests.
|
||||
$showcampaigncontent = get_config('core', 'showcampaigncontent');
|
||||
$showcampaigncontent = $showcampaigncontent === 'true';
|
||||
} else {
|
||||
// Check if moodle campaign content setting is enabled or not.
|
||||
$showcampaigncontent = !isset($CFG->showcampaigncontent) || $CFG->showcampaigncontent;
|
||||
}
|
||||
|
||||
// Encourage admins to enable the user feedback feature if it is not enabled already.
|
||||
$showfeedbackencouragement = empty($CFG->enableuserfeedback);
|
||||
@@ -935,9 +941,27 @@ admin_externalpage_setup('adminnotifications');
|
||||
|
||||
$output = $PAGE->get_renderer('core', 'admin');
|
||||
|
||||
echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
|
||||
$maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
|
||||
$registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
|
||||
$mobileconfigured, $overridetossl, $invalidforgottenpasswordurl, $croninfrequent,
|
||||
$showcampaigncontent, $showfeedbackencouragement, $servicesandsupportcontent,
|
||||
$xmlrpcwarning);
|
||||
echo $output->admin_notifications_page(
|
||||
$maturity,
|
||||
$insecuredataroot,
|
||||
$errorsdisplayed,
|
||||
$cronoverdue,
|
||||
$dbproblems,
|
||||
$maintenancemode,
|
||||
$availableupdates,
|
||||
$availableupdatesfetch,
|
||||
$buggyiconvnomb,
|
||||
$registered,
|
||||
$cachewarnings,
|
||||
$eventshandlers,
|
||||
$themedesignermode,
|
||||
$devlibdir,
|
||||
$mobileconfigured,
|
||||
$overridetossl,
|
||||
$invalidforgottenpasswordurl,
|
||||
$croninfrequent,
|
||||
$showcampaigncontent,
|
||||
$showfeedbackencouragement,
|
||||
$servicesandsupportcontent,
|
||||
$xmlrpcwarning
|
||||
);
|
||||
|
||||
+41
-8
@@ -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 = '';
|
||||
@@ -314,6 +331,7 @@ 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->userfeedback_encouragement($showfeedbackencouragement);
|
||||
$output .= $this->services_and_support_content($showservicesandsupport);
|
||||
$output .= $this->campaign_content($showcampaigncontent);
|
||||
@@ -2218,7 +2236,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
public function moodleorg_registration_message() {
|
||||
$a = new stdClass();
|
||||
$a->moreinformation = '#id_sitestats'; // More information anchor.
|
||||
$a->moodleapp = HUB_MOODLEORGHUBURL . '/solutions/moodle-app/';
|
||||
$a->moodleapp = MOODLE_PRODUCTURL . '/solutions/moodle-app/';
|
||||
$out = format_text(get_string('registerwithmoodleorginfo', 'core_hub', $a), FORMAT_MARKDOWN);
|
||||
|
||||
$out .= html_writer::link(
|
||||
@@ -2267,6 +2285,21 @@ 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 '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the theme selector list.
|
||||
*
|
||||
|
||||
@@ -65,6 +65,21 @@ if (($hassiteconfig || has_any_capability(array(
|
||||
new lang_string('allowexternalbackpack', 'badges'),
|
||||
new lang_string('allowexternalbackpack_desc', 'badges'), 1));
|
||||
|
||||
$defaultcanvasregions = [
|
||||
'Australia|https://au.badgr.io|https://api.au.badgr.io/v2',
|
||||
'Canada|https://ca.badgr.io|https://api.ca.badgr.io/v2',
|
||||
'Europe|https://eu.badgr.io|https://api.eu.badgr.io/v2',
|
||||
'Singapore|https://sg.badgr.io|https://api.sg.badgr.io/v2',
|
||||
'United States|https://badgr.io|https://api.badgr.io/v2',
|
||||
];
|
||||
$globalsettings->add(new admin_setting_configtextarea(
|
||||
'badges_canvasregions',
|
||||
new lang_string('canvasregions', 'badges'),
|
||||
new lang_string('canvasregions_desc', 'badges'),
|
||||
implode("\n", $defaultcanvasregions),
|
||||
PARAM_RAW,
|
||||
));
|
||||
|
||||
$ADMIN->add('badges', $globalsettings);
|
||||
|
||||
$ADMIN->add('badges',
|
||||
|
||||
@@ -70,6 +70,14 @@ if ($hassiteconfig) {
|
||||
|
||||
$temp->add(new admin_setting_configtext('googlemapkey3', new lang_string('googlemapkey3', 'core_admin'),
|
||||
new lang_string('googlemapkey3_help', 'core_admin'), '', PARAM_RAW, 60));
|
||||
|
||||
$temp->add(new admin_setting_configtext(
|
||||
'geopluginapikey',
|
||||
new lang_string('geopluginapikey', 'core_admin'),
|
||||
new lang_string('geopluginapikey_desc', 'core_admin'),
|
||||
'',
|
||||
PARAM_TEXT,
|
||||
));
|
||||
}
|
||||
|
||||
$ADMIN->add('location', $temp);
|
||||
|
||||
@@ -79,7 +79,7 @@ Feature: An administrator can browse user accounts
|
||||
Scenario: Delete a user account
|
||||
Given I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I press "Delete" action in the "User One" report row
|
||||
And I should see "Are you absolutely sure you want to completely delete the user User One" in the "Delete user" "dialogue"
|
||||
And I should see "Are you sure you want to delete user User One" in the "Delete user" "dialogue"
|
||||
And I click on "Delete" "button" in the "Delete user" "dialogue"
|
||||
Then I should see "Deleted user User One"
|
||||
And I should not see "User One" in the "reportbuilder-table" "table"
|
||||
@@ -90,6 +90,7 @@ Feature: An administrator can browse user accounts
|
||||
| username | firstname | lastname | email | confirmed |
|
||||
| user3 | User | Three | three@example.com | 0 |
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I change window size to "large"
|
||||
Then I should see "Confirmation pending" in the "User Three" "table_row"
|
||||
And I press "Resend confirmation email" action in the "User Three" report row
|
||||
And I should see "Confirmation email sent successfully"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
@core @core_admin
|
||||
Feature: Toggle campaign banner visibility
|
||||
In order to control the visibility of the campaign banner content
|
||||
As an admin
|
||||
I need to be able to disable the campaign banner display
|
||||
|
||||
Scenario Outline: Admin can disable the campaign banner display
|
||||
Given the following config values are set as admin:
|
||||
| showcampaigncontent | <showcampaigncontent> |
|
||||
And I log in as "admin"
|
||||
When I navigate to "Notifications" in site administration
|
||||
Then "//iframe[@id='campaign-content']" "xpath_element" <display> exist
|
||||
|
||||
Examples:
|
||||
| showcampaigncontent | display |
|
||||
| true | should |
|
||||
| false | should not |
|
||||
@@ -20,9 +20,9 @@ Feature: I can apply presets
|
||||
And I navigate to "Plugins > Course formats > Manage course formats" in site administration
|
||||
And "Disable" "icon" should exist in the "Social" "table_row"
|
||||
And I navigate to "Plugins > Question behaviours > Manage question behaviours" in site administration
|
||||
And "Enabled" "icon" should exist in the "Immediate feedback with CBM" "table_row"
|
||||
And "Enabled" "link" should exist in the "Immediate feedback with CBM" "table_row"
|
||||
And I navigate to "Plugins > Question types > Manage question types" in site administration
|
||||
And "Enabled" "icon" should exist in the "Calculated multichoice" "table_row"
|
||||
And "Enabled" "link" should exist in the "Calculated multichoice" "table_row"
|
||||
When I navigate to "Site admin presets" in site administration
|
||||
And I press "Review settings and apply" action in the "Starter" report row
|
||||
And I should see "Setting changes"
|
||||
@@ -113,9 +113,9 @@ Feature: I can apply presets
|
||||
And I navigate to "Plugins > Course formats > Manage course formats" in site administration
|
||||
And "Disable" "icon" should not exist in the "Social" "table_row"
|
||||
And I navigate to "Plugins > Question behaviours > Manage question behaviours" in site administration
|
||||
And "Enabled" "icon" should not exist in the "Immediate feedback with CBM" "table_row"
|
||||
And "Enabled" "link" should not exist in the "Immediate feedback with CBM" "table_row"
|
||||
And I navigate to "Plugins > Question types > Manage question types" in site administration
|
||||
And "Enabled" "icon" should not exist in the "Calculated multichoice" "table_row"
|
||||
And "Enabled" "link" should not exist in the "Calculated multichoice" "table_row"
|
||||
|
||||
Scenario: Re-applying Starter Moodle preset does not display setting changes
|
||||
# Apply Starter preset.
|
||||
|
||||
@@ -27,9 +27,9 @@ Feature: I can revert changes after a load
|
||||
And I navigate to "Plugins > Course formats > Manage course formats" in site administration
|
||||
And "Disable" "icon" should not exist in the "Social" "table_row"
|
||||
And I navigate to "Plugins > Question behaviours > Manage question behaviours" in site administration
|
||||
And "Enabled" "icon" should not exist in the "Immediate feedback with CBM" "table_row"
|
||||
And "Enabled" "link" should not exist in the "Immediate feedback with CBM" "table_row"
|
||||
And I navigate to "Plugins > Question types > Manage question types" in site administration
|
||||
And "Enabled" "icon" should not exist in the "Calculated multichoice" "table_row"
|
||||
And "Enabled" "link" should not exist in the "Calculated multichoice" "table_row"
|
||||
And I navigate to "Site admin presets" in site administration
|
||||
And I press "Show version history" action in the "Starter" report row
|
||||
When I click on "Restore this version" "link"
|
||||
@@ -45,6 +45,6 @@ Feature: I can revert changes after a load
|
||||
And I navigate to "Plugins > Course formats > Manage course formats" in site administration
|
||||
And "Disable" "icon" should exist in the "Social" "table_row"
|
||||
And I navigate to "Plugins > Question behaviours > Manage question behaviours" in site administration
|
||||
And "Enabled" "icon" should exist in the "Immediate feedback with CBM" "table_row"
|
||||
And "Enabled" "link" should exist in the "Immediate feedback with CBM" "table_row"
|
||||
And I navigate to "Plugins > Question types > Manage question types" in site administration
|
||||
And "Enabled" "icon" should exist in the "Calculated multichoice" "table_row"
|
||||
And "Enabled" "link" should exist in the "Calculated multichoice" "table_row"
|
||||
|
||||
@@ -39,7 +39,7 @@ The component output classes can render an action menu entirely in PHP. The step
|
||||
|
||||
The following code is a basic example of an action menu:
|
||||
|
||||
{{< php >}}
|
||||
```php
|
||||
/** @var core_renderer $output*/
|
||||
$output = $PAGE->get_renderer('core');
|
||||
|
||||
@@ -54,11 +54,11 @@ $menu->add(new action_menu_link(
|
||||
));
|
||||
|
||||
echo $output->render($menu);
|
||||
{{< / php >}}
|
||||
```
|
||||
|
||||
And this is the same example but passing the items in the creation:
|
||||
|
||||
{{< php >}}
|
||||
```php
|
||||
/** @var core_renderer $output*/
|
||||
$output = $PAGE->get_renderer('core');
|
||||
|
||||
@@ -72,32 +72,64 @@ $menu = new action_menu([
|
||||
]);
|
||||
|
||||
echo $output->render($menu);
|
||||
{{< / php >}}
|
||||
```
|
||||
|
||||
### Setup the menu trigger
|
||||
|
||||
By default, the action menu trigger is a cog icon. However, the class has methods to convert it to a kebab menu or even display any arbitrary content.
|
||||
By default, the action menu trigger is a button that uses the `t/edit_menu` icon and is displayed with a caret. However, the class has methods to convert it to a kebab menu or even display any arbitrary content.
|
||||
|
||||
Example of a kebab menu:
|
||||
#### Example of a kebab menu
|
||||
|
||||
{{< php >}}
|
||||
```php
|
||||
/** @var core_renderer $output*/
|
||||
$output = $PAGE->get_renderer('core');
|
||||
|
||||
$menu = new action_menu();
|
||||
$menu->set_kebab_trigger(get_string('edit'), $output);
|
||||
$menu->set_additional_classes('fields-actions');
|
||||
{{< / php >}}
|
||||
```
|
||||
|
||||
Example of a custom trigger:
|
||||
#### Customising the menu trigger
|
||||
|
||||
{{< php >}}
|
||||
/** @var core_renderer $output*/
|
||||
$output = $PAGE->get_renderer('core');
|
||||
##### Trigger with a text label
|
||||
|
||||
```php
|
||||
// This example displays an "Edit" label for the trigger.
|
||||
$menu = new action_menu();
|
||||
$menu->set_menu_trigger(get_string('edit'));
|
||||
{{< / php >}}
|
||||
```
|
||||
|
||||
##### Trigger with an icon
|
||||
|
||||
This example displays an icon for the trigger. When rendering the menu trigger button as an icon button, ensure that the icon is rendered as a decorative image. If you are using a `pix_icon`, pass an empty `$alt` parameter to make the icon decorative.
|
||||
|
||||
The accessible name of the icon button should be set within the button element itself. You can set an accessible name for the icon trigger button either by:
|
||||
* Using the `::set_action_label()` method.
|
||||
```php
|
||||
$menu = new action_menu();
|
||||
// Make sure the pix icon is rendered as a decorative image by passing an empty alt parameter.
|
||||
$icon = $output->pix_icon('t/edit', '');
|
||||
$menu->set_menu_trigger($icon);
|
||||
$menu->set_action_label(get_string('edit'));
|
||||
```
|
||||
|
||||
* Or alternatively, by adding a visually hidden text alongside the icon.
|
||||
```php
|
||||
$menu = new action_menu();
|
||||
// Make sure the pix icon is rendered as a decorative image by passing an empty alt parameter.
|
||||
$icon = $output->pix_icon('t/edit', '');
|
||||
// Add a visually hidden text label for the trigger button.
|
||||
$icon .= html_writer::span(get_string('edit'), 'sr-only');
|
||||
$menu->set_menu_trigger($icon);
|
||||
```
|
||||
|
||||
##### Removing the caret symbol
|
||||
|
||||
You may also remove the caret symbol by adding a `no-caret` class to the `triggerextraclasses` property.
|
||||
|
||||
```php
|
||||
$menu->triggerextraclasses = 'no-caret';
|
||||
```
|
||||
|
||||
### Add items
|
||||
|
||||
@@ -108,7 +140,7 @@ Secondary items: are displayed inside the action menu dropdown.
|
||||
|
||||
The item location must be configured before adding the element. The following example shows different ways to add primary and secondary menu items.
|
||||
|
||||
{{< php >}}
|
||||
```php
|
||||
// Primary items examples.
|
||||
$menu->add(new action_menu_link(
|
||||
new moodle_url($PAGE->url),
|
||||
@@ -134,7 +166,7 @@ $menu->add(new action_menu_link_secondary(
|
||||
new pix_icon('t/user', ''),
|
||||
'Action link example',
|
||||
));
|
||||
{{< / php >}}
|
||||
```
|
||||
|
||||
## Types of items
|
||||
|
||||
@@ -178,7 +210,7 @@ Construct params:
|
||||
|
||||
The following example creates a subpanel using a renderable choicelist instance:
|
||||
|
||||
{{< php >}}
|
||||
```php
|
||||
/** @var core_renderer $output*/
|
||||
$output = $PAGE->get_renderer('core');
|
||||
|
||||
@@ -205,7 +237,7 @@ $menu->add(new core\output\local\action_menu\subpanel(
|
||||
));
|
||||
|
||||
echo $output->render($menu);
|
||||
{{< / php >}}
|
||||
```
|
||||
|
||||
### HTML string
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ echo '<p><strong>Important note:</strong> actions menus are not prepared
|
||||
to be displayed inside iframes. You may need to scroll to see the
|
||||
action menu options.</p>';
|
||||
|
||||
echo $output->heading("Action menu default example", 4);
|
||||
echo $output->heading("Action menu default example", 3);
|
||||
|
||||
$menu = new action_menu();
|
||||
|
||||
@@ -87,11 +87,11 @@ $menu->add($subpanel);
|
||||
$menu->add($basicactionlink);
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">Example of default an action menu</div><div>';
|
||||
echo '<div class="flex-fill">An action menu rendered without customisation</div><div>';
|
||||
echo $OUTPUT->render($menu);
|
||||
echo '</div></div>';
|
||||
|
||||
echo $output->heading("Kebab menu example", 4);
|
||||
echo $output->heading("Kebab menu example", 3);
|
||||
|
||||
$menu = new action_menu();
|
||||
$menu->set_kebab_trigger(get_string('edit'), $output);
|
||||
@@ -106,11 +106,11 @@ $menu->add(new core\output\local\action_menu\subpanel(
|
||||
$menu->add($basicactionlink);
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">Example of kebab menu</div><div>';
|
||||
echo '<div class="flex-fill">An action menu with a kebab menu trigger button</div><div>';
|
||||
echo $OUTPUT->render($menu);
|
||||
echo '</div></div>';
|
||||
|
||||
echo $output->heading("Custom trigger menu example", 4);
|
||||
echo $output->heading("Custom trigger menu examples", 3);
|
||||
|
||||
$menu = new action_menu();
|
||||
$menu->set_menu_trigger(get_string('edit'));
|
||||
@@ -124,11 +124,53 @@ $menu->add(new core\output\local\action_menu\subpanel(
|
||||
$menu->add($basicactionlink);
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">Example of kebab menu</div><div>';
|
||||
echo '<div class="flex-fill">An action menu with a menu trigger button with a custom text label</div><div>';
|
||||
echo $OUTPUT->render($menu);
|
||||
echo '</div></div>';
|
||||
|
||||
echo $output->heading("Primary actions menu example", 4);
|
||||
$links = [
|
||||
$basicactionlink,
|
||||
$basicactionlink,
|
||||
];
|
||||
$moreicon = $OUTPUT->pix_icon('i/moremenu', '');
|
||||
$editicon = $OUTPUT->pix_icon('t/edit', '');
|
||||
|
||||
$menu = new action_menu($links);
|
||||
|
||||
$menu->set_menu_trigger($moreicon);
|
||||
$menu->set_action_label(get_string('moremenu'));
|
||||
$menu->triggerattributes = [
|
||||
'title' => get_string('moremenu'),
|
||||
];
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">An action menu with only an icon for its custom menu trigger button</div>';
|
||||
echo html_writer::div($OUTPUT->render($menu));
|
||||
echo '</div>';
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">An action menu with only an icon for its custom menu trigger button with the caret removed</div>';
|
||||
$menu->triggerextraclasses = 'no-caret';
|
||||
echo html_writer::div($OUTPUT->render($menu));
|
||||
echo '</div>';
|
||||
|
||||
$menu = new action_menu($links);
|
||||
$menu->set_menu_trigger($editicon . ' ' . get_string('edit'));
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">An action menu with an icon and visible text for its custom menu trigger button</div>';
|
||||
echo html_writer::div($OUTPUT->render($menu));
|
||||
echo '</div>';
|
||||
|
||||
$menu = new action_menu($links);
|
||||
$menu->set_menu_trigger($editicon . ' ' . html_writer::span(get_string('edit'), 'sr-only'));
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">An action menu with an icon and visually hidden text for its custom menu trigger button</div>';
|
||||
echo html_writer::div($OUTPUT->render($menu));
|
||||
echo '</div>';
|
||||
|
||||
echo $output->heading("Primary actions menu example", 3);
|
||||
|
||||
$menu = new action_menu();
|
||||
$menu->set_menu_trigger(get_string('edit'));
|
||||
|
||||
@@ -162,7 +162,7 @@ class data_requests_table extends table_sql {
|
||||
*/
|
||||
public function col_userid($data) {
|
||||
$user = $data->foruser;
|
||||
return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
|
||||
return html_writer::link($user->profileurl, $user->fullname);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,7 +183,7 @@ class data_requests_table extends table_sql {
|
||||
*/
|
||||
public function col_requestedby($data) {
|
||||
$user = $data->requestedbyuser;
|
||||
return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
|
||||
return html_writer::link($user->profileurl, $user->fullname);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
}}>
|
||||
<td>{{typename}}</td>
|
||||
<td>{{#userdate}} {{timecreated}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}</td>
|
||||
<td><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></td>
|
||||
<td><a href="{{requestedbyuser.profileurl}}">{{requestedbyuser.fullname}}</a></td>
|
||||
<td>
|
||||
<span class="badge {{statuslabelclass}}" title="{{statuslabeltitle}}">{{statuslabel}}</span>
|
||||
</td>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<h4 class="mt-0 mb-1">
|
||||
<a href="{{foruser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{foruser.fullname}}</a>
|
||||
<a href="{{foruser.profileurl}}">{{foruser.fullname}}</a>
|
||||
</h4>
|
||||
<a href="mailto:{{foruser.email}}">{{foruser.email}}</a>
|
||||
<div class="clearfix mt-1 mb-1">
|
||||
@@ -76,7 +76,7 @@
|
||||
</span>
|
||||
<span class="float-start me-1">
|
||||
<strong>{{#str}}requestbydetail, tool_dataprivacy{{/str}}</strong>
|
||||
<span><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></span>
|
||||
<span><a href="{{requestedbyuser.profileurl}}">{{requestedbyuser.fullname}}</a></span>
|
||||
</span>
|
||||
</div>
|
||||
{{#canreview}}
|
||||
|
||||
@@ -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 |
|
||||
@@ -28,6 +28,7 @@ use renderable;
|
||||
use templatable;
|
||||
use renderer_base;
|
||||
use single_button;
|
||||
use action_link;
|
||||
use stdClass;
|
||||
use moodle_url;
|
||||
use context;
|
||||
@@ -65,6 +66,8 @@ class manage_competency_frameworks_page implements renderable, templatable {
|
||||
* @param context $pagecontext The page context
|
||||
*/
|
||||
public function __construct(context $pagecontext) {
|
||||
global $OUTPUT;
|
||||
|
||||
$this->pagecontext = $pagecontext;
|
||||
|
||||
if (competency_framework::can_manage_context($this->pagecontext)) {
|
||||
@@ -74,10 +77,13 @@ class manage_competency_frameworks_page implements renderable, templatable {
|
||||
'get'
|
||||
);
|
||||
$this->navigation[] = $addpage;
|
||||
$competenciesrepository = new single_button(
|
||||
|
||||
$icon = $OUTPUT->pix_icon('i/externallink', get_string('opensinnewwindow'), 'moodle', ['class' => 'ms-1']);
|
||||
$competenciesrepository = new action_link(
|
||||
new moodle_url('https://moodle.net/search', ['q' => 'competency frameworks']),
|
||||
get_string('competencyframeworksrepository', 'tool_lp'),
|
||||
'get'
|
||||
get_string('competencyframeworksrepository', 'tool_lp') . $icon,
|
||||
null,
|
||||
['target' => '_blank', 'class' => 'btn btn-secondary ms-sm-2'],
|
||||
);
|
||||
$this->navigation[] = $competenciesrepository;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
}
|
||||
}}
|
||||
<span>
|
||||
<a href="{{profileurl}}" title="{{#str}}viewprofile{{/str}}">
|
||||
<a href="{{profileurl}}">
|
||||
<img height="18" src="{{profileimageurlsmall}}" alt="">
|
||||
<span>{{fullname}}</span>
|
||||
{{#hasidentity}}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
@tool @tool_lp @tool_lp_framework
|
||||
Feature: Manage CRUD operations for competencies
|
||||
In order to perform CRUD operations on competencies
|
||||
As a manager
|
||||
I need to be able to create, read, update and delete competencies
|
||||
|
||||
Background:
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| CF1 | CF1 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework | idnumber | description |
|
||||
| C1 | CF1 | C1ID | C1 description |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Competencies > Competency frameworks" in site administration
|
||||
And I click on "CF1 (CF1)" "link"
|
||||
|
||||
@javascript
|
||||
Scenario: Create a new competency
|
||||
# Targets the `CF1` element to make `Add competency` button visible and accessible to avoid ambiguity with the other 'CF1' element on the screen.
|
||||
Given I click on "//span[text()='CF1']" "xpath_element"
|
||||
When I press "Add competency"
|
||||
And I set the field "Name" to "C2"
|
||||
And I set the field "Description" to "C2 description"
|
||||
And I set the field "ID number" to "C2ID"
|
||||
And I press "Save changes"
|
||||
# Access the newly created competencies to ensure that correct information was registered.
|
||||
Then "Competency created" "text" should exist
|
||||
And "C2" "text" should appear after "C1" "text"
|
||||
And I select "C2" of the competency tree
|
||||
And "C2ID" "text" should exist
|
||||
And "C2 description" "text" should exist
|
||||
|
||||
@javascript
|
||||
Scenario: Read a competency
|
||||
When I select "C1" of the competency tree
|
||||
# Confirm that selected competency info displayed matches registered info.
|
||||
Then "C1ID" "text" should exist
|
||||
And "C1 description" "text" should exist
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
# Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit.
|
||||
And I click on "[data-action=edit]" "css_element"
|
||||
# Confirm that the details displayed when accessing edit screen values match registered data.
|
||||
And the field "Name" matches value "C1"
|
||||
And the field "Description" matches value "C1 description"
|
||||
And the field "ID number" matches value "C1ID"
|
||||
|
||||
@javascript
|
||||
Scenario: Update a competency
|
||||
Given I select "C1" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
# Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit.
|
||||
When I click on "[data-action=edit]" "css_element"
|
||||
And I set the field "Name" to "C2"
|
||||
And I set the field "Description" to "C2 description"
|
||||
And I set the field "ID number" to "C2ID"
|
||||
And I press "Save changes"
|
||||
Then "Competency updated" "text" should exist
|
||||
And "C1" "text" should not exist
|
||||
And "C2" "text" should exist
|
||||
And I select "C2" of the competency tree
|
||||
And "C2ID" "text" should exist
|
||||
And "C1ID" "text" should not exist
|
||||
And "C2 description" "text" should exist
|
||||
And "C1 description" "text" should not exist
|
||||
|
||||
@javascript
|
||||
Scenario: Delete a competency
|
||||
Given I select "C1" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
When I click on "Delete" "link"
|
||||
And I click on "Delete" "button" in the "Confirm" "dialogue"
|
||||
# Confirm that "C1" competency was successfully deleted.
|
||||
Then "C1" "text" should not exist
|
||||
@@ -0,0 +1,63 @@
|
||||
@tool @tool_lp @tool_lp_framework
|
||||
Feature: Move and cross-reference competencies
|
||||
In order to move and cross-reference competencies
|
||||
As a manager
|
||||
I need to be open the competency's menu items.
|
||||
|
||||
Background:
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| CF1 | CF1 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework |
|
||||
| C1 | CF1 |
|
||||
| C2 | CF1 |
|
||||
| C3 | CF1 |
|
||||
| C4 | CF1 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Competencies > Competency frameworks" in site administration
|
||||
And I click on "CF1 (CF1)" "link"
|
||||
|
||||
@javascript
|
||||
Scenario: Move a competency using Move up/Move down menu items
|
||||
Given I select "C1" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
When I click on "Move down" "link"
|
||||
Then "C1" "text" should appear after "C2" "text"
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
And I click on "Move up" "link"
|
||||
And "C1" "text" should appear before "C2" "text"
|
||||
|
||||
@javascript
|
||||
Scenario: Move a competency using Relocate menu item
|
||||
Given I select "C3" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
When I click on "Relocate" "link"
|
||||
And I click on "C1" "text" in the "Move competency" "dialogue"
|
||||
And I click on "Move" "button" in the "Move competency" "dialogue"
|
||||
Then "C1" "text" should appear before "C3" "text"
|
||||
And "C3" "text" should appear before "C2" "text"
|
||||
And I select "C3" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
# Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit.
|
||||
And I click on "[data-action=edit]" "css_element"
|
||||
And "C1" "text" should exist
|
||||
And "No parent (top-level competency)" "text" should not exist
|
||||
And I press "Cancel"
|
||||
And "C1" "text" should appear before "C3" "text"
|
||||
And "C3" "text" should appear before "C2" "text"
|
||||
|
||||
@javascript
|
||||
Scenario: Cross-reference a competency
|
||||
Given I select "C1" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
When I click on "Add cross-referenced competency" "link"
|
||||
And I click on "C2" "text" in the "Competency picker" "dialogue"
|
||||
And I click on "Add" "button" in the "Competency picker" "dialogue"
|
||||
Then "Cross-referenced competencies:" "text" should exist
|
||||
And I should see "C2 cmp2"
|
||||
@@ -16,16 +16,10 @@ Feature: Manage plan workflow
|
||||
| usermanageownplan | User manage own plan role | user |
|
||||
| manageplan | Manager all plans role | manager |
|
||||
And the following "role capabilities" exist:
|
||||
| role | moodle/competency:planmanageowndraft | moodle/competency:planmanageown |
|
||||
| usermanageowndraftplan | allow | |
|
||||
| usermanageownplan | allow | allow |
|
||||
| manageplan | allow | allow |
|
||||
And the following "role capability" exists:
|
||||
| role | manageplan |
|
||||
| moodle/competency:planmanage | allow |
|
||||
| moodle/competency:planview | allow |
|
||||
| moodle/competency:planreview | allow |
|
||||
| moodle/competency:planrequestreview | allow |
|
||||
| role | moodle/competency:planmanageowndraft | moodle/competency:planmanageown | moodle/competency:planmanage | moodle/competency:planview | moodle/competency:planreview | moodle/competency:planrequestreview |
|
||||
| usermanageowndraftplan | allow | | | | | |
|
||||
| usermanageownplan | allow | allow | | | | |
|
||||
| manageplan | allow | allow | allow | allow | allow | allow |
|
||||
And the following "role assigns" exist:
|
||||
| user | role | contextlevel | reference |
|
||||
| user1 | usermanageowndraftplan | System | |
|
||||
@@ -53,8 +47,7 @@ Feature: Manage plan workflow
|
||||
| lp | System | 1 | my-index | content |
|
||||
|
||||
Scenario: User can manages his own plan draft
|
||||
Given I log in as "user1"
|
||||
And I follow "Profile" in the user menu
|
||||
Given I am on the "user1" "user > profile" page logged in as user1
|
||||
When I follow "Learning plans"
|
||||
Then I should see "List of learning plans"
|
||||
And I should see "Test-Plan1"
|
||||
@@ -63,11 +56,9 @@ Feature: Manage plan workflow
|
||||
And I should see "Waiting for review"
|
||||
And I click on "Cancel review" of edit menu in the "Test-Plan1" row
|
||||
And I should see "Draft"
|
||||
And I log out
|
||||
|
||||
Scenario: User can manages his own plan
|
||||
Given I log in as "user2"
|
||||
And I follow "Profile" in the user menu
|
||||
Given I am on the "user2" "user > profile" page logged in as user2
|
||||
When I follow "Learning plans"
|
||||
Then I should see "List of learning plans"
|
||||
And I should see "Test-Plan2"
|
||||
@@ -82,11 +73,11 @@ Feature: Manage plan workflow
|
||||
And I should see "Active"
|
||||
And I click on "Complete this learning plan" of edit menu in the "Test-Plan2" row
|
||||
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
|
||||
And I wait until the page is ready
|
||||
And I should see "Complete"
|
||||
And I click on "Reopen this learning plan" of edit menu in the "Test-Plan2" row
|
||||
And I click on "Reopen this learning plan" "button" in the "Confirm" "dialogue"
|
||||
And I should see "Active"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager can see learning plan with status waiting for review
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -96,7 +87,6 @@ Feature: Manage plan workflow
|
||||
When I log in as "manager1"
|
||||
Then I should see "Test-Plan3"
|
||||
And I should not see "Test-Plan4"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager can start review of learning plan with status waiting for review
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -108,7 +98,6 @@ Feature: Manage plan workflow
|
||||
And I should see "Test-Plan3"
|
||||
When I follow "Start review"
|
||||
Then I should see "In review"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager can reject a learning plan with status in review
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -121,7 +110,6 @@ Feature: Manage plan workflow
|
||||
And I should see "In review"
|
||||
When I follow "Finish review"
|
||||
Then I should see "Draft"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager can accept a learning plan with status in review
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -134,7 +122,6 @@ Feature: Manage plan workflow
|
||||
And I should see "In review"
|
||||
When I follow "Make active"
|
||||
Then I should see "Active"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager send back to draft an active learning plan
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -150,7 +137,6 @@ Feature: Manage plan workflow
|
||||
And I follow "Learning plans"
|
||||
Then I should see "Draft"
|
||||
And I should not see "Active"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager change an active learning plan to completed
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -169,7 +155,6 @@ Feature: Manage plan workflow
|
||||
And I follow "Learning plans"
|
||||
Then I should see "Complete"
|
||||
And I should not see "Active"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager reopen a complete learning plan
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -187,4 +172,67 @@ Feature: Manage plan workflow
|
||||
And I follow "Learning plans"
|
||||
Then I should see "Active"
|
||||
And I should not see "Complete"
|
||||
And I log out
|
||||
|
||||
Scenario: Student learning plan derived from templates can be completed
|
||||
Given the following "core_competency > templates" exist:
|
||||
| shortname |
|
||||
| LPT1 |
|
||||
And the following "core_competency > template_competencies" exist:
|
||||
| template | competency |
|
||||
| LPT1 | Test-Comp1 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
# Select 1 user to assign to create learning plans for using template.
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
And I set the field "Select users" to "user1"
|
||||
And I press "Create learning plans"
|
||||
And I click on "LPT1" "link" in the "LPT1" "table_row"
|
||||
When I click on "Complete this learning plan" "link"
|
||||
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
|
||||
# Add a short wait to ensure the page has loaded before checking that "Complete" "text" exists.
|
||||
And I wait until the page is ready
|
||||
# Confirm that student's learning plan template is marked as Completed.
|
||||
Then "Complete" "text" should exist
|
||||
And "Reopen this learning plan" "link" should exist
|
||||
|
||||
Scenario: Learning plan template updates are not reflected on plans already completed
|
||||
Given the following "core_competency > templates" exist:
|
||||
| shortname |
|
||||
| LPT1 |
|
||||
And the following "core_competency > template_competencies" exist:
|
||||
| template | competency |
|
||||
| LPT1 | Test-Comp1 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
# Select 2 users to assign to create learning plans for using template.
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
And I set the field "Select users" to "user1"
|
||||
And I press "Create learning plans"
|
||||
And I set the field "Select users" to "user2"
|
||||
And I press "Create learning plans"
|
||||
# Complete the learning plan for User 1.
|
||||
And I click on "LPT1" "link" in the "User 1" "table_row"
|
||||
And I click on "Complete this learning plan" "link"
|
||||
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
|
||||
# Add another competency to the learning plan template.
|
||||
And the following "core_competency > template_competencies" exist:
|
||||
| template | competency |
|
||||
| LPT1 | Test-Comp2 |
|
||||
# Navigate back to the list of Learning plan templates in order to access User 1's learning plan.
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
# Confirm that only the first competency is reflected on User 1's learning plan since it's already completed.
|
||||
When I click on "LPT1" "link" in the "User 1" "table_row"
|
||||
Then "Test-Comp2" "link" should not exist
|
||||
And "Test-Comp2" "text" should not exist
|
||||
And "Test-Comp1" "link" should exist
|
||||
And "Test-Comp1" "text" should exist
|
||||
# Navigate back to list of Learning plan templates in order to access User 2's learning plan.
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
# Confirm that both competencies are reflected on User 2's learning plan since it's not yet completed.
|
||||
And I click on "LPT1" "link" in the "User 2" "table_row"
|
||||
And "Test-Comp2" "link" should exist
|
||||
And "Test-Comp2" "text" should exist
|
||||
And "Test-Comp1" "link" should exist
|
||||
And "Test-Comp1" "text" should exist
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
@tool @tool_lp @core_cohort
|
||||
Feature: Cohorts can be synchronized with learning plans
|
||||
In order to create learning plans for cohort members
|
||||
As an admin
|
||||
I need to be able to synchronise cohorts with learning plans
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| user1 | User | One | user1@example.com |
|
||||
| user2 | User | Two | user2@example.com |
|
||||
And the following "cohorts" exist:
|
||||
| name | idnumber |
|
||||
| Cohort 1 | CH1 |
|
||||
And the following "cohort members" exist:
|
||||
| user | cohort |
|
||||
| user1 | CH1 |
|
||||
| user2 | CH1 |
|
||||
And the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| CF1 | CF1 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework | idnumber |
|
||||
| C1 | CF1 | C1 |
|
||||
And the following "core_competency > templates" exist:
|
||||
| shortname |
|
||||
| LPT1 |
|
||||
And the following "core_competency > template_competencies" exist:
|
||||
| template | competency |
|
||||
| LPT1 | C1 |
|
||||
|
||||
@javascript
|
||||
Scenario: Cohorts can be synchronised with learning plans
|
||||
Given I log in as "admin"
|
||||
# Navigate to the list of learning plan templates in order to add cohorts.
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
When I click on "Add cohorts to sync" of edit menu in the "LPT1" row
|
||||
And I set the field "Select cohorts to sync" to "Cohort 1"
|
||||
And I press "Add cohorts"
|
||||
And I wait until the page is ready
|
||||
# Confirm that 2 learning plans were created for members of the cohort.
|
||||
Then "2 learning plans were created" "text" should exist
|
||||
# Confirm current screen is still "Cohorts synced to this learning plan template screen"
|
||||
And "Cohorts synced to this learning plan template" "text" should exist
|
||||
# Confirm that the cohort is now added to the learning plan template.
|
||||
And the following should exist in the "generaltable" table:
|
||||
| Name | Cohort ID |
|
||||
| Cohort 1 | CH1 |
|
||||
# Navigate back to the list of learning plan templates to view updated list.
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
# Confirm that the added cohort and learning plans are now reflected on the list of Learning plan templates.
|
||||
And the following should exist in the "generaltable" table:
|
||||
| Name | Category | Cohorts | Learning plans |
|
||||
| LPT1 | System | 1 | 2 |
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
# Confirm that learning plans were created for all cohort members.
|
||||
And the following should exist in the "generaltable" table:
|
||||
| Name | First name | Email address |
|
||||
| LPT1 | User One | user1@example.com |
|
||||
| LPT1 | User Two | user2@example.com |
|
||||
@@ -50,7 +50,7 @@ class manager {
|
||||
],
|
||||
'tool_mfa' => [
|
||||
'guidance',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -441,6 +441,21 @@ class manager {
|
||||
$url = new \moodle_url($url);
|
||||
}
|
||||
|
||||
// Admin not setup.
|
||||
if (!empty($CFG->adminsetuppending)) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Honor prevent_redirect.
|
||||
if ($preventredirect) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Login as.
|
||||
if (\core\session\manager::is_loggedinas()) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Check for pluginfile.php urls.
|
||||
$pluginfileurl = new \moodle_url('/pluginfile.php');
|
||||
if ($url->compare($pluginfileurl)) {
|
||||
@@ -485,22 +500,6 @@ class manager {
|
||||
}
|
||||
}
|
||||
|
||||
// Admin not setup.
|
||||
if (!empty($CFG->adminsetuppending)) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Initial installation.
|
||||
// We get this for free from get_plugins_with_function.
|
||||
|
||||
// Upgrade check.
|
||||
// We get this for free from get_plugins_with_function.
|
||||
|
||||
// Honor prevent_redirect.
|
||||
if ($preventredirect) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// User not properly setup.
|
||||
if (user_not_fully_set_up($USER)) {
|
||||
return self::NO_REDIRECT;
|
||||
@@ -516,11 +515,6 @@ class manager {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Login as.
|
||||
if (\core\session\manager::is_loggedinas()) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Site policy.
|
||||
if (isset($USER->policyagreed) && !$USER->policyagreed) {
|
||||
$manager = new \core_privacy\local\sitepolicy\manager();
|
||||
@@ -538,11 +532,8 @@ class manager {
|
||||
}
|
||||
|
||||
// WS/AJAX check.
|
||||
// Prevents any potential bypassing of multi-factor authentication.
|
||||
if (WS_SERVER || AJAX_SCRIPT) {
|
||||
if (isset($SESSION->mfa_pending) && !empty($SESSION->mfa_pending)) {
|
||||
// Allow AJAX and WS, but never from auth.php.
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
return self::REDIRECT_EXCEPTION;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,3 @@ created,tool_mfa
|
||||
createdfromip,tool_mfa
|
||||
lastverified,tool_mfa
|
||||
revoke,tool_mfa
|
||||
setup,tool_mfa
|
||||
|
||||
@@ -150,6 +150,7 @@ $string['settings:redir_exclusions'] = 'URLS which should not redirect the MFA c
|
||||
$string['settings:redir_exclusions_help'] = 'Each new line is a relative URL from the siteroot for which the MFA check will not redirect from';
|
||||
$string['settings:weight'] = 'Factor weight';
|
||||
$string['settings:weight_help'] = 'The weight of this factor if passed. A user needs at least 100 points to log in.';
|
||||
$string['setup'] = 'Setup';
|
||||
$string['setupfactor'] = 'Set up factor';
|
||||
$string['setupfactorbuttonadditional'] = 'Add additional factor';
|
||||
$string['setuprequired'] = 'User setup';
|
||||
@@ -180,4 +181,3 @@ $string['created'] = 'Created';
|
||||
$string['createdfromip'] = 'Created from IP';
|
||||
$string['lastverified'] = 'Last verified';
|
||||
$string['revoke'] = 'Revoke';
|
||||
$string['setup'] = 'Setup';
|
||||
|
||||
@@ -810,7 +810,7 @@ class api {
|
||||
$credentials[] = ['type' => 'airnotifieraccesskey', 'value' => $CFG->airnotifieraccesskey];
|
||||
}
|
||||
if (\core\hub\registration::is_registered()) {
|
||||
$credentials[] = ['type' => 'siteid', 'value' => $CFG->siteidentifier];
|
||||
$credentials[] = ['type' => 'siteid', 'value' => \core\hub\registration::get_secret()];
|
||||
}
|
||||
// Generate a hash key for validating that the request is coming from this site via WS.
|
||||
$key = complex_random_string(32);
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
namespace tool_mobile;
|
||||
|
||||
use core\session\utility\cookie_helper;
|
||||
use core\hook\output\extend_url;
|
||||
use html_writer;
|
||||
use moodle_url;
|
||||
use tool_mobile\local\hooks\before_extend_ios_app_banner;
|
||||
|
||||
/**
|
||||
* Allows plugins to add any elements to the footer.
|
||||
@@ -36,22 +39,39 @@ class hook_callbacks {
|
||||
\core\hook\output\before_standard_head_html_generation $hook,
|
||||
): void {
|
||||
global $CFG, $PAGE;
|
||||
// Smart App Banners meta tag is only displayed if mobile services are enabled and configured.
|
||||
if (!empty($CFG->enablemobilewebservice)) {
|
||||
$mobilesettings = get_config('tool_mobile');
|
||||
if (!empty($mobilesettings->enablesmartappbanners)) {
|
||||
if (!empty($mobilesettings->iosappid)) {
|
||||
$hook->add_html(
|
||||
'<meta name="apple-itunes-app" content="app-id=' . s($mobilesettings->iosappid) . ', ' .
|
||||
'app-argument=' . $PAGE->url->out() . '"/>'
|
||||
);
|
||||
}
|
||||
// Only emit mobile app metadata when mobile services are enabled + configured.
|
||||
if (empty($CFG->enablemobilewebservice)) {
|
||||
return;
|
||||
}
|
||||
$mobilesettings = get_config('tool_mobile');
|
||||
if (empty($mobilesettings->enablesmartappbanners)) {
|
||||
return;
|
||||
}
|
||||
// IOS with hook-based app id and argument augmentation.
|
||||
if (!empty($mobilesettings->iosappid)) {
|
||||
$appid = (string)$mobilesettings->iosappid;
|
||||
$appargument = $PAGE->url->out();
|
||||
// Hook to allow modification of ios smart app banner fields.
|
||||
$ioshook = new before_extend_ios_app_banner($appid, $appargument);
|
||||
\core\di::get(\core\hook\manager::class)->dispatch($ioshook);
|
||||
$appid = $ioshook->get_appid();
|
||||
$appargument = $ioshook->get_appargument();
|
||||
// Add the meta tag.
|
||||
$hook->add_html(
|
||||
'<meta name="apple-itunes-app" content="app-id=' . s($appid) .
|
||||
', app-argument=' . s($appargument) . '"/>'
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($mobilesettings->androidappid)) {
|
||||
$mobilemanifesturl = "$CFG->wwwroot/$CFG->admin/tool/mobile/mobile.webmanifest.php";
|
||||
$hook->add_html('<link rel="manifest" href="' . $mobilemanifesturl . '" />');
|
||||
}
|
||||
}
|
||||
// Android with hook-based URL augmentation.
|
||||
if (!empty($mobilesettings->androidappid)) {
|
||||
$url = new moodle_url('/admin/tool/mobile/mobile.webmanifest.php');
|
||||
$urlhook = new extend_url($url);
|
||||
\core\di::get(\core\hook\manager::class)->dispatch($urlhook);
|
||||
$url = $urlhook->get_url();
|
||||
|
||||
// Add the link tag.
|
||||
$hook->add_html('<link rel="manifest" href="' . $url->out(false) . '" />');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?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_mobile\local\hooks;
|
||||
|
||||
/**
|
||||
* Allow adjustment of ios smart app banner fields.
|
||||
*
|
||||
* @package tool_mobile
|
||||
* @copyright 2025 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
#[\core\attribute\label('Allow adjustment of ios smart app banner fields')]
|
||||
#[\core\attribute\tags('mobile')]
|
||||
final class before_extend_ios_app_banner {
|
||||
/**
|
||||
* Create a new instance of the hook.
|
||||
*
|
||||
* @param string $appid The app id for the ios smart banner
|
||||
* @param string $appargument The app argument for the ios smart banner
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var string $appid The app id for the ios smart banner */
|
||||
private string $appid,
|
||||
/** @var string $appargument The app argument for the ios smart banner */
|
||||
private string $appargument,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appid.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_appid(): string {
|
||||
return $this->appid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the appid.
|
||||
*
|
||||
* @param string $appid
|
||||
*/
|
||||
public function set_appid(string $appid): void {
|
||||
$this->appid = $appid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appargument.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_appargument(): string {
|
||||
return $this->appargument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the appargument.
|
||||
*
|
||||
* @param string $appargument
|
||||
*/
|
||||
public function set_appargument(string $appargument): void {
|
||||
$this->appargument = $appargument;
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,13 @@
|
||||
*/
|
||||
|
||||
define('NO_DEBUG_DISPLAY', true);
|
||||
define('NO_MOODLE_COOKIES', true);
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
|
||||
header('Content-Type: application/json; charset: utf-8');
|
||||
header('Cache-Control: public, max-age=' . HOURSECS . ', no-transform');
|
||||
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + HOURSECS) . ' GMT');
|
||||
|
||||
$mobilesettings = get_config('tool_mobile');
|
||||
// Display manifest contents only if all the required conditions are met.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -52,9 +52,9 @@ if ($listurl) {
|
||||
}
|
||||
|
||||
// Add a new nav item to make $listurl clickable for the Boost theme.
|
||||
if (isset($script)) {
|
||||
$lastrunnav = get_string('lastrun', 'tool_profiling');
|
||||
$PAGE->navbar->add($lastrunnav);
|
||||
if (isset($script) || isset($runid)) {
|
||||
$detailnav = get_string('detailview', 'tool_profiling');
|
||||
$PAGE->navbar->add($detailnav);
|
||||
}
|
||||
|
||||
// Header
|
||||
|
||||
@@ -31,6 +31,7 @@ $string['cannotfindanyrunforurl'] = 'Sorry, cannot find any profiling run for th
|
||||
$string['cannotfindanyrunforrunid'] = 'Sorry, cannot find the \'{$a}\' profiling run';
|
||||
$string['comment'] = 'Comment';
|
||||
$string['cputime'] = 'CPU time';
|
||||
$string['detailview'] = 'Summary of run';
|
||||
$string['differencesbetween2runsof'] = 'Differences between 2 runs of {$a}';
|
||||
$string['executiontime'] = 'Execution time';
|
||||
$string['export'] = 'Export';
|
||||
|
||||
@@ -25,7 +25,7 @@ Feature: An admin can update courses enrolments using a CSV file
|
||||
When I click on "Upload courses" "button"
|
||||
Then I should see "Course updated"
|
||||
And I am on the "Course 1" "enrolment methods" page
|
||||
And "Disable" "icon" should exist in the "Guest access" "table_row"
|
||||
And "Disable" "link" should exist in the "Guest access" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Creating enrolment method by disabling it
|
||||
@@ -42,7 +42,7 @@ Feature: An admin can update courses enrolments using a CSV file
|
||||
When I click on "Upload courses" "button"
|
||||
Then I should see "Course updated"
|
||||
And I am on the "Course 1" "enrolment methods" page
|
||||
And "Enable" "icon" should exist in the "Guest access" "table_row"
|
||||
And "Enable" "link" should exist in the "Guest access" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Enabling enrolment method
|
||||
@@ -55,13 +55,13 @@ Feature: An admin can update courses enrolments using a CSV file
|
||||
When I click on "Upload courses" "button"
|
||||
Then I should see "Course updated"
|
||||
And I am on the "Course 1" "enrolment methods" page
|
||||
And "Disable" "icon" should exist in the "Guest access" "table_row"
|
||||
And "Disable" "link" should exist in the "Guest access" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Disable an enrolment method
|
||||
Given I am on the "Course 1" "enrolment methods" page
|
||||
And I click on "Enable" "link" in the "Guest access" "table_row"
|
||||
And "Disable" "icon" should exist in the "Guest access" "table_row"
|
||||
And "Disable" "link" should exist in the "Guest access" "table_row"
|
||||
And I navigate to "Courses > Upload courses" in site administration
|
||||
And I upload "admin/tool/uploadcourse/tests/fixtures/enrolment_disable.csv" file to "File" filemanager
|
||||
And I set the field "Upload mode" to "Only update existing courses"
|
||||
@@ -71,7 +71,7 @@ Feature: An admin can update courses enrolments using a CSV file
|
||||
When I click on "Upload courses" "button"
|
||||
Then I should see "Course updated"
|
||||
And I am on the "Course 1" "enrolment methods" page
|
||||
And "Enable" "icon" should exist in the "Guest access" "table_row"
|
||||
And "Enable" "link" should exist in the "Guest access" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Delete an enrolment method
|
||||
|
||||
@@ -910,8 +910,8 @@ class process {
|
||||
}
|
||||
|
||||
if ($this->get_bulk() == UU_BULK_UPDATED or $this->get_bulk() == UU_BULK_ALL) {
|
||||
if (!in_array($user->id, $SESSION->bulk_users)) {
|
||||
$SESSION->bulk_users[] = $user->id;
|
||||
if (!array_key_exists($user->id, $SESSION->bulk_users)) {
|
||||
$SESSION->bulk_users[$user->id] = $user->id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -924,8 +924,8 @@ class process {
|
||||
$this->usersuptodate++;
|
||||
|
||||
if ($this->get_bulk() == UU_BULK_ALL) {
|
||||
if (!in_array($user->id, $SESSION->bulk_users)) {
|
||||
$SESSION->bulk_users[] = $user->id;
|
||||
if (!array_key_exists($user->id, $SESSION->bulk_users)) {
|
||||
$SESSION->bulk_users[$user->id] = $user->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1059,8 +1059,8 @@ class process {
|
||||
\context_user::instance($user->id);
|
||||
|
||||
if ($this->get_bulk() == UU_BULK_NEW or $this->get_bulk() == UU_BULK_ALL) {
|
||||
if (!in_array($user->id, $SESSION->bulk_users)) {
|
||||
$SESSION->bulk_users[] = $user->id;
|
||||
if (!array_key_exists($user->id, $SESSION->bulk_users)) {
|
||||
$SESSION->bulk_users[$user->id] = $user->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
@@ -92,10 +92,11 @@ class summarise_text extends external_api {
|
||||
// Send the action to the AI manager.
|
||||
$manager = \core\di::get(\core_ai\manager::class);
|
||||
$response = $manager->process_action($action);
|
||||
$generatedcontent = $response->get_response_data()['generatedcontent'] ?? '';
|
||||
// Return the response.
|
||||
return [
|
||||
'success' => $response->get_success(),
|
||||
'generatedcontent' => $response->get_response_data()['generatedcontent'] ?? '',
|
||||
'generatedcontent' => \core_external\util::format_text($generatedcontent, FORMAT_PLAIN, $contextid)[0],
|
||||
'finishreason' => $response->get_response_data()['finishreason'] ?? '',
|
||||
'errorcode' => $response->get_errorcode(),
|
||||
'error' => $response->get_errormessage(),
|
||||
|
||||
@@ -370,7 +370,7 @@ class core_auth_external extends external_api {
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function resend_confirmation_email($username, $password, $redirect = '') {
|
||||
global $PAGE;
|
||||
global $PAGE, $CFG;
|
||||
|
||||
$warnings = array();
|
||||
$params = self::validate_parameters(
|
||||
@@ -387,20 +387,27 @@ class core_auth_external extends external_api {
|
||||
$username = trim(core_text::strtolower($params['username']));
|
||||
$password = $params['password'];
|
||||
|
||||
$user = core_user::get_user_by_username($username);
|
||||
|
||||
if (!empty($user) && $user->confirmed) {
|
||||
if (!empty($CFG->protectusernames)) {
|
||||
throw new moodle_exception('invalidlogin');
|
||||
}
|
||||
throw new moodle_exception('alreadyconfirmed');
|
||||
}
|
||||
|
||||
if (is_restored_user($username)) {
|
||||
if (!empty($CFG->protectusernames)) {
|
||||
throw new moodle_exception('invalidlogin');
|
||||
}
|
||||
throw new moodle_exception('restoredaccountresetpassword', 'webservice');
|
||||
}
|
||||
|
||||
$user = authenticate_user_login($username, $password);
|
||||
|
||||
if (empty($user)) {
|
||||
throw new moodle_exception('invalidlogin');
|
||||
}
|
||||
|
||||
if ($user->confirmed) {
|
||||
throw new moodle_exception('alreadyconfirmed');
|
||||
}
|
||||
|
||||
// Check if we should redirect the user once the user is confirmed.
|
||||
$confirmationurl = null;
|
||||
if (!empty($params['redirect'])) {
|
||||
|
||||
@@ -938,6 +938,7 @@ class auth_plugin_ldap extends auth_plugin_base {
|
||||
} catch (Exception $e) {
|
||||
print_string('invaliduserexception', 'auth_ldap', print_r($user, true) . $e->getMessage());
|
||||
$errors++;
|
||||
$transaction->allow_commit();
|
||||
continue;
|
||||
}
|
||||
echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n";
|
||||
|
||||
@@ -104,6 +104,7 @@ class auth_plugin_lti extends \auth_plugin_base {
|
||||
* @param int $provisioningmode the desired account provisioning mode, which controls the auth flow for unbound users.
|
||||
* @param array $legacyconsumersecrets an array of secrets used by the legacy consumer if a migration claim exists.
|
||||
* @throws coding_exception if the specified provisioning mode is invalid.
|
||||
* @throws \core\exception\moodle_exception if user authentication fails.
|
||||
*/
|
||||
public function complete_login(array $launchdata, moodle_url $returnurl, int $provisioningmode,
|
||||
array $legacyconsumersecrets = []): void {
|
||||
@@ -112,6 +113,19 @@ class auth_plugin_lti extends \auth_plugin_base {
|
||||
if ($this->get_user_binding($launchdata['iss'], $launchdata['sub'])) {
|
||||
$user = $this->find_or_create_user_from_launch($launchdata);
|
||||
|
||||
if ($user->suspended) {
|
||||
$failurereason = AUTH_LOGIN_SUSPENDED;
|
||||
$event = \core\event\user_login_failed::create([
|
||||
'userid' => $user->id,
|
||||
'other' => [
|
||||
'username' => $user->username,
|
||||
'reason' => $failurereason
|
||||
]
|
||||
]);
|
||||
$event->trigger();
|
||||
throw new \core\exception\moodle_exception('invalidlogin', 'core');
|
||||
}
|
||||
|
||||
if (isloggedin()) {
|
||||
// If a different user is currently logged in, authenticate the linked user instead.
|
||||
global $USER;
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
require_once('../../config.php');
|
||||
|
||||
$issuerid = required_param('id', PARAM_INT);
|
||||
$wantsurl = new moodle_url(optional_param('wantsurl', '', PARAM_URL));
|
||||
$wantsurl = new moodle_url(optional_param('wantsurl', '', PARAM_LOCALURL));
|
||||
|
||||
$PAGE->set_context(context_system::instance());
|
||||
$PAGE->set_url(new moodle_url('/auth/oauth2/login.php', ['id' => $issuerid]));
|
||||
|
||||
@@ -96,13 +96,15 @@ Feature: Test if the login form provides the correct feedback
|
||||
And I follow "Log in"
|
||||
Then the focused element is "Password" "field"
|
||||
|
||||
@accessibility
|
||||
Scenario: Test the login page focus after error feature
|
||||
Given I follow "Log in"
|
||||
And I set the field "Username" to "admin"
|
||||
And I set the field "Password" to "wrongpassword"
|
||||
And I press "Log in"
|
||||
And I press the tab key
|
||||
And I wait until the page is ready
|
||||
Then the focused element is "Username" "field"
|
||||
And the page should meet accessibility standards with "best-practice" extra tests
|
||||
|
||||
Scenario: Display the password visibility toggle icon
|
||||
Given the following config values are set as admin:
|
||||
|
||||
+9
@@ -182,6 +182,7 @@ final class external_test extends externallib_advanced_testcase {
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
set_config('protectusernames', 0);
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts.
|
||||
$this->expectException('\moodle_exception');
|
||||
$this->expectExceptionMessage('error/invalidlogin');
|
||||
@@ -205,6 +206,7 @@ final class external_test extends externallib_advanced_testcase {
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
set_config('protectusernames', 0);
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts.
|
||||
$this->expectException('\moodle_exception');
|
||||
$this->expectExceptionMessage('error/invalidlogin');
|
||||
@@ -235,6 +237,13 @@ final class external_test extends externallib_advanced_testcase {
|
||||
$result = external_api::clean_returnvalue(core_auth_external::confirm_user_returns(), $result);
|
||||
$this->assertTrue($result['success']);
|
||||
|
||||
// Keep protectusernames enabled so the call returns invalidlogin exception.
|
||||
$this->expectException('\moodle_exception');
|
||||
$this->expectExceptionMessage('error/invalidlogin');
|
||||
core_auth_external::resend_confirmation_email($username, $password);
|
||||
|
||||
// Now disable protectusernames and expect an exception.
|
||||
set_config('protectusernames', 0);
|
||||
$this->expectException('\moodle_exception');
|
||||
$this->expectExceptionMessage('error/alreadyconfirmed');
|
||||
core_auth_external::resend_confirmation_email($username, $password);
|
||||
|
||||
@@ -73,7 +73,15 @@ class auth_plugin_webservice extends auth_plugin_base {
|
||||
function user_login_webservice($username, $password) {
|
||||
global $CFG, $DB;
|
||||
// special web service login
|
||||
if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) {
|
||||
$user = $DB->get_record('user', [
|
||||
'username' => $username,
|
||||
'auth' => 'webservice',
|
||||
'deleted' => '0',
|
||||
'suspended' => '0',
|
||||
'mnethostid' => $CFG->mnet_localhost_id,
|
||||
]);
|
||||
|
||||
if ($user) {
|
||||
return validate_internal_user_password($user, $password);
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -568,8 +568,7 @@ abstract class restore_qtype_plugin extends restore_plugin {
|
||||
|
||||
foreach ($excludefields as $excludefield) {
|
||||
$pathparts = explode('/', ltrim($excludefield, '/'));
|
||||
$data = $questiondata;
|
||||
self::unset_excluded_fields($data, $pathparts);
|
||||
$questiondata = self::unset_excluded_fields($questiondata, $pathparts);
|
||||
}
|
||||
|
||||
return $questiondata;
|
||||
@@ -581,25 +580,60 @@ abstract class restore_qtype_plugin extends restore_plugin {
|
||||
* If any of the elements in the path is an array, this is called recursively on each element in the array to unset fields
|
||||
* in each child of the array.
|
||||
*
|
||||
* @param stdClass|array $data The questiondata object, or a subsection of it.
|
||||
* @param stdClass|array $data The questiondata structure, or a subsection of it.
|
||||
* @param array $pathparts The remaining elements in the path to the excluded field.
|
||||
* @return void
|
||||
* @return stdClass|array The $data structure with excluded fields removed.
|
||||
*/
|
||||
private static function unset_excluded_fields(stdClass|array $data, array $pathparts): void {
|
||||
private static function unset_excluded_fields(stdClass|array $data, array $pathparts): stdClass|array {
|
||||
$element = array_shift($pathparts);
|
||||
if (!isset($data->{$element})) {
|
||||
// This element is not present in the data structure, nothing to unset.
|
||||
return;
|
||||
$unset = false;
|
||||
// Get the current element from the data structure.
|
||||
if (is_object($data)) {
|
||||
if (!property_exists($data, $element)) {
|
||||
// This element is not present in the data structure, nothing to unset.
|
||||
return $data;
|
||||
}
|
||||
$dataelement = $data->{$element};
|
||||
} else { // It's an array.
|
||||
if (!array_key_exists($element, $data)) {
|
||||
return $data;
|
||||
}
|
||||
$dataelement = $data[$element];
|
||||
}
|
||||
if (is_object($data->{$element})) {
|
||||
self::unset_excluded_fields($data->{$element}, $pathparts);
|
||||
} else if (is_array($data->{$element})) {
|
||||
foreach ($data->{$element} as $item) {
|
||||
self::unset_excluded_fields($item, $pathparts);
|
||||
// Check if we need to recur, or unset this element.
|
||||
if (is_object($dataelement)) {
|
||||
$dataelement = self::unset_excluded_fields($dataelement, $pathparts);
|
||||
} else if (is_array($dataelement)) {
|
||||
foreach ($dataelement as $key => $item) {
|
||||
if (is_object($item) || is_array($item)) {
|
||||
// This is an array of objects or arrays, recur.
|
||||
$dataelement[$key] = self::unset_excluded_fields($item, $pathparts);
|
||||
} else {
|
||||
// This is an associative array of values, check if they should be removed.
|
||||
$subelement = reset($pathparts);
|
||||
if ($key == $subelement) {
|
||||
unset($dataelement[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (empty($pathparts)) {
|
||||
// This is the last element of the path and it's a scalar value, unset it.
|
||||
unset($data->{$element});
|
||||
$unset = true;
|
||||
}
|
||||
// Write the modified element back to the data structure, or unset it.
|
||||
if (is_object($data)) {
|
||||
if ($unset) {
|
||||
unset($data->{$element});
|
||||
} else {
|
||||
$data->{$element} = $dataelement;
|
||||
}
|
||||
} else {
|
||||
if ($unset) {
|
||||
unset($data[$element]);
|
||||
} else {
|
||||
$data[$element] = $dataelement;
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5285,6 +5285,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);
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<?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_backup;
|
||||
|
||||
/**
|
||||
* Tests for question type restore methods
|
||||
*
|
||||
* @package core_backup
|
||||
* @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
|
||||
* @covers \restore_qtype_plugin
|
||||
*/
|
||||
final class restore_qtype_plugin_test extends \basic_testcase {
|
||||
/**
|
||||
* All default and specified fields should be removed from the provided data structure.
|
||||
*/
|
||||
public function test_remove_excluded_question_data(): void {
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/backup/moodle2/restore_plugin.class.php');
|
||||
require_once($CFG->dirroot . '/backup/moodle2/restore_qtype_plugin.class.php');
|
||||
$data = (object) [
|
||||
// Default excluded fields should be removed.
|
||||
'id' => 1,
|
||||
'createdby' => 2,
|
||||
'modifiedby' => 3,
|
||||
// This field is not specified for removal, it should remain.
|
||||
'questiontext' => 'Some question text',
|
||||
// Excluded paths that address an array should operate on all items in the array.
|
||||
'hints' => [
|
||||
(object) [
|
||||
'id' => 4,
|
||||
'questionid' => 1,
|
||||
// This field is not specified for removal.
|
||||
'text' => 'Lorem ipsum',
|
||||
],
|
||||
(object) [
|
||||
'id' => 5,
|
||||
'questionid' => 1,
|
||||
'text' => 'Lorem ipsum',
|
||||
],
|
||||
],
|
||||
'options' => [ // This is an array of arrays, rather than an array of objects. It should be handled the same.
|
||||
[
|
||||
'id' => 6,
|
||||
'questionid' => 1,
|
||||
// This field is not specified for removal.
|
||||
'option' => true,
|
||||
],
|
||||
[
|
||||
'id' => 7,
|
||||
'questionid' => 1,
|
||||
'option' => false,
|
||||
],
|
||||
[
|
||||
'id' => 8,
|
||||
'questionid' => 1,
|
||||
'option' => false,
|
||||
],
|
||||
],
|
||||
'custom1' => 'Some custom text',
|
||||
// This field is not specified for removal.
|
||||
'custom2' => 'Some custom text2',
|
||||
// Fields specified for removal should be removed even if they contain null values.
|
||||
'custom3' => null,
|
||||
'customarray' => [
|
||||
(object) [
|
||||
// Null values should also be removed.
|
||||
'id' => null,
|
||||
// This field is not specified for removal.
|
||||
'text' => 'Custom item text',
|
||||
],
|
||||
(object) [
|
||||
'id' => null,
|
||||
'text' => 'Custom item text2',
|
||||
],
|
||||
],
|
||||
'customstructure' => [ // This array contains scalar values, not a list of objects/arrays.
|
||||
'id' => null,
|
||||
'text' => 'Custom structure text',
|
||||
'number' => 1,
|
||||
'bool' => true,
|
||||
],
|
||||
];
|
||||
|
||||
$expecteddata = (object) [
|
||||
'questiontext' => 'Some question text',
|
||||
'hints' => [
|
||||
(object) [
|
||||
'text' => 'Lorem ipsum',
|
||||
],
|
||||
(object) [
|
||||
'text' => 'Lorem ipsum',
|
||||
],
|
||||
],
|
||||
'options' => [
|
||||
[
|
||||
'option' => true,
|
||||
],
|
||||
[
|
||||
'option' => false,
|
||||
],
|
||||
[
|
||||
'option' => false,
|
||||
],
|
||||
],
|
||||
'custom2' => 'Some custom text2',
|
||||
'customarray' => [
|
||||
(object) [
|
||||
'text' => 'Custom item text',
|
||||
],
|
||||
(object) [
|
||||
'text' => 'Custom item text2',
|
||||
],
|
||||
],
|
||||
'customstructure' => [
|
||||
'text' => 'Custom structure text',
|
||||
'number' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
$excludedfields = [
|
||||
'/custom1',
|
||||
'/custom3',
|
||||
'/customarray/id',
|
||||
'/customstructure/id',
|
||||
'/customstructure/bool',
|
||||
// A field that is not in the data structure will be ignored.
|
||||
'/custom4',
|
||||
];
|
||||
$this->assertEquals($expecteddata, \restore_qtype_plugin::remove_excluded_question_data($data, $excludedfields));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
# core_badges (subsystem) Upgrade notes
|
||||
|
||||
## 4.5.7
|
||||
|
||||
### Added
|
||||
|
||||
- A number of new static methods have been added to `core_badges\backpack_api` to support the new Canvas Credentials backpack provider. These methods allow you to retrieve lists of providers and regions, check if Canvas Credentials fields should be displayed, and get a region URL or API URL based on a given region ID. The new methods include `get_providers`, `get_regions`, `display_canvas_credentials_fields`, `get_region_url`, `get_region_api_url`, `get_regionid_from_url`, and `is_canvas_credentials_region`.
|
||||
|
||||
For more information see [MDL-86174](https://tracker.moodle.org/browse/MDL-86174)
|
||||
|
||||
## 4.5
|
||||
|
||||
### Added
|
||||
|
||||
@@ -82,6 +82,16 @@ if (!$badge->is_active()) {
|
||||
die();
|
||||
}
|
||||
|
||||
if (!empty($role)) {
|
||||
if (!user_has_role_assignment($USER->id, $role, $context->id) && !$isadmin) {
|
||||
// User does not have the role passed by the parameter.
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->notification(get_string('wrongrole', 'badges'));
|
||||
echo $OUTPUT->footer();
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
$returnurl = new moodle_url('recipients.php', array('id' => $badge->id));
|
||||
$returnlink = html_writer::link($returnurl, $strrecipients);
|
||||
$actionbar = new \core_badges\output\standard_action_bar(
|
||||
|
||||
@@ -42,6 +42,17 @@ define('BADGE_EXPIRES_TOKEN', 'expires');
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class backpack_api {
|
||||
/** @var int Canvas Credentials backpack provider */
|
||||
public const PROVIDER_CANVAS_CREDENTIALS = 0;
|
||||
|
||||
/** @var int Other backpack provider */
|
||||
public const PROVIDER_OTHER = 1;
|
||||
|
||||
/** @var int Empty provider */
|
||||
public const PROVIDER_EMPTY = -1;
|
||||
|
||||
/** @var int Empty region */
|
||||
public const REGION_EMPTY = -1;
|
||||
|
||||
/** @var string The email address of the issuer or the backpack owner. */
|
||||
private $email;
|
||||
@@ -579,4 +590,123 @@ class backpack_api {
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of backpack providers for OBv2.0.
|
||||
*
|
||||
* @return string[] Array with the OBv2.0 backpack providers.
|
||||
*/
|
||||
public static function get_providers(): array {
|
||||
$allproviders = [
|
||||
self::PROVIDER_CANVAS_CREDENTIALS => 'canvascredentialsprovider',
|
||||
self::PROVIDER_OTHER => 'otherprovider',
|
||||
];
|
||||
|
||||
foreach ($allproviders as $key => $value) {
|
||||
if (get_string_manager()->string_exists($value, 'badges')) {
|
||||
$providers[$key] = get_string($value, 'badges');
|
||||
} else {
|
||||
// If the string does not exist, use the key as a fallback.
|
||||
$providers[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of regions for backpack providers.
|
||||
*
|
||||
* @return array Regions with the following information: name, url and apiurl.
|
||||
*/
|
||||
public static function get_regions() {
|
||||
global $CFG;
|
||||
|
||||
$regions = [];
|
||||
if (empty(trim($CFG->badges_canvasregions))) {
|
||||
return $regions;
|
||||
}
|
||||
|
||||
$entries = explode("\n", $CFG->badges_canvasregions);
|
||||
foreach ($entries as $entry) {
|
||||
if (empty(trim($entry)) || substr_count($entry, '|') != 2) {
|
||||
continue;
|
||||
}
|
||||
$entry = trim($entry);
|
||||
$parts = explode('|', $entry);
|
||||
$regions[] = [
|
||||
'name' => $parts[0],
|
||||
'url' => rtrim($parts[1], '/'),
|
||||
'apiurl' => rtrim($parts[2], '/'),
|
||||
];
|
||||
}
|
||||
|
||||
return $regions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Canvas Credentials fields should be displayed or not in the backpack form.
|
||||
*
|
||||
* @return bool True if the fields should be displayed; false otherwise.
|
||||
*/
|
||||
public static function display_canvas_credentials_fields(): bool {
|
||||
return !empty(self::get_providers()) && !empty(self::get_regions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backpack URL for a given regionid.
|
||||
*
|
||||
* @param int $regionid The region identifier.
|
||||
* @return string|null The backpack URL.
|
||||
*/
|
||||
public static function get_region_url(int $regionid): ?string {
|
||||
$regions = self::get_regions();
|
||||
if (!array_key_exists($regionid, $regions)) {
|
||||
return null;
|
||||
}
|
||||
return $regions[$regionid]['url'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backpack API URL for a given regionid.
|
||||
*
|
||||
* @param int $regionid The region identifier.
|
||||
* @return string|null The backpack API URL.
|
||||
*/
|
||||
public static function get_region_api_url(int $regionid): ?string {
|
||||
$regions = self::get_regions();
|
||||
if (!array_key_exists($regionid, $regions)) {
|
||||
return null;
|
||||
}
|
||||
return $regions[$regionid]['apiurl'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get region identifier from a given backpack URL.
|
||||
* When the URL is not found, the last region index is returned.
|
||||
*
|
||||
* @param string $url The backpack URL.
|
||||
* @return int The region identifier associated to the given backpack URL or the last region index if not found.
|
||||
*/
|
||||
public static function get_regionid_from_url(string $url): int {
|
||||
$regions = self::get_regions();
|
||||
if (empty($regions)) {
|
||||
return self::REGION_EMPTY;
|
||||
}
|
||||
|
||||
// Normalize the URL by removing the trailing slash.
|
||||
$normalizedurl = rtrim($url, '/');
|
||||
$regionurl = array_search($normalizedurl, array_column($regions, 'url'));
|
||||
return $regionurl !== false ? (int)$regionurl : count($regions) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given URL is a Canvas Credentials one.
|
||||
*
|
||||
* @param string $url The backpack URL.
|
||||
* @return bool True is the given URL is a Canvas Credentials region; false otherwise.
|
||||
*/
|
||||
public static function is_canvas_credentials_region(string $url): bool {
|
||||
$regions = self::get_regions();
|
||||
return in_array($url, array_column($regions, 'url'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
namespace core_badges\form;
|
||||
|
||||
use core_badges\backpack_api;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->libdir.'/formslib.php');
|
||||
@@ -37,11 +39,7 @@ class external_backpack extends \moodleform {
|
||||
global $CFG;
|
||||
|
||||
$mform = $this->_form;
|
||||
$backpack = false;
|
||||
|
||||
if (isset($this->_customdata['externalbackpack'])) {
|
||||
$backpack = $this->_customdata['externalbackpack'];
|
||||
}
|
||||
$backpack = $this->_customdata['externalbackpack'] ?? null;
|
||||
|
||||
$mform->addElement('hidden', 'action', 'edit');
|
||||
$mform->setType('action', PARAM_ALPHA);
|
||||
@@ -52,14 +50,22 @@ class external_backpack extends \moodleform {
|
||||
$mform->setDefault('apiversion', OPEN_BADGES_V2P1);
|
||||
$mform->addRule('apiversion', null, 'required', null, 'client');
|
||||
|
||||
$this->add_provider_fields();
|
||||
|
||||
$mform->addElement('text', 'backpackweburl', get_string('backpackweburl', 'core_badges'));
|
||||
$mform->setType('backpackweburl', PARAM_URL);
|
||||
$mform->addRule('backpackweburl', null, 'required', null, 'client');
|
||||
$mform->addRule('backpackweburl', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
|
||||
$mform->hideIf('backpackweburl', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
|
||||
$mform->addElement('text', 'backpackapiurl', get_string('backpackapiurl', 'core_badges'));
|
||||
$mform->setType('backpackapiurl', PARAM_URL);
|
||||
$mform->addRule('backpackapiurl', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
|
||||
$mform->hideIf('backpackapiurl', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
|
||||
$mform->addElement('text', 'backpackweburlv2p1', get_string('backpackweburl', 'core_badges'));
|
||||
$mform->setType('backpackweburlv2p1', PARAM_URL);
|
||||
$mform->addRule('backpackweburlv2p1', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
|
||||
$mform->hideIf('backpackweburlv2p1', 'apiversion', 'ne', (string) OPEN_BADGES_V2P1);
|
||||
|
||||
$mform->addElement('hidden', 'id', ($backpack->id ?? null));
|
||||
$mform->setType('id', PARAM_INT);
|
||||
@@ -70,31 +76,116 @@ class external_backpack extends \moodleform {
|
||||
$mform->addElement('hidden', 'backpackuid', 0);
|
||||
$mform->setType('backpackuid', PARAM_INT);
|
||||
|
||||
$mform->addElement('advcheckbox', 'includeauthdetails', null, get_string('includeauthdetails', 'core_badges'));
|
||||
if (!empty($backpack->backpackemail) || !empty($backpack->password)) {
|
||||
$mform->setDefault('includeauthdetails', 1);
|
||||
// Add rules for backpack URL fields.
|
||||
if (backpack_api::display_canvas_credentials_fields()) {
|
||||
$mform->hideIf('backpackweburl', 'provider', 'ne', backpack_api::PROVIDER_OTHER);
|
||||
$mform->hideIf('backpackapiurl', 'provider', 'ne', backpack_api::PROVIDER_OTHER);
|
||||
}
|
||||
|
||||
$issuercontact = $CFG->badges_defaultissuercontact;
|
||||
$this->add_auth_fields($issuercontact);
|
||||
$issueremail = $CFG->badges_defaultissuercontact;
|
||||
// Connect to a Canvas Credentials provider.
|
||||
$this->add_connect_issuer_canvas_fields($issueremail);
|
||||
|
||||
// Connect to another provider.
|
||||
$this->add_connect_issuer_fields($backpack, $issueremail);
|
||||
|
||||
if ($backpack) {
|
||||
$this->set_data($backpack);
|
||||
}
|
||||
|
||||
$mform->hideIf('includeauthdetails', 'apiversion', 'in', [OPEN_BADGES_V2P1]);
|
||||
$mform->hideIf('backpackemail', 'includeauthdetails');
|
||||
$mform->hideIf('backpackemail', 'apiversion', 'in', [OPEN_BADGES_V2P1]);
|
||||
$mform->hideIf('password', 'includeauthdetails');
|
||||
$mform->hideIf('password', 'apiversion', 'in', [OPEN_BADGES_V2P1]);
|
||||
$mform->hideIf('backpackapiurl', 'apiversion', 'in', [OPEN_BADGES_V2P1]);
|
||||
|
||||
// Disable short forms.
|
||||
$mform->setDisableShortforms();
|
||||
|
||||
$this->add_action_buttons();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function definition_after_data(): void {
|
||||
parent::definition_after_data();
|
||||
$mform = $this->_form;
|
||||
|
||||
if ($this->is_submitted()) {
|
||||
if (!$mform->elementExists('apiversion')) {
|
||||
return;
|
||||
}
|
||||
$apiversion = $mform->getElement('apiversion')->getValue();
|
||||
$apiversion = $apiversion ? array_pop($apiversion) : null;
|
||||
$provider = $mform->elementExists('provider') ? $mform->getElement('provider')->getValue() : null;
|
||||
$provider = $provider ? array_pop($provider) : null;
|
||||
$region = $mform->elementExists('region') ? $mform->getElement('region')->getValue() : null;
|
||||
$region = $region ? array_pop($region) : null;
|
||||
if ($apiversion == OPEN_BADGES_V2) {
|
||||
if (
|
||||
$provider == backpack_api::PROVIDER_CANVAS_CREDENTIALS
|
||||
&& isset($region) && $region != backpack_api::REGION_EMPTY
|
||||
) {
|
||||
$mform->getElement('backpackweburl')->setValue(
|
||||
backpack_api::get_region_url($region),
|
||||
);
|
||||
$mform->getElement('backpackapiurl')->setValue(
|
||||
backpack_api::get_region_api_url($region),
|
||||
);
|
||||
|
||||
if ($mform->getElement('includeauthdetailscanvas')->getValue()) {
|
||||
$mform->getElement('backpackemail')->setValue(
|
||||
$mform->getElement('backpackemailcanvas')->getValue(),
|
||||
);
|
||||
$mform->getElement('password')->setValue(
|
||||
$mform->getElement('backpackpasswordcanvas')->getValue(),
|
||||
);
|
||||
}
|
||||
} else if (is_null($provider) || $provider == backpack_api::PROVIDER_OTHER) {
|
||||
if ($mform->getElement('includeauthdetails')->getValue() == 0) {
|
||||
// Clear backpack issuer fields when authentication details checkbox is not checked.
|
||||
$mform->getElement('backpackemail')->setValue('');
|
||||
$mform->getElement('password')->setValue('');
|
||||
}
|
||||
}
|
||||
} else if ($apiversion == OPEN_BADGES_V2P1) {
|
||||
if (!empty($mform->getElement('backpackweburlv2p1')->getValue())) {
|
||||
$mform->getElement('backpackweburl')->setValue(
|
||||
$mform->getElement('backpackweburlv2p1')->getValue(),
|
||||
);
|
||||
}
|
||||
// Clear backpack issuer fields when OBv2.1 is selected.
|
||||
$mform->getElement('includeauthdetails')->setValue(0);
|
||||
$mform->getElement('backpackemail')->setValue('');
|
||||
$mform->getElement('password')->setValue('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function set_data($backpack) {
|
||||
if ($backpack->apiversion == OPEN_BADGES_V2) {
|
||||
if (backpack_api::is_canvas_credentials_region($backpack->backpackweburl)) {
|
||||
// Calculate provider and region fields based on backpack URLs.
|
||||
$backpack->provider = backpack_api::PROVIDER_CANVAS_CREDENTIALS;
|
||||
$backpack->region = backpack_api::get_regionid_from_url($backpack->backpackweburl);
|
||||
$backpack->backpackweburl = '';
|
||||
$backpack->backpackapiurl = '';
|
||||
if (isset($backpack->backpackemail) && !empty($backpack->backpackemail)) {
|
||||
// Update Canvas Credentials fields.
|
||||
$backpack->includeauthdetailscanvas = 1;
|
||||
$backpack->backpackemailcanvas = $backpack->backpackemail;
|
||||
$backpack->backpackpasswordcanvas = $backpack->password;
|
||||
// Clear email and password fields for another providers.
|
||||
$backpack->includeauthdetails = 0;
|
||||
$backpack->backpackemail = '';
|
||||
$backpack->password = '';
|
||||
}
|
||||
} else {
|
||||
$backpack->provider = backpack_api::PROVIDER_OTHER;
|
||||
}
|
||||
} else if ($backpack->apiversion == OPEN_BADGES_V2P1) {
|
||||
$backpack->backpackweburlv2p1 = $backpack->backpackweburl;
|
||||
$backpack->backpackweburl = '';
|
||||
$backpack->backpackapiurl = '';
|
||||
}
|
||||
|
||||
parent::set_data($backpack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the data from the form.
|
||||
*
|
||||
@@ -106,42 +197,212 @@ class external_backpack extends \moodleform {
|
||||
$errors = parent::validation($data, $files);
|
||||
|
||||
// Ensure backpackapiurl and backpackweburl are valid URLs.
|
||||
$isobv21 = isset($data['apiversion']) && $data['apiversion'] == OPEN_BADGES_V2P1;
|
||||
if (!$isobv21) {
|
||||
if (empty($data['backpackapiurl'])) {
|
||||
$errors['backpackapiurl'] = get_string('err_required', 'form');
|
||||
} else if (!preg_match('@^https?://.+@', $data['backpackapiurl'])) {
|
||||
$errors['backpackapiurl'] = get_string('invalidurl', 'badges');
|
||||
}
|
||||
$isobv20 = isset($data['apiversion']) && $data['apiversion'] == OPEN_BADGES_V2;
|
||||
$isobv2p1 = isset($data['apiversion']) && $data['apiversion'] == OPEN_BADGES_V2P1;
|
||||
if ($isobv20) {
|
||||
$errors = array_merge($errors, $this->validate_obv20($data));
|
||||
} else if ($isobv2p1) {
|
||||
$errors = array_merge($errors, $this->validate_obv2p1($data));
|
||||
}
|
||||
if (!empty($data['backpackweburl']) && !preg_match('@^https?://.+@', $data['backpackweburl'])) {
|
||||
$errors['backpackweburl'] = get_string('invalidurl', 'badges');
|
||||
|
||||
// Check email and password are not empty when including auth details.
|
||||
if (!empty($data['includeauthdetails']) && empty($data['backpackemail'])) {
|
||||
$errors['backpackemail'] = get_string('err_required', 'form');
|
||||
}
|
||||
if (!empty($data['includeauthdetails']) && empty($data['password'])) {
|
||||
$errors['password'] = get_string('err_required', 'form');
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return submitted data if properly submitted or returns NULL if validation fails or
|
||||
* if there is no submitted data.
|
||||
* Validate the data for Open Badges v2.0.
|
||||
*
|
||||
* @return object|void
|
||||
* @param array $data Form data.
|
||||
* @return string[] An array of error messages.
|
||||
*/
|
||||
public function get_data() {
|
||||
$data = parent::get_data();
|
||||
if ($data ) {
|
||||
if ((isset($data->includeauthdetails) && !$data->includeauthdetails)
|
||||
|| (isset($data->apiversion) && $data->apiversion == 2.1)) {
|
||||
$data->backpackemail = "";
|
||||
$data->password = "";
|
||||
}
|
||||
private function validate_obv20(array $data): array {
|
||||
$errors = [];
|
||||
|
||||
if ((isset($data->apiversion) && $data->apiversion == 1)) {
|
||||
$data->password = "";
|
||||
$displaycanvasfields = backpack_api::display_canvas_credentials_fields();
|
||||
if (
|
||||
$displaycanvasfields
|
||||
&& (!array_key_exists('provider', $data) || $data['provider'] == backpack_api::PROVIDER_EMPTY)
|
||||
) {
|
||||
// Check provider is set.
|
||||
$errors['provider'] = get_string('err_required', 'form');
|
||||
} else if (
|
||||
$displaycanvasfields
|
||||
&& ($data['provider'] == backpack_api::PROVIDER_CANVAS_CREDENTIALS)
|
||||
) {
|
||||
// Check region is set.
|
||||
if (!array_key_exists('region', $data) || $data['region'] == backpack_api::REGION_EMPTY) {
|
||||
$errors['region'] = get_string('err_required', 'form');
|
||||
}
|
||||
} else {
|
||||
if (empty($data['backpackweburl'])) {
|
||||
$errors['backpackweburl'] = get_string('err_required', 'form');
|
||||
} else if (!preg_match('@^https?://.+@', $data['backpackweburl'])) {
|
||||
$errors['backpackweburl'] = get_string('invalidurl', 'badges');
|
||||
}
|
||||
if (empty($data['backpackapiurl'])) {
|
||||
$errors['backpackapiurl'] = get_string('err_required', 'form');
|
||||
} else if (!preg_match('@^https?://.+@', $data['backpackapiurl'])) {
|
||||
$errors['backpackapiurl'] = get_string('invalidurl', 'badges');
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
if ($displaycanvasfields) {
|
||||
if (!empty($data['includeauthdetailscanvas']) && empty($data['backpackemailcanvas'])) {
|
||||
$errors['backpackemailcanvas'] = get_string('err_required', 'form');
|
||||
}
|
||||
if (!empty($data['includeauthdetailscanvas']) && empty($data['backpackpasswordcanvas'])) {
|
||||
$errors['backpackpasswordcanvas'] = get_string('err_required', 'form');
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the data for Open Badges v2.1.
|
||||
*
|
||||
* @param array $data Form data.
|
||||
* @return string[] An array of error messages.
|
||||
*/
|
||||
private function validate_obv2p1(array $data): array {
|
||||
$errors = [];
|
||||
|
||||
if (empty($data['backpackweburlv2p1'])) {
|
||||
$errors['backpackweburlv2p1'] = get_string('err_required', 'form');
|
||||
} else if (!preg_match('@^https?://.+@', $data['backpackweburlv2p1'])) {
|
||||
$errors['backpackweburlv2p1'] = get_string('invalidurl', 'badges');
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add provider fields to the form.
|
||||
*/
|
||||
protected function add_provider_fields(): void {
|
||||
$mform = $this->_form;
|
||||
|
||||
if (!backpack_api::display_canvas_credentials_fields()) {
|
||||
// If canvas credentials fields are not to be displayed, return early.
|
||||
return;
|
||||
}
|
||||
|
||||
// Add an empty option at the start.
|
||||
$providers = backpack_api::get_providers();
|
||||
$providers = [backpack_api::PROVIDER_EMPTY => ''] + $providers;
|
||||
$mform->addElement('select', 'provider', get_string('provider', 'core_badges'), $providers);
|
||||
$mform->setType('provider', PARAM_RAW);
|
||||
$mform->hideIf('provider', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
|
||||
// Add an empty option at the start.
|
||||
$regions = backpack_api::get_regions();
|
||||
$regions = [backpack_api::REGION_EMPTY => ''] + array_column($regions, 'name');
|
||||
$mform->addElement('select', 'region', get_string('region', 'core_badges'), $regions);
|
||||
$mform->setType('region', PARAM_RAW);
|
||||
$mform->hideIf('region', 'provider', 'ne', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
$mform->hideIf('region', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Canvas backpack specific issuer auth details.
|
||||
*
|
||||
* @param string|null $email The email addressed provided or null if it's new.
|
||||
*/
|
||||
protected function add_connect_issuer_canvas_fields(?string $email): void {
|
||||
$mform = $this->_form;
|
||||
|
||||
if (!backpack_api::display_canvas_credentials_fields()) {
|
||||
// If canvas credentials fields are not to be displayed, return early.
|
||||
return;
|
||||
}
|
||||
|
||||
$providers = backpack_api::get_providers();
|
||||
$regions = backpack_api::get_regions();
|
||||
if (empty($providers) || empty($regions)) {
|
||||
// If no providers or regions are available, return early.
|
||||
return;
|
||||
}
|
||||
|
||||
// Checkbox and information to enable/disable issuer account.
|
||||
$mform->addElement('static', '', null, '');
|
||||
$mform->addElement(
|
||||
'advcheckbox',
|
||||
'includeauthdetailscanvas',
|
||||
null,
|
||||
'<strong>' . get_string('includeauthdetailscanvas', 'core_badges') . '</strong> '
|
||||
. get_string('includeauthdetailscanvas_subtitle', 'core_badges'),
|
||||
);
|
||||
if (!empty($backpack->backpackemail) || !empty($backpack->password)) {
|
||||
$mform->setDefault('includeauthdetailscanvas', 1);
|
||||
}
|
||||
$mform->addHelpButton('includeauthdetailscanvas', 'includeauthdetailscanvas', 'core_badges');
|
||||
$mform->hideIf('includeauthdetailscanvas', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
$mform->hideIf('includeauthdetailscanvas', 'region', 'eq', backpack_api::REGION_EMPTY);
|
||||
$mform->hideIf('includeauthdetailscanvas', 'provider', 'ne', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
|
||||
$mform->addElement(
|
||||
'static',
|
||||
'includeauthdetailscanvasdesc',
|
||||
null,
|
||||
get_string('includeauthdetailscanvas_desc', 'core_badges'),
|
||||
);
|
||||
$mform->hideIf('includeauthdetailscanvasdesc', 'includeauthdetailscanvas');
|
||||
$mform->hideIf('includeauthdetailscanvasdesc', 'region', 'eq', backpack_api::REGION_EMPTY);
|
||||
$mform->hideIf('includeauthdetailscanvasdesc', 'provider', 'ne', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
$mform->hideIf('includeauthdetailscanvasdesc', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
|
||||
// Email.
|
||||
$mform->addElement('text', 'backpackemailcanvas', get_string('issueremail', 'core_badges'));
|
||||
$mform->setType('backpackemailcanvas', PARAM_EMAIL);
|
||||
$mform->setDefault('backpackemailcanvas', $email);
|
||||
$mform->hideIf('backpackemailcanvas', 'includeauthdetailscanvas');
|
||||
$mform->hideIf('backpackemailcanvas', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
$mform->hideIf('backpackemailcanvas', 'provider', 'ne', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
|
||||
// Password.
|
||||
$mform->addElement('passwordunmask', 'backpackpasswordcanvas', get_string('password'));
|
||||
$mform->setType('backpackpasswordcanvas', PARAM_RAW);
|
||||
$mform->hideIf('backpackpasswordcanvas', 'includeauthdetailscanvas');
|
||||
$mform->hideIf('backpackpasswordcanvas', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
$mform->hideIf('backpackpasswordcanvas', 'provider', 'ne', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add generic backpack issuer auth details.
|
||||
*
|
||||
* @param \stdClass|null $backpack The backpack instance.
|
||||
* @param string|null $email The issuer email or null if it's new.
|
||||
*/
|
||||
protected function add_connect_issuer_fields(?\stdClass $backpack, ?string $email): void {
|
||||
$mform = $this->_form;
|
||||
|
||||
// Checkbox and information to enable/disable issuer account.
|
||||
$mform->addElement(
|
||||
'advcheckbox',
|
||||
'includeauthdetails',
|
||||
null,
|
||||
'<strong>' . get_string('includeauthdetails', 'core_badges') . '</strong>',
|
||||
);
|
||||
if ($backpack && (!empty($backpack->backpackemail) || !empty($backpack->password))) {
|
||||
$mform->setDefault('includeauthdetails', 1);
|
||||
}
|
||||
$mform->addHelpButton('includeauthdetails', 'includeauthdetails', 'core_badges');
|
||||
$mform->hideIf('includeauthdetails', 'provider', 'eq', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
$mform->hideIf('includeauthdetails', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
|
||||
$mform->addElement('static', 'includeauthdetailsdesc', null, get_string('includeauthdetails_desc', 'core_badges'));
|
||||
$mform->hideIf('includeauthdetailsdesc', 'includeauthdetails');
|
||||
$mform->hideIf('includeauthdetailsdesc', 'provider', 'eq', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
|
||||
// Email and password fields.
|
||||
$this->add_auth_fields($email);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,25 +414,22 @@ class external_backpack extends \moodleform {
|
||||
*/
|
||||
protected function add_auth_fields(?string $email, bool $includepassword = true) {
|
||||
$mform = $this->_form;
|
||||
$emailstring = get_string('email');
|
||||
$passwordstring = get_string('password');
|
||||
$showpasswordhelp = false;
|
||||
if (!isset($this->_customdata['userbackpack'])) {
|
||||
$emailstring = get_string('defaultissuercontact', 'core_badges');
|
||||
$passwordstring = get_string('defaultissuerpassword', 'core_badges');
|
||||
$showpasswordhelp = true;
|
||||
}
|
||||
|
||||
$mform->addElement('text', 'backpackemail', $emailstring);
|
||||
// Email.
|
||||
$mform->addElement('text', 'backpackemail', get_string('issueremail', 'core_badges'));
|
||||
$mform->setType('backpackemail', PARAM_EMAIL);
|
||||
$mform->setDefault('backpackemail', $email);
|
||||
$mform->hideIf('backpackemail', 'includeauthdetails');
|
||||
$mform->hideIf('backpackemail', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
$mform->hideIf('backpackemail', 'provider', 'eq', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
|
||||
// Password.
|
||||
if ($includepassword) {
|
||||
$mform->addElement('passwordunmask', 'password', $passwordstring);
|
||||
$mform->addElement('passwordunmask', 'password', get_string('password'));
|
||||
$mform->setType('password', PARAM_RAW);
|
||||
if ($showpasswordhelp) {
|
||||
$mform->addHelpButton('password', 'defaultissuerpassword', 'badges');
|
||||
}
|
||||
}
|
||||
$mform->hideIf('password', 'includeauthdetails');
|
||||
$mform->hideIf('password', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
$mform->hideIf('password', 'provider', 'eq', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,9 @@ class manage_badge_action_bar extends base_action_bar {
|
||||
$elements['button'] = new single_button(new moodle_url('/badges/index.php', $params), get_string('back'), 'get');
|
||||
$badgenav = $this->generate_badge_navigation();
|
||||
if ($badgenav) {
|
||||
$elements['urlselect'] = new url_select($badgenav, $this->page->url->out(false), null);
|
||||
$badgenavselect = new url_select($badgenav, $this->page->url->out(false), null);
|
||||
$badgenavselect->set_label(get_string('badgesnavigation', 'badges'), ['class' => 'sr-only']);
|
||||
$elements['urlselect'] = $badgenavselect;
|
||||
}
|
||||
foreach ($elements as $key => $element) {
|
||||
$elements[$key] = $element->export_for_template($output);
|
||||
|
||||
+40
-13
@@ -151,12 +151,17 @@ class core_badges_renderer extends plugin_renderer_base {
|
||||
'value' => $this->output->larrow() . ' ' . get_string('award', 'badges'),
|
||||
'class' => 'actionbutton btn btn-secondary')
|
||||
);
|
||||
$actioncell->text .= html_writer::empty_tag('input', array(
|
||||
if (has_capability('moodle/badges:revokebadge', $this->page->context)) {
|
||||
$actioncell->text .= html_writer::empty_tag(
|
||||
'input',
|
||||
[
|
||||
'type' => 'submit',
|
||||
'name' => 'revoke',
|
||||
'value' => get_string('revoke', 'badges') . ' ' . $this->output->rarrow(),
|
||||
'class' => 'actionbutton btn btn-secondary')
|
||||
);
|
||||
'class' => 'actionbutton btn btn-secondary',
|
||||
]
|
||||
);
|
||||
}
|
||||
$actioncell->text .= html_writer::end_tag('div', array());
|
||||
$actioncell->attributes['class'] = 'actions';
|
||||
$potentialcell = new html_table_cell();
|
||||
@@ -187,10 +192,22 @@ class core_badges_renderer extends plugin_renderer_base {
|
||||
$dl[get_string('createdon', 'search')] = userdate($badge->timecreated);
|
||||
$dl[get_string('badgeimage', 'badges')] = print_badge_image($badge, $context, 'large');
|
||||
$dl[get_string('imageauthorname', 'badges')] = $badge->imageauthorname;
|
||||
$dl[get_string('imageauthoremail', 'badges')] =
|
||||
html_writer::tag('a', $badge->imageauthoremail, array('href' => 'mailto:' . $badge->imageauthoremail));
|
||||
$dl[get_string('imageauthorurl', 'badges')] =
|
||||
html_writer::link($badge->imageauthorurl, $badge->imageauthorurl, array('target' => '_blank'));
|
||||
$dl[get_string('imageauthoremail', 'badges')] = '';
|
||||
if (trim($badge->imageauthoremail)) {
|
||||
$dl[get_string('imageauthoremail', 'badges')] = html_writer::tag(
|
||||
'a',
|
||||
$badge->imageauthoremail,
|
||||
['href' => 'mailto:' . $badge->imageauthoremail],
|
||||
);
|
||||
}
|
||||
$dl[get_string('imageauthorurl', 'badges')] = '';
|
||||
if (trim($badge->imageauthorurl)) {
|
||||
$dl[get_string('imageauthorurl', 'badges')] = html_writer::link(
|
||||
$badge->imageauthorurl,
|
||||
$badge->imageauthorurl,
|
||||
['target' => '_blank'],
|
||||
);
|
||||
}
|
||||
$dl[get_string('imagecaption', 'badges')] = $badge->imagecaption;
|
||||
$tags = \core_tag_tag::get_item_tags('core_badges', 'badge', $badge->id);
|
||||
$dl[get_string('tags', 'badges')] = $this->output->tag_list($tags, '');
|
||||
@@ -200,12 +217,22 @@ class core_badges_renderer extends plugin_renderer_base {
|
||||
$display .= $this->heading(get_string('issuerdetails', 'badges'), 3);
|
||||
$dl = array();
|
||||
$dl[get_string('issuername', 'badges')] = $badge->issuername;
|
||||
$dl[get_string('contact', 'badges')] = html_writer::tag('a', $badge->issuercontact, array('href' => 'mailto:' . $badge->issuercontact));
|
||||
$dl[get_string('issuerurl', 'badges')] = html_writer::tag(
|
||||
'a',
|
||||
$badge->issuerurl,
|
||||
['href' => $badge->issuerurl, 'target' => '_blank'],
|
||||
);
|
||||
$dl[get_string('contact', 'badges')] = '';
|
||||
if (trim($badge->issuercontact)) {
|
||||
$dl[get_string('contact', 'badges')] = html_writer::tag(
|
||||
'a',
|
||||
$badge->issuercontact,
|
||||
['href' => 'mailto:' . $badge->issuercontact],
|
||||
);
|
||||
}
|
||||
$dl[get_string('issuerurl', 'badges')] = '';
|
||||
if (trim($badge->issuerurl)) {
|
||||
$dl[get_string('issuerurl', 'badges')] = html_writer::tag(
|
||||
'a',
|
||||
$badge->issuerurl,
|
||||
['href' => $badge->issuerurl, 'target' => '_blank'],
|
||||
);
|
||||
}
|
||||
$display .= $this->definition_list($dl);
|
||||
|
||||
// Issuance details if any.
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
<?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 core_badges;
|
||||
|
||||
/**
|
||||
* Unit tests for backpack_api class.
|
||||
*
|
||||
* @package core_badges
|
||||
* @covers \core_badges\backpack_api
|
||||
* @copyright 2025 Sara Arjona <sara@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class backpack_api_test extends \advanced_testcase {
|
||||
/**
|
||||
* Test get_providers function.
|
||||
*/
|
||||
public function test_get_providers(): void {
|
||||
global $CFG;
|
||||
|
||||
$providers = backpack_api::get_providers();
|
||||
$this->assertCount(2, $providers);
|
||||
$this->assertArrayHasKey(backpack_api::PROVIDER_CANVAS_CREDENTIALS, $providers);
|
||||
$this->assertArrayHasKey(backpack_api::PROVIDER_OTHER, $providers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_regions function.
|
||||
*/
|
||||
public function test_get_regions(): void {
|
||||
global $CFG;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Default: 5 regions (Canvas Credentials).
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertCount(5, $regions);
|
||||
|
||||
// No regions.
|
||||
$CFG->badges_canvasregions = '';
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertEmpty($regions);
|
||||
|
||||
// One region.
|
||||
$CFG->badges_canvasregions = 'Australia|https://au.badgr.io|https://api.au.badgr.io/v2';
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertCount(1, $regions);
|
||||
$this->assertEquals('Australia', $regions[0]['name']);
|
||||
$this->assertEquals('https://au.badgr.io', $regions[0]['url']);
|
||||
$this->assertEquals('https://api.au.badgr.io/v2', $regions[0]['apiurl']);
|
||||
|
||||
// Two regions + empty lines + invalid line.
|
||||
$CFG->badges_canvasregions = "\nUnited States|https://badgr.io|https://api.badgr.io/v2\ninvalidline\n" .
|
||||
'Europe|https://eu.badgr.io|https://api.eu.badgr.io/v2' . "\n";
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertCount(2, $regions);
|
||||
$expected = [
|
||||
[
|
||||
'name' => 'United States',
|
||||
'url' => 'https://badgr.io',
|
||||
'apiurl' => 'https://api.badgr.io/v2',
|
||||
],
|
||||
[
|
||||
'name' => 'Europe',
|
||||
'url' => 'https://eu.badgr.io',
|
||||
'apiurl' => 'https://api.eu.badgr.io/v2',
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expected, $regions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test display_canvas_credentials_fields function.
|
||||
*/
|
||||
public function test_display_canvas_credentials_fields(): void {
|
||||
global $CFG;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// By default, the fields should be displayed (5 regions).
|
||||
$this->assertTrue(backpack_api::display_canvas_credentials_fields());
|
||||
|
||||
// No regions configured, fields should not be displayed.
|
||||
$CFG->badges_canvasregions = '';
|
||||
$this->assertFalse(backpack_api::display_canvas_credentials_fields());
|
||||
|
||||
// One region configured, fields should be displayed.
|
||||
$CFG->badges_canvasregions = 'Australia|https://au.badgr.io|https://api.au.badgr.io/v2';
|
||||
$this->assertTrue(backpack_api::display_canvas_credentials_fields());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_region_url and get_region_api_url functions.
|
||||
*/
|
||||
public function test_get_region_urls(): void {
|
||||
global $CFG;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Default: 5 regions (Canvas Credentials).
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertCount(5, $regions);
|
||||
$this->assertEquals('https://au.badgr.io', backpack_api::get_region_url(0));
|
||||
$this->assertEquals('https://ca.badgr.io', backpack_api::get_region_url(1));
|
||||
$this->assertEquals('https://eu.badgr.io', backpack_api::get_region_url(2));
|
||||
$this->assertEquals('https://sg.badgr.io', backpack_api::get_region_url(3));
|
||||
$this->assertEquals('https://badgr.io', backpack_api::get_region_url(4));
|
||||
$this->assertEquals('https://api.au.badgr.io/v2', backpack_api::get_region_api_url(0));
|
||||
$this->assertEquals('https://api.ca.badgr.io/v2', backpack_api::get_region_api_url(1));
|
||||
$this->assertEquals('https://api.eu.badgr.io/v2', backpack_api::get_region_api_url(2));
|
||||
$this->assertEquals('https://api.sg.badgr.io/v2', backpack_api::get_region_api_url(3));
|
||||
$this->assertEquals('https://api.badgr.io/v2', backpack_api::get_region_api_url(4));
|
||||
|
||||
// Wrong index.
|
||||
$this->assertNull(backpack_api::get_region_url(10));
|
||||
$this->assertNull(backpack_api::get_region_api_url(10));
|
||||
|
||||
// No regions.
|
||||
$CFG->badges_canvasregions = '';
|
||||
$this->assertNull(backpack_api::get_region_url(0));
|
||||
$this->assertNull(backpack_api::get_region_api_url(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_regionid_from_url function.
|
||||
*/
|
||||
public function test_get_regionid_from_url(): void {
|
||||
global $CFG;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Default: 5 regions (Canvas Credentials).
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertCount(5, $regions);
|
||||
$this->assertEquals(0, backpack_api::get_regionid_from_url('https://au.badgr.io'));
|
||||
$this->assertEquals(1, backpack_api::get_regionid_from_url('https://ca.badgr.io'));
|
||||
$this->assertEquals(2, backpack_api::get_regionid_from_url('https://eu.badgr.io'));
|
||||
$this->assertEquals(3, backpack_api::get_regionid_from_url('https://sg.badgr.io'));
|
||||
$this->assertEquals(4, backpack_api::get_regionid_from_url('https://badgr.io'));
|
||||
// Test with trailing slash.
|
||||
$this->assertEquals(0, backpack_api::get_regionid_from_url('https://au.badgr.io/'));
|
||||
|
||||
// Wrong URL.
|
||||
$this->assertEquals(4, backpack_api::get_regionid_from_url('https://unknown.badgr.io'));
|
||||
|
||||
// One region.
|
||||
$CFG->badges_canvasregions = 'Australia|https://au.badgr.io|https://api.au.badgr.io/v2';
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertEquals(0, backpack_api::get_regionid_from_url('https://au.badgr.io'));
|
||||
|
||||
// No regions.
|
||||
$CFG->badges_canvasregions = '';
|
||||
$this->assertEquals(backpack_api::REGION_EMPTY, backpack_api::get_regionid_from_url('https://au.badgr.io'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test is_canvas_credentials_region function.
|
||||
*/
|
||||
public function test_is_canvas_credentials_region(): void {
|
||||
global $CFG;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Default: 5 regions (Canvas Credentials).
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertCount(5, $regions);
|
||||
$this->assertTrue(backpack_api::is_canvas_credentials_region('https://au.badgr.io'));
|
||||
$this->assertTrue(backpack_api::is_canvas_credentials_region('https://ca.badgr.io'));
|
||||
$this->assertTrue(backpack_api::is_canvas_credentials_region('https://eu.badgr.io'));
|
||||
$this->assertTrue(backpack_api::is_canvas_credentials_region('https://sg.badgr.io'));
|
||||
$this->assertTrue(backpack_api::is_canvas_credentials_region('https://badgr.io'));
|
||||
|
||||
// Non Canvas URL.
|
||||
$this->assertFalse(backpack_api::is_canvas_credentials_region('https://unknown.badgr.io'));
|
||||
|
||||
// No regions.
|
||||
$CFG->badges_canvasregions = '';
|
||||
$this->assertFalse(backpack_api::is_canvas_credentials_region('https://au.badgr.io'));
|
||||
}
|
||||
}
|
||||
@@ -93,12 +93,111 @@ Feature: Backpack badges
|
||||
And "Add to backpack" "link" should exist
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site backpack
|
||||
Scenario: Add a new site OBv2.1 backpack
|
||||
Given I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2.1"
|
||||
And I should see "Backpack URL"
|
||||
And I set the field "backpackweburlv2p1" to "http://backpackweburl.cat"
|
||||
And I should not see "Backpack API URL"
|
||||
Then "Connect to backpack provider account" "checkbox" should not be visible
|
||||
And "Connect to a Canvas Credentials issuer account" "checkbox" should not be visible
|
||||
And I should not see "Email"
|
||||
And I should not see "Password"
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site OBv2.0 backpack with Canvas provider
|
||||
Given I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2"
|
||||
And I press "Save changes"
|
||||
And I should see "You must supply a value here"
|
||||
And I set the field "provider" to "Canvas Credentials"
|
||||
And I press "Save changes"
|
||||
And I should see "You must supply a value here"
|
||||
And I set the field "region" to "Singapore"
|
||||
And I should not see "Backpack web URL"
|
||||
And I should not see "Backpack API URL"
|
||||
And I press "Save changes"
|
||||
Then I should see "https://sg.badgr.io"
|
||||
And "Delete" "icon" should exist in the "https://sg.badgr.io" "table_row"
|
||||
And "Edit settings" "icon" should exist in the "https://sg.badgr.io" "table_row"
|
||||
And "Test settings" "icon" should exist in the "https://sg.badgr.io" "table_row"
|
||||
# Check that editing the backpack shows the correct values.
|
||||
And I click on "Edit settings" "link" in the "https://sg.badgr.io" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And I should see "Provider"
|
||||
And the field "provider" matches value "Canvas Credentials"
|
||||
And I should see "Region"
|
||||
And the field "region" matches value "Singapore"
|
||||
And I should see "Connect to a Canvas Credentials issuer account"
|
||||
And the field "Connect to a Canvas Credentials issuer account" matches value "0"
|
||||
And I should not see "Connect to backpack provider account"
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site OBv2.0 backpack with Canvas provider and issuer authentication details
|
||||
Given I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2"
|
||||
And I set the field "provider" to "Canvas Credentials"
|
||||
And I set the field "region" to "Canada"
|
||||
And I should see "Connect to a Canvas Credentials issuer account"
|
||||
And I should not see "Connect to backpack provider account"
|
||||
And the field "Connect to a Canvas Credentials issuer account" matches value "0"
|
||||
And I click on "includeauthdetailscanvas" "checkbox"
|
||||
And I should see "Email"
|
||||
And I should see "Password"
|
||||
And I press "Save changes"
|
||||
And I should see "You must supply a value here"
|
||||
And I set the field "backpackemailcanvas" to "test@test.com"
|
||||
And I should see "You must supply a value here"
|
||||
And I press "Save changes"
|
||||
And I set the field "backpackpasswordcanvas" to "123456"
|
||||
And I press "Save changes"
|
||||
Then I should see "https://ca.badgr.io"
|
||||
# Check that editing the backpack shows the correct values.
|
||||
And I click on "Edit settings" "link" in the "https://ca.badgr.io" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And I should see "Provider"
|
||||
And the field "provider" matches value "Canvas Credentials"
|
||||
And I should see "Region"
|
||||
And the field "region" matches value "Canada"
|
||||
And I should see "Connect to a Canvas Credentials issuer account"
|
||||
And the field "Connect to a Canvas Credentials issuer account" matches value "1"
|
||||
And the field "backpackemailcanvas" matches value "test@test.com"
|
||||
And the field "backpackpasswordcanvas" matches value "123456"
|
||||
# Disable authentication details and check that email and password are cleared.
|
||||
But I click on "includeauthdetailscanvas" "checkbox"
|
||||
And I press "Save changes"
|
||||
And I click on "Edit settings" "link" in the "https://ca.badgr.io" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And I should see "Provider"
|
||||
And the field "provider" matches value "Canvas Credentials"
|
||||
And I should see "Region"
|
||||
And the field "region" matches value "Canada"
|
||||
And I should see "Connect to a Canvas Credentials issuer account"
|
||||
And the field "Connect to a Canvas Credentials issuer account" matches value "0"
|
||||
And the field "backpackemailcanvas" matches value ""
|
||||
And the field "backpackpasswordcanvas" matches value ""
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site OBv2.0 backpack with Other provider
|
||||
Given I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2"
|
||||
And I set the field "provider" to "Other"
|
||||
And I should not see "Region"
|
||||
And I set the field "backpackweburl" to "aaa"
|
||||
And I press "Save changes"
|
||||
And I should see "Invalid URL"
|
||||
@@ -110,6 +209,115 @@ Feature: Backpack badges
|
||||
Then I should see "http://backpackweburl.cat"
|
||||
And "Delete" "icon" should exist in the "http://backpackweburl.cat" "table_row"
|
||||
And "Edit settings" "icon" should exist in the "http://backpackweburl.cat" "table_row"
|
||||
And "Test settings" "icon" should exist in the "http://backpackweburl.cat" "table_row"
|
||||
# Check that editing the backpack shows the correct values.
|
||||
And I click on "Edit settings" "link" in the "http://backpackweburl.cat" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And I should see "Provider"
|
||||
And the field "provider" matches value "Other"
|
||||
And I should not see "Region"
|
||||
And the field "backpackweburl" matches value "http://backpackweburl.cat"
|
||||
And the field "backpackapiurl" matches value "http://backpackapiurl.cat"
|
||||
And the field "Connect to backpack provider account" matches value "0"
|
||||
And I should not see "Connect to a Canvas Credentials issuer account"
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site OBv2.0 backpack with Other provider and issuer authentication details
|
||||
Given I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2"
|
||||
And I set the field "provider" to "Other"
|
||||
And I set the field "backpackweburl" to "http://backpackweburl.cat"
|
||||
And I set the field "backpackapiurl" to "http://backpackapiurl.cat"
|
||||
And I should see "Connect to backpack provider account"
|
||||
And I should not see "Connect to a Canvas Credentials issuer account"
|
||||
And the field "Connect to backpack provider account" matches value "0"
|
||||
And I click on "includeauthdetails" "checkbox"
|
||||
And I should see "Email"
|
||||
And I should see "Password"
|
||||
And I press "Save changes"
|
||||
And I should see "You must supply a value here"
|
||||
And I set the field "backpackemail" to "test@test.com"
|
||||
And I press "Save changes"
|
||||
And I should see "You must supply a value here"
|
||||
And I set the field "password" to "123456"
|
||||
And I press "Save changes"
|
||||
Then I should see "http://backpackweburl.cat"
|
||||
# Check that editing the backpack shows the correct values.
|
||||
And I click on "Edit settings" "link" in the "http://backpackweburl.cat" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And the field "provider" matches value "Other"
|
||||
And the field "backpackweburl" matches value "http://backpackweburl.cat"
|
||||
And the field "backpackapiurl" matches value "http://backpackapiurl.cat"
|
||||
And the field "Connect to backpack provider account" matches value "1"
|
||||
And the field "backpackemail" matches value "test@test.com"
|
||||
And the field "password" matches value "123456"
|
||||
# Disable authentication details and check that email and password are cleared.
|
||||
But I click on "includeauthdetails" "checkbox"
|
||||
And I press "Save changes"
|
||||
And I click on "Edit settings" "link" in the "http://backpackweburl.cat" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And the field "provider" matches value "Other"
|
||||
And the field "backpackweburl" matches value "http://backpackweburl.cat"
|
||||
And the field "backpackapiurl" matches value "http://backpackapiurl.cat"
|
||||
And the field "Connect to backpack provider account" matches value "0"
|
||||
And the field "backpackemail" matches value ""
|
||||
And the field "password" matches value ""
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site OBv2.0 backpack without providers
|
||||
Given the following config values are set as admin:
|
||||
| badges_canvasregions | |
|
||||
And I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2"
|
||||
And I should not see "Provider"
|
||||
And I should see "Backpack URL"
|
||||
And I should see "Backpack API URL"
|
||||
And I press "Save changes"
|
||||
And I should see "You must supply a value here"
|
||||
And I set the field "backpackweburl" to "https://eu.badgr.io"
|
||||
And I set the field "backpackapiurl" to "https://api.eu.badgr.io/v2"
|
||||
And I should see "Connect to backpack provider account"
|
||||
And I should not see "Connect to a Canvas Credentials issuer account"
|
||||
And I press "Save changes"
|
||||
Then I should see "https://eu.badgr.io"
|
||||
And "Delete" "icon" should exist in the "https://eu.badgr.io" "table_row"
|
||||
And "Edit settings" "icon" should exist in the "https://eu.badgr.io" "table_row"
|
||||
And "Test settings" "icon" should exist in the "https://eu.badgr.io" "table_row"
|
||||
# Check that editing the backpack shows the correct values.
|
||||
And I click on "Edit settings" "link" in the "https://eu.badgr.io" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And I should not see "Provider"
|
||||
And the field "backpackweburl" matches value "https://eu.badgr.io"
|
||||
And the field "backpackapiurl" matches value "https://api.eu.badgr.io/v2"
|
||||
And I should see "Connect to backpack provider account"
|
||||
And the field "Connect to backpack provider account" matches value "0"
|
||||
And I should not see "Connect to a Canvas Credentials issuer account"
|
||||
And I press "Cancel"
|
||||
# Add Europe to the providers list and check that editing the backpack shows the correct values.
|
||||
But the following config values are set as admin:
|
||||
| badges_canvasregions | Europe\|https://eu.badgr.io\|https://api.eu.badgr.io/v2 |
|
||||
And I click on "Edit settings" "link" in the "https://eu.badgr.io" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And I should see "Provider"
|
||||
And the field "provider" matches value "Canvas Credentials"
|
||||
And I should see "Region"
|
||||
And the field "region" matches value "Europe"
|
||||
And I should see "Connect to a Canvas Credentials issuer account"
|
||||
And the field "Connect to a Canvas Credentials issuer account" matches value "0"
|
||||
And I should not see "Connect to backpack provider account"
|
||||
And I should not see "Backpack URL"
|
||||
And I should not see "Backpack API URL"
|
||||
|
||||
@javascript
|
||||
Scenario: Remove a site backpack
|
||||
@@ -138,36 +346,6 @@ Feature: Backpack badges
|
||||
And "Move up" "icon" should exist in the "https://dc.imsglobal.org" "table_row"
|
||||
And "Move down" "icon" should not exist in the "https://dc.imsglobal.org" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site backpack with authentication details checkbox
|
||||
Given I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2.1"
|
||||
And I set the field "backpackweburl" to "http://backpackweburl.cat"
|
||||
And I should not see "Backpack API URL"
|
||||
Then "Include authentication details with the backpack" "checkbox" should not be visible
|
||||
And I should not see "Badge issuer email address"
|
||||
And I should not see "Badge issuer password"
|
||||
And I set the field "apiversion" to "2"
|
||||
And "Include authentication details with the backpack" "checkbox" should be visible
|
||||
And I click on "includeauthdetails" "checkbox"
|
||||
And I should see "Badge issuer email address"
|
||||
And I should see "Badge issuer password"
|
||||
And I set the field "backpackemail" to "test@test.com"
|
||||
And I set the field "password" to "123456"
|
||||
And I set the field "backpackapiurl" to "http://backpackapiurl.cat"
|
||||
And I press "Save changes"
|
||||
And I click on "Edit" "link" in the "http://backpackweburl.cat" "table_row"
|
||||
And the field "Include authentication details with the backpack" matches value "1"
|
||||
And I click on "includeauthdetails" "checkbox"
|
||||
And I press "Save changes"
|
||||
And I click on "Edit" "link" in the "http://backpackweburl.cat" "table_row"
|
||||
And the field "Include authentication details with the backpack" matches value "0"
|
||||
And I click on "includeauthdetails" "checkbox"
|
||||
And I should not see "test@test.com"
|
||||
|
||||
@javascript
|
||||
Scenario: View backpack form as a student
|
||||
Given I log in as "student1"
|
||||
@@ -175,11 +353,11 @@ Feature: Backpack badges
|
||||
And I follow "Backpack settings"
|
||||
When I set the field "externalbackpackid" to "https://dc.imsglobal.org"
|
||||
Then I should not see "Log in to your backpack"
|
||||
And I should not see "Email address"
|
||||
And I should not see "Email"
|
||||
And I should not see "Password"
|
||||
But I set the field "externalbackpackid" to "https://test.com/"
|
||||
And I should see "Log in to your backpack"
|
||||
And I should see "Email address"
|
||||
And I should see "Email"
|
||||
And I should see "Password"
|
||||
|
||||
@javascript
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
@core @core_badges
|
||||
Feature: Badge overview
|
||||
In order to view a badge's information
|
||||
As an user with the capability to view badges
|
||||
I need to access the badge overview page
|
||||
|
||||
Background:
|
||||
Given the following config values are set as admin:
|
||||
| badges_defaultissuername | Test Badge Site |
|
||||
|
||||
@javascript @accessibility @_file_upload
|
||||
Scenario: Test accessibility of badge overview page
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Cool badge |
|
||||
| Description | Badge for cool people |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
When I select "Overview" from the "Badges navigation" singleselect
|
||||
Then the "region-main" "region" should meet accessibility standards with "best-practice" extra tests
|
||||
@@ -24,6 +24,7 @@ Feature: Display badges
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
|
||||
@accessibility
|
||||
Scenario: Display badge without expired date
|
||||
# Enable the badge.
|
||||
Given I press "Enable access" action in the "Testing system badge" report row
|
||||
@@ -44,6 +45,7 @@ Feature: Display badges
|
||||
And I follow "More details"
|
||||
And I should see "Catalan"
|
||||
And I should see "1.1"
|
||||
And the page should meet accessibility standards with "best-practice" extra tests
|
||||
|
||||
Scenario: Display badge with ALL criteria
|
||||
# Add another criterion and enable the badge.
|
||||
|
||||
@@ -22,9 +22,9 @@ Feature: Block accessreview
|
||||
When I add the "Accessibility Review" block
|
||||
Then I should see "Accessibility Review"
|
||||
And I should see "Your accessibility toolkit needs to be registered."
|
||||
And I click on "Actions menu" "icon" in the "Accessibility Review" "block"
|
||||
And I click on "Actions menu" "menuitem" in the "Accessibility Review" "block"
|
||||
And I follow "Hide Accessibility Review block"
|
||||
And I should not see "Your accessibility toolkit needs to be registered."
|
||||
And I click on "Actions menu" "icon" in the "Accessibility Review" "block"
|
||||
And I click on "Actions menu" "menuitem" in the "Accessibility Review" "block"
|
||||
And I follow "Show Accessibility Review block"
|
||||
And I should see "Your accessibility toolkit needs to be registered."
|
||||
|
||||
@@ -24,9 +24,9 @@ Feature: Block accessreview
|
||||
When I add the "Accessibility review" block
|
||||
Then I should see "Accessibility review"
|
||||
And I should see "Your accessibility toolkit needs to be registered."
|
||||
And I click on "Actions menu" "icon" in the "Accessibility review" "block"
|
||||
And I click on "Actions menu" "menuitem" in the "Accessibility review" "block"
|
||||
And I follow "Hide Accessibility review block"
|
||||
And I should not see "Your accessibility toolkit needs to be registered."
|
||||
And I click on "Actions menu" "icon" in the "Accessibility review" "block"
|
||||
And I click on "Actions menu" "menuitem" in the "Accessibility review" "block"
|
||||
And I follow "Show Accessibility review block"
|
||||
And I should see "Your accessibility toolkit needs to be registered."
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{}
|
||||
}}
|
||||
|
||||
<div id="block-myoverview-{{uniqid}}" class="block-myoverview block-cards" data-region="myoverview" role="navigation">
|
||||
<div id="block-myoverview-{{uniqid}}" class="block-myoverview block-cards" data-region="myoverview">
|
||||
<div role="search" data-region="filter" class="d-flex align-items-center my-2" aria-label="{{#str}} aria:controls, block_myoverview {{/str}}">
|
||||
<div class="row g-0">
|
||||
{{> block_myoverview/nav-grouping-selector }}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
}
|
||||
}}
|
||||
|
||||
<div class="block-myoverview block-cards" data-region="myoverview" role="navigation">
|
||||
<div class="block-myoverview block-cards" data-region="myoverview">
|
||||
<hr class="mt-0"/>
|
||||
|
||||
<div class="container-fluid p-0">
|
||||
@@ -53,7 +53,7 @@
|
||||
src="{{nocoursesimg}}"
|
||||
alt=""
|
||||
>
|
||||
<h5 class="h5 mt-3 mb-0">{{{ title }}}</h5>
|
||||
<h4 class="h4 mt-3 mb-0">{{{ title }}}</h4>
|
||||
<p class="mt-3 mb-0">{{{ intro }}}</p>
|
||||
|
||||
<div class="mt-5 mb-0 whitebutton" id="action_bar">
|
||||
|
||||
@@ -181,13 +181,14 @@ Feature: The my overview block allows users to group courses by custom fields
|
||||
And I set the field "Custom field" to "1"
|
||||
Then the "Field to use" select box should not contain "Hidden field"
|
||||
|
||||
@core_customfield
|
||||
Scenario: Hidden fields not displayed in the filter
|
||||
Given the following config values are set as admin:
|
||||
| displaygroupingcustomfield | 1 | block_myoverview |
|
||||
| customfiltergrouping | textfield2 | block_myoverview |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Courses > Default settings > Course custom fields" in site administration
|
||||
And I click on "Edit" "link" in the "Text field 2" "table_row"
|
||||
And I press "Edit custom field: Text field 2"
|
||||
And I set the field "Visible to" to "Nobody"
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
|
||||
@@ -314,6 +314,6 @@ Feature: The my overview block allows users to easily access their courses
|
||||
Then I should not see "Category 1" in the "Course overview" "block"
|
||||
|
||||
@accessibility
|
||||
Scenario: The dashboard page must have sufficient colour contrast
|
||||
Scenario: The My courses page must meet accessibility standards
|
||||
When I am on the "My courses" page logged in as "student1"
|
||||
Then the page should meet "wcag143" accessibility standards
|
||||
Then the page should meet accessibility standards with "best-practice" extra tests
|
||||
|
||||
@@ -88,4 +88,4 @@ Feature: Zero state on my overview block
|
||||
@javascript @accessibility
|
||||
Scenario: Evaluate the accessibility of the My courses (zero state)
|
||||
When I am on the "My courses" page logged in as "manager"
|
||||
Then the page should meet accessibility standards
|
||||
Then the page should meet accessibility standards with "best-practice" extra tests
|
||||
|
||||
@@ -9,8 +9,9 @@ Feature: Block removal via modal
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| search_forums | Course | C1 | course-view-* | side-pre |
|
||||
| blockname | contextlevel | reference | pagetypepattern | showinsubcontexts | defaultregion |
|
||||
| search_forums | Course | C1 | course-view-* | 0 | side-pre |
|
||||
| myprofile | System | | * | 1 | side-pre |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
|
||||
@@ -18,16 +19,24 @@ Feature: Block removal via modal
|
||||
Scenario: Removing a block via modal should remove the block on the page
|
||||
Given I open the "Search forums" blocks action menu
|
||||
When I click on "Delete Search forums block" "link" in the "Search forums" "block"
|
||||
Then "Delete block?" "dialogue" should exist
|
||||
And I should see "This will delete the block Search forums." in the "Delete block?" "dialogue"
|
||||
And I click on "Delete" "button" in the "Delete block?" "dialogue"
|
||||
And I wait to be redirected
|
||||
And "Search forums" "block" should not exist
|
||||
Then "Search forums" "block" should not exist
|
||||
And I open the "Logged in user" blocks action menu
|
||||
And I click on "Delete Logged in user block" "link" in the "Logged in user" "block"
|
||||
And I should see "You are about to delete a block that appears elsewhere" in the "Delete block?" "dialogue"
|
||||
And I should see "Original block location: System" in the "Delete block?" "dialogue"
|
||||
And I should see "Display on page types: Any page" in the "Delete block?" "dialogue"
|
||||
And I should see "Are you sure you want to continue?" in the "Delete block?" "dialogue"
|
||||
And I click on "Delete" "button" in the "Delete block?" "dialogue"
|
||||
And I wait to be redirected
|
||||
And "Logged in user" "block" should not exist
|
||||
|
||||
@javascript
|
||||
Scenario: Cancel removing a block via modal should retain the block on the page
|
||||
Given I open the "Search forums" blocks action menu
|
||||
When I click on "Delete Search forums block" "link" in the "Search forums" "block"
|
||||
Then "Delete block?" "dialogue" should exist
|
||||
And I click on "Cancel" "button" in the "Delete block?" "dialogue"
|
||||
And I should not see "Delete block?"
|
||||
And "Search forums" "block" should exist
|
||||
Then "Search forums" "block" should exist
|
||||
|
||||
Vendored
+47
-5
@@ -756,10 +756,16 @@ class helper {
|
||||
debugging('Cache stores used for session definitions should ideally be searchable.', DEBUG_DEVELOPER);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load all of the keys into memory so we can compare against multiple prefixes.
|
||||
$keys = $store->find_all();
|
||||
sort($keys);
|
||||
|
||||
// Get all of the last access keys.
|
||||
$keys = $store->find_by_prefix(session_cache::LASTACCESS);
|
||||
$lastaccess = array_filter($keys, fn($key) => str_starts_with($key, session_cache::LASTACCESS));
|
||||
$todelete = [];
|
||||
foreach ($store->get_many($keys) as $key => $value) {
|
||||
$prefixtodelete = [];
|
||||
foreach ($store->get_many($lastaccess) as $key => $value) {
|
||||
$expiresvalue = 0;
|
||||
if ($value instanceof ttl_wrapper) {
|
||||
$expiresvalue = $value->data;
|
||||
@@ -771,11 +777,16 @@ class helper {
|
||||
$expires = (int) $expiresvalue;
|
||||
|
||||
if ($expires > 0 && $expires < $purgetime) {
|
||||
$prefix = substr($key, strlen(session_cache::LASTACCESS));
|
||||
$foundbyprefix = $store->find_by_prefix($prefix);
|
||||
$todelete = array_merge($todelete, [$key], $foundbyprefix);
|
||||
$todelete[] = $key;
|
||||
$prefixtodelete[] = substr($key, strlen(session_cache::LASTACCESS));
|
||||
}
|
||||
}
|
||||
|
||||
// Match all of the prefixes to delete to keys to delete.
|
||||
if ($prefixtodelete) {
|
||||
sort($prefixtodelete);
|
||||
$todelete = array_merge($todelete, self::filter_sorted_keys_by_prefixes($keys, $prefixtodelete));
|
||||
}
|
||||
if ($todelete) {
|
||||
$outcome = (int) $store->delete_many($todelete);
|
||||
if ($output) {
|
||||
@@ -788,6 +799,37 @@ class helper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a sorted list of keys by a sorted list of prefixes.
|
||||
* This relies on the sorting to reduce the number of comparisons.
|
||||
*
|
||||
* @param array $keys a sorted array of keys
|
||||
* @param array $prefixes a sorted array of prefixes
|
||||
* @return array of keys containing any of the prefixes
|
||||
*/
|
||||
public static function filter_sorted_keys_by_prefixes(array $keys, array $prefixes): array {
|
||||
// Reverse the prefixes to allow for simple processing.
|
||||
$prefixes = array_reverse($prefixes);
|
||||
|
||||
$matches = [];
|
||||
$prefix = array_pop($prefixes);
|
||||
foreach ($keys as $key) {
|
||||
// The keys and prefixes are sorted so we only need to compare against one prefix at a time.
|
||||
// This is done inside a loop to check the next prefix at break points.
|
||||
while ($prefix) {
|
||||
if (str_starts_with($key, $prefix)) {
|
||||
$matches[] = $key;
|
||||
} else if ($prefix < $key) {
|
||||
// The key has moved past the current prefix alphabetically, so check the next.
|
||||
$prefix = array_pop($prefixes);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of stores that would meet the requirements for every definition.
|
||||
*
|
||||
|
||||
Vendored
+23
-8
@@ -714,13 +714,34 @@ class cachestore_redis extends store implements
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all of the keys being used by this cache store instance using a scan.
|
||||
* This is preferred over keys to avoid blocking the server for a long time.
|
||||
*
|
||||
* @param string $prefix
|
||||
* @return array of all matching keys in the hash as a numbered array.
|
||||
*/
|
||||
protected function scan_keys($prefix = '') {
|
||||
$return = [];
|
||||
$iterator = null;
|
||||
do {
|
||||
$results = $this->redis->hScan($this->hash, $iterator, "$prefix*", 1000);
|
||||
if ($results !== false) {
|
||||
foreach ($results as $key => $value) {
|
||||
$return[] = $key;
|
||||
}
|
||||
}
|
||||
} while ($iterator != 0);
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all of the keys being used by this cache store instance.
|
||||
*
|
||||
* @return array of all keys in the hash as a numbered array.
|
||||
*/
|
||||
public function find_all() {
|
||||
return $this->redis->hKeys($this->hash);
|
||||
return $this->scan_keys();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -731,13 +752,7 @@ class cachestore_redis extends store implements
|
||||
* @return array List of keys that match this prefix.
|
||||
*/
|
||||
public function find_by_prefix($prefix) {
|
||||
$return = [];
|
||||
foreach ($this->find_all() as $key) {
|
||||
if (strpos($key, $prefix) === 0) {
|
||||
$return[] = $key;
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
return $this->scan_keys($prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+77
@@ -60,4 +60,81 @@ final class cache_helper_test extends \advanced_testcase {
|
||||
[new \stdClass(), true],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the filter_sorted_keys_by_prefixes method.
|
||||
*
|
||||
* @param array $keys
|
||||
* @param array $prefixes
|
||||
* @param array $expected
|
||||
* @dataProvider filter_sorted_keys_by_prefixes_provider
|
||||
*/
|
||||
public function test_filter_sorted_keys_by_prefixes(array $keys, array $prefixes, array $expected): void {
|
||||
$this->assertEquals($expected, helper::filter_sorted_keys_by_prefixes($keys, $prefixes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for filter_sorted_keys_by_prefixes tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function filter_sorted_keys_by_prefixes_provider(): array {
|
||||
return [
|
||||
'simple match' => [
|
||||
'keys' => ['aa', 'ab', 'ba', 'bb'],
|
||||
'prefixes' => ['a'],
|
||||
'expected' => ['aa', 'ab'],
|
||||
],
|
||||
'multiple prefixes match' => [
|
||||
'keys' => ['aa', 'ab', 'ba', 'bb', 'ca', 'cb'],
|
||||
'prefixes' => ['a', 'c'],
|
||||
'expected' => ['aa', 'ab', 'ca', 'cb'],
|
||||
],
|
||||
'consecutive prefixes match' => [
|
||||
'keys' => ['aa', 'ab', 'ba', 'bb', 'ca', 'cb'],
|
||||
'prefixes' => ['a', 'b'],
|
||||
'expected' => ['aa', 'ab', 'ba', 'bb'],
|
||||
],
|
||||
'overlapping prefixes' => [
|
||||
'keys' => ['a', 'ab', 'abc', 'abcd'],
|
||||
'prefixes' => ['ab', 'abc'],
|
||||
'expected' => ['ab', 'abc', 'abcd'],
|
||||
],
|
||||
'exact match' => [
|
||||
'keys' => ['a', 'b', 'c'],
|
||||
'prefixes' => ['a', 'c'],
|
||||
'expected' => ['a', 'c'],
|
||||
],
|
||||
'duplicate keys' => [
|
||||
'keys' => ['a', 'a', 'b', 'c'],
|
||||
'prefixes' => ['a'],
|
||||
'expected' => ['a', 'a'],
|
||||
],
|
||||
'duplicate prefixes' => [
|
||||
'keys' => ['a', 'b', 'c'],
|
||||
'prefixes' => ['a', 'a', 'b'],
|
||||
'expected' => ['a', 'b'],
|
||||
],
|
||||
'unsorted keys boundry' => [
|
||||
'keys' => ['c', 'b', 'a'],
|
||||
'prefixes' => ['a'],
|
||||
'expected' => [],
|
||||
],
|
||||
'unsorted prefixes boundry' => [
|
||||
'keys' => ['a', 'b', 'c'],
|
||||
'prefixes' => ['d', 'a'],
|
||||
'expected' => [],
|
||||
],
|
||||
'empty keys' => [
|
||||
'keys' => [],
|
||||
'prefixes' => ['a'],
|
||||
'expected' => [],
|
||||
],
|
||||
'empty prefixes' => [
|
||||
'keys' => ['a', 'b', 'c'],
|
||||
'prefixes' => [],
|
||||
'expected' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -915,10 +915,10 @@ class core_calendar_external extends external_api {
|
||||
$formoptions['eventtypes'] = $allowedeeventtypes;
|
||||
if ($courseid) {
|
||||
require_once($CFG->libdir . '/grouplib.php');
|
||||
$groupcoursedata = groups_get_course_data($courseid);
|
||||
if (!empty($groupcoursedata->groups)) {
|
||||
$groupcoursedata = groups_get_all_groups($courseid);
|
||||
if (!empty($groupcoursedata)) {
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = $groupdata->name;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -3639,9 +3639,9 @@ function calendar_output_fragment_event_form($args) {
|
||||
|
||||
if (is_null($eventid)) {
|
||||
if (!empty($courseid)) {
|
||||
$groupcoursedata = groups_get_course_data($courseid);
|
||||
$groupcoursedata = groups_get_all_groups($courseid);
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = format_string($groupdata->name, false, ['context' => $context]);
|
||||
}
|
||||
}
|
||||
@@ -3683,9 +3683,9 @@ function calendar_output_fragment_event_form($args) {
|
||||
$formoptions['event'] = $event;
|
||||
|
||||
if (!empty($event->courseid)) {
|
||||
$groupcoursedata = groups_get_course_data($event->courseid);
|
||||
$groupcoursedata = groups_get_all_groups($event->courseid);
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = format_string($groupdata->name, false, ['context' => $context]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,11 +48,12 @@
|
||||
<a{{!
|
||||
}} href="#"{{!
|
||||
}} class="arrow_link previous"{{!
|
||||
}} title="{{#str}}monthprev, calendar{{/str}}"{{!
|
||||
}} title="{{#str}}monthprevwithname, calendar, {{previousperiodname}}{{/str}}"{{!
|
||||
}} data-year="{{previousperiod.year}}"{{!
|
||||
}} data-month="{{previousperiod.mon}}"{{!
|
||||
}} aria-label="{{#str}}monthprevwithname, calendar, {{previousperiodname}}{{/str}}" {{!
|
||||
}}>
|
||||
<span class="arrow">{{{larrow}}}</span>
|
||||
<span class="arrow" aria-hidden="true">{{{larrow}}}</span>
|
||||
</a>
|
||||
<span class="hide"> | </span>
|
||||
<span class="current">
|
||||
@@ -62,11 +63,12 @@
|
||||
<a{{!
|
||||
}} href="#"{{!
|
||||
}} class="arrow_link next"{{!
|
||||
}} title="{{#str}}monthnext, calendar{{/str}}"{{!
|
||||
}} title="{{#str}}monthnextwithname, calendar, {{nextperiodname}}{{/str}}"{{!
|
||||
}} data-year="{{nextperiod.year}}"{{!
|
||||
}} data-month="{{nextperiod.mon}}"{{!
|
||||
}} aria-label="{{#str}}monthnextwithname, calendar, {{nextperiodname}}{{/str}}" {{!
|
||||
}}>
|
||||
<span class="arrow">{{{rarrow}}}</span>
|
||||
<span class="arrow" aria-hidden="true">{{{rarrow}}}</span>
|
||||
</a>
|
||||
{{/includenavigation}}
|
||||
{{^includenavigation}}
|
||||
|
||||
@@ -36,10 +36,11 @@
|
||||
<a{{!
|
||||
}} href="{{previousperiodlink}}"{{!
|
||||
}} class="arrow_link previous"{{!
|
||||
}} title="{{#str}}monthprev, calendar{{/str}}"{{!
|
||||
}} {{#viewinginblock}}title="{{#str}}monthprevwithname, calendar, {{previousperiodname}}{{/str}}" {{/viewinginblock}} {{!
|
||||
}} data-year="{{previousperiod.year}}"{{!
|
||||
}} data-month="{{previousperiod.mon}}"{{!
|
||||
}} data-drop-zone="nav-link" {{!
|
||||
}} aria-label="{{#str}}monthprevwithname, calendar, {{previousperiodname}}{{/str}}" {{!
|
||||
}}>
|
||||
<span class="arrow" aria-hidden="true">{{{larrow}}}</span>
|
||||
|
||||
@@ -56,10 +57,11 @@
|
||||
<a{{!
|
||||
}} href="{{nextperiodlink}}"{{!
|
||||
}} class="arrow_link next"{{!
|
||||
}} title="{{#str}}monthnext, calendar{{/str}}"{{!
|
||||
}} {{#viewinginblock}}title="{{#str}}monthnextwithname, calendar, {{nextperiodname}}{{/str}}" {{/viewinginblock}} {{!
|
||||
}} data-year="{{nextperiod.year}}"{{!
|
||||
}} data-month="{{nextperiod.mon}}"{{!
|
||||
}} data-drop-zone="nav-link" {{!
|
||||
}} aria-label="{{#str}}monthnextwithname, calendar, {{nextperiodname}}{{/str}}" {{!
|
||||
}}>
|
||||
<span class="arrow_text">{{nextperiodname}}</span>
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
parent::setUp();
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Set our timezone based on the timezone in the RFC's samples (US/Eastern).
|
||||
$tz = 'US/Eastern';
|
||||
// Set our timezone based on the timezone in the RFC's samples (America/New_York).
|
||||
$tz = 'America/New_York';
|
||||
$this->setTimezone($tz);
|
||||
$timezone = new \DateTimeZone($tz);
|
||||
// Create our event's DTSTART date based on RFC's samples (most commonly used in RFC is 1997-09-02 09:00:00 EDT).
|
||||
@@ -686,8 +686,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
|
||||
// Change the start date for forever events to 9am of the 2nd day of the current month and year.
|
||||
$this->change_event_startdate(date('Ym02\T090000'));
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart));
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
|
||||
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
$interval = new \DateInterval('P12M');
|
||||
@@ -843,8 +843,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
// Change the start date for forever events to 9am of the 2nd day of the current month and year.
|
||||
$this->change_event_startdate(date('Ym02\T090000'));
|
||||
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart));
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
|
||||
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
$interval = new \DateInterval('P12M');
|
||||
@@ -1202,8 +1202,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_other_day_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change the start date for forever events to 9am of the current date in US/Eastern time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
|
||||
// Change the start date for forever events to 9am of the current date in America/New_York time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart));
|
||||
$interval = new \DateInterval('P2D');
|
||||
@@ -1272,7 +1272,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 01-01-1998, based on the example from the RFC.
|
||||
$this->change_event_startdate('19980101T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19980101T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -1306,7 +1306,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 01-01-1998, based on the example from the RFC.
|
||||
$this->change_event_startdate('19980101T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19980101T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -1404,8 +1404,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_other_week_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change the start date for forever events to 9am of the current date in US/Eastern time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
|
||||
// Change the start date for forever events to 9am of the current date in America/New_York time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$interval = new \DateInterval('P2W');
|
||||
|
||||
@@ -1615,7 +1615,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 05-09-1997, based on the example from the RFC.
|
||||
$startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970905T090000', 'America/New_York');
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1651,7 +1651,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 05-09-1997, based on the example from the RFC.
|
||||
$startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970905T090000', 'America/New_York');
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1689,7 +1689,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 05-09-1997, based on the example from the RFC.
|
||||
$startdatetime = $this->change_event_startdate('19970907T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970907T090000', 'America/New_York');
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1732,7 +1732,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 05-09-1997, based on the example from the RFC.
|
||||
$startdatetime = $this->change_event_startdate('19970922T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970922T090000', 'America/New_York');
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1774,7 +1774,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 28 September of the current year, based on the example from the RFC.
|
||||
$this->change_event_startdate(date('Y0928\T090000'), 'US/Eastern');
|
||||
$this->change_event_startdate(date('Y0928\T090000'), 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;BYMONTHDAY=-3';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -1860,7 +1860,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_first_and_last_day_of_the_month_10_count(): void {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970930T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970930T090000', 'America/New_York');
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1905,7 +1905,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_18_months_days_10_to_15_10_count(): void {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970910T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970910T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -1950,9 +1950,9 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_tuesday_every_other_month_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change the start date for forever events to 9am of the Tuesday on or before of the current date in US/Eastern time.
|
||||
// Change the start date for forever events to 9am of the Tuesday on or before of the current date in America/New_York time.
|
||||
$nexttuesday = new \DateTime('next Tuesday');
|
||||
$this->change_event_startdate($nexttuesday->format('Ymd\T090000'), 'US/Eastern');
|
||||
$this->change_event_startdate($nexttuesday->format('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;INTERVAL=2;BYDAY=TU';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2005,7 +2005,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_yearly_in_june_july_10_count(): void {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970610T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970610T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=YEARLY;COUNT=10;BYMONTH=6,7';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2048,7 +2048,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_other_year_in_june_july_10_count(): void {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970310T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970310T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2095,7 +2095,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_3_years_1st_100th_200th_days_10_count(): void {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970101T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970101T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2145,7 +2145,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
// Change our event's date to the 20th Monday of the current year.
|
||||
$twentiethmonday = new \DateTime(date('Y-01-01'));
|
||||
$twentiethmonday->modify('+20 Monday');
|
||||
$startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T000000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T000000'), 'America/New_York');
|
||||
|
||||
$interval = new \DateInterval('P1Y');
|
||||
|
||||
@@ -2191,7 +2191,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
// Change our event's date to the start of the 20th week of the current year.
|
||||
$twentiethweek = new \DateTime(date('Y-01-01'));
|
||||
$twentiethweek->setISODate($twentiethweek->format('Y'), 20);
|
||||
$startdatetime = $this->change_event_startdate($twentiethweek->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($twentiethweek->format('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$startdate = clone($startdatetime);
|
||||
$startdate->modify($startdate->format('Y-m-d'));
|
||||
@@ -2238,9 +2238,9 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_thursday_in_march_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to the first Thursday of March of the current year at 9am US/Eastern time.
|
||||
// Change our event's date to the first Thursday of March of the current year at 9am America/New_York time.
|
||||
$firstthursdayofmarch = new \DateTime('first Thursday of March');
|
||||
$startdatetime = $this->change_event_startdate($firstthursdayofmarch->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($firstthursdayofmarch->format('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$interval = new \DateInterval('P1Y');
|
||||
|
||||
@@ -2297,9 +2297,9 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_thursday_june_july_august_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to the first Thursday of June in the current year at 9am US/Eastern time.
|
||||
// Change our event's date to the first Thursday of June in the current year at 9am America/New_York time.
|
||||
$firstthursdayofjune = new \DateTime('first Thursday of June');
|
||||
$startdatetime = $this->change_event_startdate($firstthursdayofjune->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($firstthursdayofjune->format('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
|
||||
@@ -2354,8 +2354,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_friday_the_thirteenth_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to the first Thursday of June in the current year at 9am US/Eastern time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
|
||||
// Change our event's date to the first Thursday of June in the current year at 9am America/New_York time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2391,10 +2391,10 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_first_saturday_following_first_sunday_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to the next Saturday after the first Sunday of the the current month at 9am US/Eastern time.
|
||||
// Change our event's date to the next Saturday after the first Sunday of the current month at 9am America/New_York time.
|
||||
$firstsaturdayafterfirstsunday = new \DateTime('first Sunday of this month');
|
||||
$firstsaturdayafterfirstsunday->modify('next Saturday');
|
||||
$startdatetime = $this->change_event_startdate($firstsaturdayafterfirstsunday->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($firstsaturdayafterfirstsunday->format('Ymd\T090000'), 'America/New_York');
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
$offset = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -2452,7 +2452,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
$electiondate->modify('+1 Tuesday');
|
||||
|
||||
// Use the most recent election date as the starting date of our recurring events.
|
||||
$startdatetime = $this->change_event_startdate($electiondate->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($electiondate->format('Ymd\T090000'), 'America/New_York');
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
$offset = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -2495,7 +2495,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_monthly_bysetpos_3_count(): void {
|
||||
global $DB;
|
||||
|
||||
$this->change_event_startdate('19970904T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19970904T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2532,7 +2532,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_second_to_the_last_weekday_of_the_month(): void {
|
||||
global $DB;
|
||||
|
||||
$this->change_event_startdate('19970929T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19970929T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;COUNT=7';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2757,7 +2757,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_weekly_byday_with_wkst_mo(): void {
|
||||
global $DB;
|
||||
|
||||
$this->change_event_startdate('19970805T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19970805T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2790,7 +2790,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_weekly_byday_with_wkst_su(): void {
|
||||
global $DB;
|
||||
|
||||
$this->change_event_startdate('19970805T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19970805T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2870,7 +2870,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
* Change the event's timestart (DTSTART) based on the test's needs.
|
||||
*
|
||||
* @param string $datestr The date string. In 'Ymd\This' format. e.g. 19990902T090000.
|
||||
* @param null|string $timezonestr A valid timezone string. e.g. 'US/Eastern'.
|
||||
* @param null|string $timezonestr A valid timezone string. e.g. 'America/New_York'.
|
||||
* If not provided, the default timezone will be used.
|
||||
* @return bool|DateTime
|
||||
*/
|
||||
|
||||
@@ -405,6 +405,12 @@ class core_cohort_external extends external_api {
|
||||
$results = $results + cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query);
|
||||
}
|
||||
} else if ($includes == 'all') {
|
||||
$contextsystem = context_system::instance();
|
||||
if (!$context instanceof context_system &&
|
||||
!has_any_capability(['moodle/cohort:view', 'moodle/cohort:manage'], $contextsystem)) {
|
||||
|
||||
throw new required_capability_exception($contextsystem, 'moodle/cohort:view', 'nopermissions', '');
|
||||
}
|
||||
$results = cohort_get_all_cohorts($limitfrom, $limitnum, $query);
|
||||
$results = $results['cohorts'];
|
||||
} else {
|
||||
|
||||
@@ -672,6 +672,7 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
role_assign($userrole, $catuser->id, $catcontext->id);
|
||||
|
||||
// Enrol user in the course.
|
||||
$this->getDataGenerator()->enrol_user($creator->id, $course->id);
|
||||
$this->getDataGenerator()->enrol_user($courseuser->id, $course->id, 'courserole');
|
||||
|
||||
$syscontext = array('contextid' => \context_system::instance()->id);
|
||||
@@ -758,18 +759,27 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
$this->assertCount(4, $result['cohorts']);
|
||||
|
||||
// A user in the course context with the system cohort:view capability. Check that all the system cohorts are returned.
|
||||
$this->setUser($courseuser);
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
|
||||
$this->assertCount(2, $result['cohorts']);
|
||||
$this->assertEquals('Cohortsearch 1', $result['cohorts'][$cohort1->id]->name);
|
||||
$this->assertCount(4, $result['cohorts']);
|
||||
|
||||
// A user in the course context without the ability to view system cohorts.
|
||||
$this->setUser($courseuser);
|
||||
try {
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
|
||||
$this->fail('Exception expected');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertInstanceOf(\required_capability_exception::class, $e);
|
||||
$this->assertStringContainsString('(View site-wide cohorts)', $e->getMessage());
|
||||
}
|
||||
|
||||
// Detect invalid parameter $includes.
|
||||
$this->setUser($creator);
|
||||
try {
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $syscontext, 'invalid');
|
||||
$this->fail('Invalid parameter includes');
|
||||
} catch (\coding_exception $e) {
|
||||
// All good.
|
||||
$this->fail('Exception expected');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertInstanceOf(\coding_exception::class, $e);
|
||||
$this->assertStringContainsString('Invalid parameter value for \'includes\'', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +239,7 @@ class provider implements
|
||||
$params = (isset($user)) ? ['userid' => $user->id, 'coursemoduleid' => $cmid] : ['coursemoduleid' => $cmid];
|
||||
// Only delete the record for course modules completion.
|
||||
$DB->delete_records('course_modules_completion', $params);
|
||||
$DB->delete_records('course_modules_viewed', $params);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,25 +14,19 @@
|
||||
// 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 the request helper.
|
||||
*
|
||||
* @package core_completion
|
||||
* @category test
|
||||
* @copyright 2018 Adrian Greeve <adriangreeve.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace core_completion\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/completion/tests/fixtures/completion_creation.php');
|
||||
|
||||
/**
|
||||
* Tests for the \core_completion API's provider functionality.
|
||||
*
|
||||
* @package core_completion
|
||||
* @category test
|
||||
* @covers \core_completion\privacy\provider
|
||||
* @copyright 2018 Adrian Greeve <adriangreeve.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
@@ -122,14 +116,35 @@ final class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
* Test deleting activity completion information for a user.
|
||||
*/
|
||||
public function test_delete_completion_activity_user(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->create_course_completion();
|
||||
$this->complete_course($user);
|
||||
\core_completion\privacy\provider::delete_completion($user, null, $this->cm->id);
|
||||
$activitycompletion = \core_completion\privacy\provider::get_activity_completion_info($user, $this->course,
|
||||
$this->cm);
|
||||
|
||||
$course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
|
||||
$user = $this->getDataGenerator()->create_and_enrol($course);
|
||||
|
||||
$page = $this->getDataGenerator()->create_module('page', [
|
||||
'course' => $course->id,
|
||||
'completion' => 1,
|
||||
'completionview' => 1,
|
||||
]);
|
||||
$cm = get_coursemodule_from_id('page', $page->cmid);
|
||||
|
||||
$completion = new \completion_info($course);
|
||||
$completion->set_module_viewed($cm, $user->id);
|
||||
|
||||
// Sanity test.
|
||||
$this->assertTrue($DB->record_exists('course_modules_completion', ['userid' => $user->id, 'coursemoduleid' => $cm->id]));
|
||||
$this->assertTrue($DB->record_exists('course_modules_viewed', ['userid' => $user->id, 'coursemoduleid' => $cm->id]));
|
||||
|
||||
provider::delete_completion($user, null, $cm->id);
|
||||
|
||||
$activitycompletion = provider::get_activity_completion_info($user, $course, $cm);
|
||||
$this->assertEquals(0, $activitycompletion->completionstate);
|
||||
|
||||
// Assert we cleared that data.
|
||||
$this->assertFalse($DB->record_exists('course_modules_completion', ['userid' => $user->id, 'coursemoduleid' => $cm->id]));
|
||||
$this->assertFalse($DB->record_exists('course_modules_viewed', ['userid' => $user->id, 'coursemoduleid' => $cm->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Generated
+6
-6
@@ -4037,16 +4037,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v6.4.4",
|
||||
"version": "v6.4.33",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "710e27879e9be3395de2b98da3f52a946039f297"
|
||||
"reference": "596cc67dc84d1718679240e0805dd50dbbd33992"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/710e27879e9be3395de2b98da3f52a946039f297",
|
||||
"reference": "710e27879e9be3395de2b98da3f52a946039f297",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/596cc67dc84d1718679240e0805dd50dbbd33992",
|
||||
"reference": "596cc67dc84d1718679240e0805dd50dbbd33992",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4078,7 +4078,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v6.4.4"
|
||||
"source": "https://github.com/symfony/process/tree/v6.4.33"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -4094,7 +4094,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-02-20T12:31:00+00:00"
|
||||
"time": "2026-01-28T10:02:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
|
||||
+100
-89
@@ -38,94 +38,106 @@ $CFG = new stdClass();
|
||||
// will be stored. This database must already have been created //
|
||||
// and a username/password created to access it. //
|
||||
|
||||
$CFG->dbtype = 'pgsql'; // 'pgsql', 'mariadb', 'mysqli', 'auroramysql', 'sqlsrv' or 'oci'
|
||||
$CFG->dblibrary = 'native'; // 'native' only at the moment
|
||||
$CFG->dbhost = 'localhost'; // eg 'localhost' or 'db.isp.com' or IP
|
||||
$CFG->dbname = 'moodle'; // database name, eg moodle
|
||||
$CFG->dbuser = 'username'; // your database username
|
||||
$CFG->dbpass = 'password'; // your database password
|
||||
$CFG->prefix = 'mdl_'; // prefix to use for all table names
|
||||
$CFG->dboptions = array(
|
||||
'dbpersist' => false, // should persistent database connections be
|
||||
// used? set to 'false' for the most stable
|
||||
// setting, 'true' can improve performance
|
||||
// sometimes
|
||||
'dbsocket' => false, // should connection via UNIX socket be used?
|
||||
// if you set it to 'true' or custom path
|
||||
// here set dbhost to 'localhost',
|
||||
// (please note mysql is always using socket
|
||||
// if dbhost is 'localhost' - if you need
|
||||
// local port connection use '127.0.0.1')
|
||||
'dbport' => '', // the TCP port number to use when connecting
|
||||
// to the server. keep empty string for the
|
||||
// default port
|
||||
'dbhandlesoptions' => false,// On PostgreSQL poolers like pgbouncer don't
|
||||
// support advanced options on connection.
|
||||
// If you set those in the database then
|
||||
// the advanced settings will not be sent.
|
||||
'dbcollation' => 'utf8mb4_unicode_ci', // MySQL has partial and full UTF-8
|
||||
// support. If you wish to use partial UTF-8
|
||||
// (three bytes) then set this option to
|
||||
// 'utf8_unicode_ci'. If using the recommended
|
||||
// settings with full UTF-8 support this should
|
||||
// be set to 'utf8mb4_unicode_ci'. This option
|
||||
// should be removed for all other databases.
|
||||
// 'versionfromdb' => false, // On MySQL and MariaDB, this can force
|
||||
// the DB version to be evaluated using
|
||||
// the VERSION function instead of the version
|
||||
// provided by the PHP client which could be
|
||||
// wrong based on the DB server infrastructure,
|
||||
// e.g. PaaS on Azure. Default is false/unset.
|
||||
// Uncomment and set to true to force MySQL and
|
||||
// MariaDB to use 'SELECT VERSION();'.
|
||||
// 'extrainfo' => [], // Extra information for the DB driver, e.g. SQL Server,
|
||||
// has additional configuration according to its environment,
|
||||
// which the administrator can specify to alter and
|
||||
// override any connection options.
|
||||
// 'ssl' => '', // A connection mode string from the list below.
|
||||
// Not supported by all drivers.
|
||||
// prefer Use SSL if available - postgres default Postgres only
|
||||
// disable Force non secure connection Postgres only
|
||||
// require Force SSL Postgres and MySQL
|
||||
// verify-full Force SSL and verify root CA Postgres and MySQL
|
||||
// All mode names are adopted from Postgres
|
||||
// and other databases align where possible:
|
||||
// Postgres: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE
|
||||
// MySql: https://www.php.net/manual/en/mysqli.real-connect.php
|
||||
// It is worth noting that for MySQL require and verify-full are the same - in both cases
|
||||
// verification will take place if you specify hostname as a name,
|
||||
// and it will be omitted if you put an IP address.
|
||||
// 'fetchbuffersize' => 100000, // On PostgreSQL, this option sets a limit
|
||||
// on the number of rows that are fetched into
|
||||
// memory when doing a large recordset query
|
||||
// (e.g. search indexing). Default is 100000.
|
||||
// Uncomment and set to a value to change it,
|
||||
// or zero to turn off the limit. You need to
|
||||
// set to zero if you are using pg_bouncer in
|
||||
// 'transaction' mode (it is fine in 'session'
|
||||
// mode).
|
||||
// 'clientcompress' => true // Use compression protocol to communicate with the database server.
|
||||
// Decreases traffic from the database server.
|
||||
// Not needed if the databse is on the same host.
|
||||
// Currently supported only with mysqli, mariadb, and aurora drivers.
|
||||
$CFG->dbtype = 'pgsql'; // 'pgsql', 'mariadb', 'mysqli', 'auroramysql', 'sqlsrv' or 'oci'
|
||||
$CFG->dblibrary = 'native'; // 'native' only at the moment
|
||||
$CFG->dbhost = 'localhost'; // eg 'localhost' or 'db.isp.com' or IP
|
||||
$CFG->dbname = 'moodle'; // database name, eg moodle
|
||||
$CFG->dbuser = 'username'; // your database username
|
||||
$CFG->dbpass = 'password'; // your database password
|
||||
$CFG->prefix = 'mdl_'; // prefix to use for all table names
|
||||
$CFG->dboptions = [
|
||||
'dbpersist' => false, // Should persistent database connections be
|
||||
// used? Set to 'false' for the most stable
|
||||
// setting, 'true' can improve performance
|
||||
// sometimes
|
||||
'dbsocket' => false, // Should connection via UNIX socket be used?
|
||||
// if you set it to 'true' or custom path
|
||||
// here set dbhost to 'localhost',
|
||||
// (please note mysql is always using socket
|
||||
// if dbhost is 'localhost' - if you need
|
||||
// local port connection use '127.0.0.1')
|
||||
'dbport' => '', // The TCP port number to use when connecting
|
||||
// to the server. Keep empty string for the
|
||||
// default port
|
||||
'dbhandlesoptions' => false, // On PostgreSQL poolers like pgbouncer don't
|
||||
// support advanced options on connection.
|
||||
// If you set those in the database then
|
||||
// the advanced settings will not be sent.
|
||||
'dbcollation' => 'utf8mb4_unicode_ci', // MySQL has partial and full UTF-8
|
||||
// support. If you wish to use partial UTF-8
|
||||
// (three bytes) then set this option to
|
||||
// 'utf8_unicode_ci'. If using the recommended
|
||||
// settings with full UTF-8 support this should
|
||||
// be set to 'utf8mb4_unicode_ci'. This option
|
||||
// should be removed for all other databases.
|
||||
// 'dbschema' => '', // On PostgreSQL this sets the database schema to use
|
||||
// for the connection.
|
||||
// 'dbtransactions' => null // Set this to true to explicitly enable database transactions
|
||||
// for MySQL. By default only the following storage engines are
|
||||
// configured to use transactions: InnoDB, INNOBASE, BDB, XtraDB,
|
||||
// Aria, Falcon.
|
||||
// 'versionfromdb' => false, // On MySQL and MariaDB, this can force
|
||||
// the DB version to be evaluated using
|
||||
// the VERSION function instead of the version
|
||||
// provided by the PHP client which could be
|
||||
// wrong based on the DB server infrastructure,
|
||||
// e.g. PaaS on Azure. Default is false/unset.
|
||||
// Uncomment and set to true to force MySQL and
|
||||
// MariaDB to use 'SELECT VERSION();'.
|
||||
// 'extrainfo' => [], // Extra information for the DB driver, e.g. SQL Server,
|
||||
// has additional configuration according to its environment,
|
||||
// which the administrator can specify to alter and
|
||||
// override any connection options.
|
||||
// 'ssl' => '', // A connection mode string from the list below.
|
||||
// Not supported by all drivers.
|
||||
// prefer Use SSL if available - postgres default Postgres only
|
||||
// disable Force non secure connection Postgres only
|
||||
// require Force SSL Postgres and MySQL
|
||||
// verify-full Force SSL and verify root CA Postgres and MySQL
|
||||
// All mode names are adopted from Postgres
|
||||
// and other databases align where possible:
|
||||
// Postgres: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE
|
||||
// MySQL: https://www.php.net/manual/en/mysqli.real-connect.php
|
||||
// It is worth noting that for MySQL require and verify-full are the same - in both cases
|
||||
// verification will take place if you specify hostname as a name,
|
||||
// and it will be omitted if you put an IP address.
|
||||
// 'fetchbuffersize' => 100000, // On PostgreSQL, this option sets a limit
|
||||
// on the number of rows that are fetched into
|
||||
// memory when doing a large recordset query
|
||||
// (e.g. search indexing). Default is 100000.
|
||||
// Uncomment and set to a value to change it,
|
||||
// or zero to turn off the limit. You need to
|
||||
// set to zero if you are using pg_bouncer in
|
||||
// 'transaction' mode (it is fine in 'session'
|
||||
// mode).
|
||||
// 'clientcompress' => true // Use compression protocol to communicate with the database server.
|
||||
// Decreases traffic from the database server.
|
||||
// Not needed if the databse is on the same host.
|
||||
// Currently supported only with mysqli, mariadb, and aurora drivers.
|
||||
// 'connecttimeout' => null, // Set connect timeout in seconds. Not all drivers support it.
|
||||
// 'logall' => false, // Log all queries to the database. They are stored in the table log_queries.
|
||||
// 'logslow' => 0, // Log slow queries to the database. They are stored in the table log_queries.
|
||||
// This parameter controls the threshold in milliseconds and requires an integer,
|
||||
// not a string!
|
||||
// 'logerror' => false, // Log erroneous queries to the database. They are stored in the table log_queries.
|
||||
// 'bulkinsertsize' => null, // Set the bulk insert size for database operations. MySQL has a relatively small
|
||||
// query length limit by default, make sure 'max_allowed_packet' in my.cnf is
|
||||
// high enough if you set this setting.
|
||||
// 'readonly' => [ // Set to read-only slave details, to get safe reads
|
||||
// from there instead of the master node. Optional.
|
||||
// Currently supported by pgsql and mysqli variety classes.
|
||||
// If not supported silently ignored.
|
||||
// 'instance' => [ // Readonly slave connection parameters
|
||||
// [
|
||||
// 'dbhost' => 'slave.dbhost',
|
||||
// 'dbport' => '', // Defaults to master port
|
||||
// 'dbuser' => '', // Defaults to master user
|
||||
// 'dbpass' => '', // Defaults to master password
|
||||
// ],
|
||||
// [...],
|
||||
// ],
|
||||
/*
|
||||
'connecttimeout' => null, // Set connect timeout in seconds. Not all drivers support it.
|
||||
'readonly' => [ // Set to read-only slave details, to get safe reads
|
||||
// from there instead of the master node. Optional.
|
||||
// Currently supported by pgsql and mysqli variety classes.
|
||||
// If not supported silently ignored.
|
||||
'instance' => [ // Readonly slave connection parameters
|
||||
[
|
||||
'dbhost' => 'slave.dbhost',
|
||||
'dbport' => '', // Defaults to master port
|
||||
'dbuser' => '', // Defaults to master user
|
||||
'dbpass' => '', // Defaults to master password
|
||||
],
|
||||
[...],
|
||||
],
|
||||
|
||||
Instance(s) can alternatively be specified as:
|
||||
|
||||
'instance' => 'slave.dbhost',
|
||||
'instance' => ['slave.dbhost1', 'slave.dbhost2'],
|
||||
'instance' => ['dbhost' => 'slave.dbhost', 'dbport' => '', 'dbuser' => '', 'dbpass' => ''],
|
||||
@@ -146,9 +158,8 @@ $CFG->dboptions = array(
|
||||
|
||||
More info available in lib/dml/moodle_read_slave_trait.php where the feature is implemented.
|
||||
]
|
||||
*/
|
||||
// For all database config settings see https://docs.moodle.org/en/Database_settings
|
||||
);
|
||||
*/
|
||||
];
|
||||
|
||||
|
||||
//=========================================================================
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@core @core_contentbank @core_h5p @contenttype_h5p @_switch_iframe @javascript
|
||||
@core @core_contentbank @core_h5p @contenttype_h5p @_switch_iframe
|
||||
Feature: H5P file upload to content bank for admins
|
||||
In order import new H5P content to content bank
|
||||
As an admin
|
||||
@@ -16,6 +16,7 @@ Feature: H5P file upload to content bank for admins
|
||||
And I expand "Site pages" node
|
||||
And I click on "Content bank" "link"
|
||||
|
||||
@javascript
|
||||
Scenario: Admins can upload .h5p extension files to content bank
|
||||
Given I should not see "filltheblanks.h5p"
|
||||
When I click on "Upload" "link"
|
||||
@@ -27,6 +28,7 @@ Feature: H5P file upload to content bank for admins
|
||||
And I wait until the page is ready
|
||||
Then I should see "filltheblanks.h5p"
|
||||
|
||||
@javascript
|
||||
Scenario: Admins can see uploaded H5P contents
|
||||
Given I should not see "filltheblanks.h5p"
|
||||
When I click on "Upload" "link"
|
||||
@@ -40,6 +42,7 @@ Feature: H5P file upload to content bank for admins
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
Then I should see "Of which countries"
|
||||
|
||||
@javascript
|
||||
Scenario: Users can't see content managed by disabled plugins
|
||||
Given I click on "Upload" "link"
|
||||
And I click on "Choose a file..." "button"
|
||||
@@ -55,6 +58,7 @@ Feature: H5P file upload to content bank for admins
|
||||
When I navigate to "Plugins > Content bank" in site administration
|
||||
Then I should not see "filltheblanks.h5p"
|
||||
|
||||
@javascript
|
||||
Scenario: Contents in a context are not available from other contexts
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
@@ -75,6 +79,7 @@ Feature: H5P file upload to content bank for admins
|
||||
And I click on "Content bank" "link"
|
||||
And I should not see "filltheblanks.h5p"
|
||||
|
||||
@javascript
|
||||
Scenario: Admins can upload and deployed content types when libraries are not installed
|
||||
Given I navigate to "H5P > Manage H5P content types" in site administration
|
||||
And I should not see "Fill in the Blanks"
|
||||
@@ -95,6 +100,7 @@ Feature: H5P file upload to content bank for admins
|
||||
And I navigate to "H5P > Manage H5P content types" in site administration
|
||||
And I should see "Fill in the Blanks"
|
||||
|
||||
@javascript
|
||||
Scenario: Uploading invalid packages throws error
|
||||
Given the following "user private files" exist:
|
||||
| user | filepath |
|
||||
@@ -122,3 +128,28 @@ Feature: H5P file upload to content bank for admins
|
||||
And I wait until the page is ready
|
||||
And I should see "It is not possible to unzip it"
|
||||
And I should not see "Sorry, this file is not valid"
|
||||
|
||||
@_file_upload @javascript
|
||||
Scenario: Uploading invalid file types is not allowed
|
||||
Given I am on the "Content bank" page logged in as "admin"
|
||||
When I click on "Upload" "link"
|
||||
And I click on "Choose a file..." "button"
|
||||
And I click on "Upload a file" "link" in the ".fp-repo-area" "css_element"
|
||||
And I set the field "Attachment" to "#dirroot#/course/tests/fixtures/image.jpg"
|
||||
And I press "Upload this file"
|
||||
# Confirm that jpg files cannot be added to content bank.
|
||||
Then "Image (JPEG) filetype cannot be accepted." "text" should exist
|
||||
And I click on "OK" "button" in the "Error" "dialogue"
|
||||
And I click on "Close" "button" in the "File picker" "dialogue"
|
||||
And I click on "Cancel" "button" in the "Upload" "dialogue"
|
||||
# Confirm that jpg file was not added to the content bank.
|
||||
And "No content available" "text" should exist
|
||||
And I should not see "1.jpg"
|
||||
|
||||
Scenario: File upload for content bank can be disabled by admin
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Plugins > Content bank > Manage content types" in site administration
|
||||
When I follow "Disable"
|
||||
And I am on the "Content bank" page
|
||||
# Confirm that "Upload" does not exist after admin disabled from Plugins > Content bank > Manage content types.
|
||||
Then "Upload" "link" should not exist
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
@core @core_contentbank @core_h5p @contentbank_h5p
|
||||
Feature: Content bank contents are retained when course is restored
|
||||
In order to restore content bank contents
|
||||
As a manager
|
||||
I need to be able to restore course containing the content bank
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| admin | C1 | editingteacher |
|
||||
And the following "contentbank content" exist:
|
||||
| contextlevel | reference | contenttype | user | contentname | filepath |
|
||||
| Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And the following config values are set as admin:
|
||||
| unaddableblocks | | theme_boost|
|
||||
# TODO MDL-57120 site "Content bank" link not accessible without navigation block.
|
||||
And I add the "Navigation" block if not present
|
||||
|
||||
@javascript
|
||||
Scenario: Deleted courses with content banks can be restored
|
||||
Given I navigate to "Courses > Manage courses and categories" in site administration
|
||||
And I click on "delete" action for "Course 1" in management course listing
|
||||
And I click on "Delete" "button" in the "Confirm" "dialogue"
|
||||
And I press "Continue"
|
||||
And I navigate to "Recycle bin" in current page administration
|
||||
And I click on "Restore" "link" in the "Course 1" "table_row"
|
||||
And I am on "Course 1" course homepage
|
||||
And I expand "Site pages" node
|
||||
When I click on "Content bank" "link"
|
||||
And I click on "filltheblanks.h5p" "link"
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
Then I should see "Of which countries are Berlin, Washington, Beijing, Canberra and Brasilia the capitals?"
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -22,10 +22,11 @@
|
||||
*/
|
||||
|
||||
import $ from 'jquery';
|
||||
import CustomEvents from 'core/custom_interaction_events';
|
||||
import * as ModalEvents from 'core/modal_events';
|
||||
import selectors from 'core_course/local/activitychooser/selectors';
|
||||
import * as Templates from 'core/templates';
|
||||
import {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';
|
||||
import {enter, space} from 'core/key_codes';
|
||||
import {addIconToContainer} from 'core/loadingicon';
|
||||
import * as Repository from 'core_course/local/activitychooser/repository';
|
||||
import Notification from 'core/notification';
|
||||
@@ -246,7 +247,54 @@ const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOption
|
||||
const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container);
|
||||
|
||||
Array.from(chooserOptions).forEach((element) => {
|
||||
return element.addEventListener('keydown', (e) => {
|
||||
const $element = $(element);
|
||||
|
||||
// Set up custom interaction events for RTL-aware keyboard navigation.
|
||||
CustomEvents.define($element, [
|
||||
CustomEvents.events.next,
|
||||
CustomEvents.events.previous,
|
||||
CustomEvents.events.home,
|
||||
CustomEvents.events.end,
|
||||
]);
|
||||
|
||||
// Handle focus move (automatically handles RTL).
|
||||
const createNavHandler = (resolver) => (e, data) => {
|
||||
const currentOption = data.originalEvent.target.closest(
|
||||
selectors.regions.chooserOption.container
|
||||
);
|
||||
if (currentOption !== null) {
|
||||
const toFocusOption = resolver(currentOption);
|
||||
if (toFocusOption) {
|
||||
focusChooserOption(toFocusOption, currentOption);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$element.on(
|
||||
CustomEvents.events.next,
|
||||
createNavHandler(
|
||||
(current) => current.nextElementSibling || chooserOptionsContainer.firstElementChild
|
||||
)
|
||||
);
|
||||
|
||||
$element.on(
|
||||
CustomEvents.events.previous,
|
||||
createNavHandler(
|
||||
(current) => current.previousElementSibling || chooserOptionsContainer.lastElementChild
|
||||
)
|
||||
);
|
||||
|
||||
$element.on(
|
||||
CustomEvents.events.home,
|
||||
createNavHandler(() => chooserOptionsContainer.firstElementChild)
|
||||
);
|
||||
|
||||
$element.on(
|
||||
CustomEvents.events.end,
|
||||
createNavHandler(() => chooserOptionsContainer.lastElementChild)
|
||||
);
|
||||
|
||||
element.addEventListener('keydown', (e) => {
|
||||
|
||||
// Check for enter/ space triggers for showing the help.
|
||||
if (e.keyCode === enter || e.keyCode === space) {
|
||||
@@ -267,40 +315,6 @@ const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOption
|
||||
showModuleHelp(carousel, moduleData, modal);
|
||||
}
|
||||
}
|
||||
|
||||
// Next.
|
||||
if (e.keyCode === arrowRight) {
|
||||
e.preventDefault();
|
||||
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
|
||||
const nextOption = currentOption.nextElementSibling;
|
||||
const firstOption = chooserOptionsContainer.firstElementChild;
|
||||
const toFocusOption = clickErrorHandler(nextOption, firstOption);
|
||||
focusChooserOption(toFocusOption, currentOption);
|
||||
}
|
||||
|
||||
// Previous.
|
||||
if (e.keyCode === arrowLeft) {
|
||||
e.preventDefault();
|
||||
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
|
||||
const previousOption = currentOption.previousElementSibling;
|
||||
const lastOption = chooserOptionsContainer.lastElementChild;
|
||||
const toFocusOption = clickErrorHandler(previousOption, lastOption);
|
||||
focusChooserOption(toFocusOption, currentOption);
|
||||
}
|
||||
|
||||
if (e.keyCode === home) {
|
||||
e.preventDefault();
|
||||
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
|
||||
const firstOption = chooserOptionsContainer.firstElementChild;
|
||||
focusChooserOption(firstOption, currentOption);
|
||||
}
|
||||
|
||||
if (e.keyCode === end) {
|
||||
e.preventDefault();
|
||||
const currentOption = e.target.closest(selectors.regions.chooserOption.container);
|
||||
const lastOption = chooserOptionsContainer.lastElementChild;
|
||||
focusChooserOption(lastOption, currentOption);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -348,22 +362,6 @@ const toggleFocusableChooserOption = (chooserOption, isFocusable) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Small error handling function to make sure the navigated to object exists
|
||||
*
|
||||
* @method clickErrorHandler
|
||||
* @param {HTMLElement} item What we want to check exists
|
||||
* @param {HTMLElement} fallback If we dont match anything fallback the focus
|
||||
* @return {HTMLElement}
|
||||
*/
|
||||
const clickErrorHandler = (item, fallback) => {
|
||||
if (item !== null) {
|
||||
return item;
|
||||
} else {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the search results in a defined container
|
||||
*
|
||||
|
||||
+57
-30
@@ -3227,41 +3227,68 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
|
||||
return $parentcat;
|
||||
}
|
||||
|
||||
// Get all course category contexts that are children of the parent category's context where
|
||||
// a) there is a role assignment for the current user or
|
||||
// b) there are role capability overrides for a role that the user has in this context.
|
||||
// We never need to return the system context because it cannot be a child of another context.
|
||||
// Build portable SQL parts.
|
||||
$fields = array_keys(array_filter(self::$coursecatfields));
|
||||
$ctxselect = context_helper::get_preload_record_columns_sql('ctx');
|
||||
$rs = $DB->get_recordset_sql("
|
||||
SELECT cc.". join(',cc.', $fields). ", $ctxselect
|
||||
FROM {course_categories} cc
|
||||
JOIN {context} ctx ON cc.id = ctx.instanceid AND ctx.contextlevel = :contextcoursecat1
|
||||
JOIN {role_assignments} ra ON ra.contextid = ctx.id
|
||||
WHERE ctx.path LIKE :parentpath1
|
||||
AND ra.userid = :userid1
|
||||
UNION
|
||||
SELECT cc.". join(',cc.', $fields). ", $ctxselect
|
||||
FROM {course_categories} cc
|
||||
JOIN {context} ctx ON cc.id = ctx.instanceid AND ctx.contextlevel = :contextcoursecat2
|
||||
JOIN {role_capabilities} rc ON rc.contextid = ctx.id
|
||||
JOIN {role_assignments} rc_ra ON rc_ra.roleid = rc.roleid
|
||||
JOIN {context} rc_ra_ctx ON rc_ra_ctx.id = rc_ra.contextid
|
||||
WHERE ctx.path LIKE :parentpath2
|
||||
AND rc_ra.userid = :userid2
|
||||
AND (ctx.path = rc_ra_ctx.path OR ctx.path LIKE " . $DB->sql_concat("rc_ra_ctx.path", "'/%'") . ")
|
||||
", [
|
||||
'contextcoursecat1' => CONTEXT_COURSECAT,
|
||||
'contextcoursecat2' => CONTEXT_COURSECAT,
|
||||
'parentpath1' => $parentcat->get_context()->path . '/%',
|
||||
'parentpath2' => $parentcat->get_context()->path . '/%',
|
||||
'userid1' => $USER->id,
|
||||
'userid2' => $USER->id
|
||||
]);
|
||||
|
||||
// Check if user has required capabilities in any of the contexts.
|
||||
// Paths under the given parent context path.
|
||||
$parentpath = $parentcat->get_context()->path . '/%';
|
||||
$likeparent = $DB->sql_like('path', ':parentpath', false);
|
||||
|
||||
// Join predicate for descendant-or-equal between ctx.path and uc.upath.
|
||||
$eqpaths = $DB->sql_compare_text('ctx.path') . ' = ' . $DB->sql_compare_text('uc.upath');
|
||||
$likechild = "ctx.path LIKE " . $DB->sql_concat('uc.upath', "'/%'");
|
||||
|
||||
// NOTE: This query intentionally uses a CTE (WITH clause).
|
||||
// CTEs are supported by PostgreSQL, MySQL 8+, and SQL Server, and Moodle allows
|
||||
// pass-through SQL when needed.
|
||||
//
|
||||
// Moodle generally avoids using raw SQL features that are not represented in
|
||||
// the database abstraction layer, but in this case the CTE delivers a substantial
|
||||
// performance improvement for large datasets.
|
||||
//
|
||||
// IMPORTANT: This is an exception, not a precedent. The broader use of CTEs
|
||||
// and potential abstraction-layer support will be discussed separately.
|
||||
$sql = "
|
||||
WITH ctx AS (
|
||||
SELECT id, instanceid, path, depth, contextlevel, locked
|
||||
FROM {context}
|
||||
WHERE contextlevel = :ctxlevel
|
||||
AND $likeparent
|
||||
),
|
||||
user_ctx AS (
|
||||
SELECT DISTINCT c.path AS upath, ra.roleid
|
||||
FROM {role_assignments} ra
|
||||
JOIN {context} c ON c.id = ra.contextid
|
||||
WHERE ra.userid = :userid
|
||||
)
|
||||
SELECT DISTINCT cc." . join(', cc.', $fields) . ", $ctxselect
|
||||
FROM {course_categories} cc
|
||||
JOIN ctx ON cc.id = ctx.instanceid
|
||||
LEFT JOIN {role_assignments} ra
|
||||
ON ra.userid = :userid2
|
||||
AND ra.contextid = ctx.id
|
||||
LEFT JOIN {role_capabilities} rc
|
||||
ON rc.contextid = ctx.id
|
||||
LEFT JOIN user_ctx uc
|
||||
ON uc.roleid = rc.roleid
|
||||
AND ( $eqpaths OR $likechild )
|
||||
WHERE (ra.id IS NOT NULL OR uc.upath IS NOT NULL)
|
||||
ORDER BY cc.sortorder, cc.id
|
||||
";
|
||||
|
||||
$params = [
|
||||
'ctxlevel' => CONTEXT_COURSECAT,
|
||||
'parentpath' => $parentpath,
|
||||
'userid' => $USER->id,
|
||||
'userid2' => $USER->id,
|
||||
];
|
||||
|
||||
// Stream results and pick the first subcategory that actually passes capability checks.
|
||||
$tocache = [];
|
||||
$result = null;
|
||||
|
||||
$rs = $DB->get_recordset_sql($sql, $params);
|
||||
foreach ($rs as $record) {
|
||||
$subcategory = new self($record);
|
||||
$tocache[$subcategory->id] = $subcategory;
|
||||
|
||||
@@ -133,7 +133,8 @@ class course_content_item_exporter extends exporter {
|
||||
'title' => $this->contentitem->get_title()->get_value(),
|
||||
'link' => $this->contentitem->get_link()->out(false),
|
||||
'icon' => $this->contentitem->get_icon(),
|
||||
'help' => format_text($this->contentitem->get_help(), FORMAT_MARKDOWN),
|
||||
// Help text should not be parsed using course filters.
|
||||
'help' => format_text($this->contentitem->get_help(), FORMAT_MARKDOWN, ['filter' => false]),
|
||||
'archetype' => $this->contentitem->get_archetype(),
|
||||
'componentname' => $this->contentitem->get_component_name(),
|
||||
'favourite' => $favourite,
|
||||
|
||||
@@ -51,7 +51,9 @@ class content_item_readonly_repository implements content_item_readonly_reposito
|
||||
if ($sm->string_exists('modulename_help', $modname)) {
|
||||
$help = get_string('modulename_help', $modname);
|
||||
if ($sm->string_exists('modulename_link', $modname)) { // Link to further info in Moodle docs.
|
||||
$link = get_string('modulename_link', $modname);
|
||||
// The link is stored in a language file but should not be translated, use value for English.
|
||||
$link = $sm->get_string('modulename_link', $modname, null, 'en');
|
||||
// The text 'More help' and other strings should be in the current language.
|
||||
$linktext = get_string('morehelp');
|
||||
$arialabel = get_string('morehelpaboutmodule', '', get_string('modulename', $modname));
|
||||
$doclink = $OUTPUT->doc_link($link, $linktext, true, ['aria-label' => $arialabel]);
|
||||
|
||||
@@ -343,6 +343,10 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
$actions = \core_course\management\helper::get_category_listitem_actions($category);
|
||||
}
|
||||
$menu = new action_menu();
|
||||
$label = get_string('actionsmenu');
|
||||
$actionicon = $this->output->pix_icon('t/edit_menu', '') . html_writer::span($label, 'sr-only');
|
||||
$menu->set_menu_trigger($actionicon, 'iconsmall actionmenu');
|
||||
$menu->triggerattributes['title'] = $label;
|
||||
$menu->attributes['class'] .= ' category-item-actions item-actions';
|
||||
$hasitems = false;
|
||||
foreach ($actions as $key => $action) {
|
||||
|
||||
@@ -896,8 +896,12 @@ class core_course_external extends external_api {
|
||||
'showreports' => new external_value(PARAM_INT,
|
||||
'are activity report shown (yes = 1, no =0)', VALUE_DEFAULT,
|
||||
$courseconfig->showreports),
|
||||
'visible' => new external_value(PARAM_INT,
|
||||
'1: available to student, 0:not available', VALUE_OPTIONAL),
|
||||
'visible' => new external_value(
|
||||
PARAM_INT,
|
||||
'1: available to student, 0: not available',
|
||||
VALUE_DEFAULT,
|
||||
$courseconfig->visible,
|
||||
),
|
||||
'hiddensections' => new external_value(PARAM_INT,
|
||||
'(deprecated, use courseformatoptions) How the hidden sections in the course are displayed to students',
|
||||
VALUE_OPTIONAL),
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user