Compare commits
675 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb57854de5 | |||
| 4fa5e7e6ea | |||
| aedff688e9 | |||
| a7a6d32f3b | |||
| 1d39913329 | |||
| 1cdbf1089b | |||
| b1aacfaab4 | |||
| 4260b9a0bf | |||
| 7e150c3221 | |||
| 4d8935e15e | |||
| 8926183e8e | |||
| 8288ac9e11 | |||
| 6bd427fad3 | |||
| 03608d8fc6 | |||
| 01ae2a52cb | |||
| ba49df6fbb | |||
| 2c1e8c2d2d | |||
| 828b8f51b4 | |||
| 9b5861e5d1 | |||
| b4dddbfd75 | |||
| d7eb914233 | |||
| 9e62c289ce | |||
| 564da75657 | |||
| e7582fab55 | |||
| 47e76e08b6 | |||
| 8126a90efb | |||
| e8b7ff09db | |||
| 71b78c2d21 | |||
| f35845fcad | |||
| a76cc39623 | |||
| f896d4b381 | |||
| 3ca9f3a5e0 | |||
| e77c82dc1d | |||
| 857fa9d371 | |||
| 1abad7745b | |||
| 4449bd5399 | |||
| b7dcc4e960 | |||
| b1d5327464 | |||
| 1f2ecfb129 | |||
| e05f5eb00e | |||
| d348e7a481 | |||
| 67b633fa5b | |||
| e14847283c | |||
| 8bbcf2a84c | |||
| c5b9e1af04 | |||
| d52b85dfb5 | |||
| 971fc7f1a8 | |||
| 465cbf2813 | |||
| 0f51395604 | |||
| b9f78d526d | |||
| 0528058a20 | |||
| 10837f283a | |||
| 920eb9e0eb | |||
| 56d29efbd1 | |||
| 17ccafb6f0 | |||
| 6bfb046d86 | |||
| 1619586794 | |||
| 86079406d9 | |||
| 8a79c5fb4b | |||
| 27c9a39712 | |||
| 5a75954abb | |||
| 58168c958c | |||
| 80e9a89704 | |||
| df91fc31a3 | |||
| 4b26a84586 | |||
| 6ee622ad13 | |||
| f7cf98ea86 | |||
| 8c1fe31370 | |||
| fe92590370 | |||
| 1718b04587 | |||
| 44cb65c5a3 | |||
| dc7dea0cf2 | |||
| 81a7de4721 | |||
| 4027f44c77 | |||
| 5c572f8a6b | |||
| 08cc1f3d32 | |||
| 5a1fb35680 | |||
| 6f18148684 | |||
| 6e6d5fa3cf | |||
| 37170e5714 | |||
| fb680bb33d | |||
| 4c872ab444 | |||
| 7cbd576777 | |||
| 2c8cfb9e13 | |||
| 1bfe81f05d | |||
| 2cf09ff8f3 | |||
| d9284eee0f | |||
| aa1723e748 | |||
| c079d2c3ea | |||
| 39aa019a86 | |||
| 882235917f | |||
| e12ffcdda0 | |||
| b2c2f2a0c5 | |||
| 7a7f029149 | |||
| 6cfefde94b | |||
| be8d3d004f | |||
| d42c5d9311 | |||
| 1c0e0f55bf | |||
| c5aebec657 | |||
| 4e689e468e | |||
| 6b4d33a98f | |||
| 506a4a33d4 | |||
| afe3c2b88f | |||
| 1a8415f1f1 | |||
| caab968624 | |||
| f3748c1006 | |||
| 72f49e4c76 | |||
| 4e8e5ca6ca | |||
| 3cf9457a36 | |||
| 31d96fb54f | |||
| 43874f1138 | |||
| 77e3a9f987 | |||
| 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 | |||
| 23a0e2a9a4 | |||
| d88cbc344d | |||
| 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"
|
||||
|
||||
+21
-20
@@ -56,6 +56,14 @@ jobs:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
postgres:
|
||||
image: ${{ matrix.db == 'pgsql' && 'postgres:13' || '' }}
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_PASSWORD: test
|
||||
ports:
|
||||
- 5432:5432
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -71,27 +79,20 @@ jobs:
|
||||
db: pgsql
|
||||
|
||||
steps:
|
||||
- name: Setting up DB mysql
|
||||
- name: Run MySQL Server
|
||||
if: ${{ matrix.db == 'mysqli' }}
|
||||
uses: moodlehq/mysql-action@v1
|
||||
with:
|
||||
collation server: utf8mb4_bin
|
||||
mysql version: 8.0
|
||||
mysql database: test
|
||||
mysql user: test
|
||||
mysql password: test
|
||||
use tmpfs: true
|
||||
tmpfs size: '1024M'
|
||||
extra conf: --skip-log-bin
|
||||
|
||||
- name: Setting up DB pgsql
|
||||
if: ${{ matrix.db == 'pgsql' }}
|
||||
uses: m4nu56/postgresql-action@v1
|
||||
with:
|
||||
postgresql version: 13
|
||||
postgresql db: test
|
||||
postgresql user: test
|
||||
postgresql password: test
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e MYSQL_DATABASE=test \
|
||||
-e MYSQL_USER=test \
|
||||
-e MYSQL_PASSWORD=test \
|
||||
-e MYSQL_ROOT_PASSWORD=test \
|
||||
-p 3306:3306 \
|
||||
-d \
|
||||
--tmpfs /var/lib/mysql:rw,noexec,nosuid,size=1024M \
|
||||
mysql:8.0 \
|
||||
--skip-log-bin \
|
||||
--collation-server=utf8mb4_bin
|
||||
|
||||
- name: Configuring git vars
|
||||
uses: rlespinasse/github-slug-action@v4
|
||||
|
||||
@@ -18,7 +18,7 @@ import inquirer from 'inquirer';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { createNote } from './note.mjs';
|
||||
import { getInitialValues } from './helpers.mjs';
|
||||
import { getInitialValues, getIssueNumberFromBranchName } from './helpers.mjs';
|
||||
import * as Prompts from './prompts.mjs';
|
||||
import logger from './logger.mjs';
|
||||
|
||||
@@ -29,7 +29,7 @@ export default async (options) => {
|
||||
// Fetch information.
|
||||
const messages = [];
|
||||
const { issueNumber } = await inquirer.prompt([
|
||||
Prompts.getIssuePrompt(),
|
||||
Prompts.getIssuePrompt(getIssueNumberFromBranchName()),
|
||||
], initialValues);
|
||||
|
||||
let selection = {};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import chalk from 'chalk';
|
||||
import branchName from 'current-git-branch';
|
||||
import { isStandardComponent, isCommunityComponent, rewritePlugintypeAsSubsystem } from './components.mjs';
|
||||
import { isValidNoteName } from './noteTypes.mjs';
|
||||
import logger from './logger.mjs';
|
||||
@@ -59,6 +60,19 @@ export const formatIssueNumber = (input) => {
|
||||
return input;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the issue number, based off current git branch name
|
||||
*
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export const getIssueNumberFromBranchName = () => {
|
||||
const issueNumber = String(branchName()).match(/^[a-zA-Z]*-\d+/);
|
||||
if (issueNumber) {
|
||||
return formatIssueNumber(issueNumber[0]);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate a component.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
issueNumber: MDL-75067
|
||||
notes:
|
||||
tool_behat:
|
||||
- message: >-
|
||||
The `behat_session_trait::ensure_element_[does_not_]exists(...)` methods
|
||||
now accept optional `$container` parameter to define the parent node to
|
||||
look within
|
||||
type: improved
|
||||
@@ -0,0 +1,7 @@
|
||||
issueNumber: MDL-87100
|
||||
notes:
|
||||
core:
|
||||
- message: >-
|
||||
When creating upgrade notes, the issue number will be inferred from the
|
||||
current Git branch name by default
|
||||
type: improved
|
||||
@@ -0,0 +1,7 @@
|
||||
issueNumber: MDL-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
|
||||
@@ -0,0 +1,8 @@
|
||||
issueNumber: MDL-87993
|
||||
notes:
|
||||
core:
|
||||
- message: >-
|
||||
The `core/toast` JS module now accepts a `visuallyHidden` configuration
|
||||
parameter to render visually hidden toast messages for screen reader
|
||||
users.
|
||||
type: improved
|
||||
@@ -32,7 +32,7 @@ Moodle also offers hosting through both [MoodleCloud][11], and our [partner netw
|
||||
|
||||
## License
|
||||
|
||||
Moodle is provided freely as open source software, under version 3 of the GNU General Public License. For more information on our license see
|
||||
Moodle is provided freely as open source software, under version 3 of the GNU General Public License. See our [license page][12] for more information.
|
||||
|
||||
[1]: https://moodle.org
|
||||
[2]: https://moodle.com
|
||||
|
||||
+112
@@ -6,6 +6,118 @@ 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.10+
|
||||
|
||||
### core
|
||||
|
||||
#### Added
|
||||
|
||||
- When creating upgrade notes, the issue number will be inferred from the current Git branch name by default
|
||||
|
||||
For more information see [MDL-87100](https://tracker.moodle.org/browse/MDL-87100)
|
||||
- There is a new Behat `toast_message` named selector to more easily assert the presence of Toast messages on the page
|
||||
|
||||
For more information see [MDL-87443](https://tracker.moodle.org/browse/MDL-87443)
|
||||
- The `core/toast` JS module now accepts a `visuallyHidden` configuration parameter to render visually hidden toast messages for screen reader users.
|
||||
|
||||
For more information see [MDL-87993](https://tracker.moodle.org/browse/MDL-87993)
|
||||
|
||||
### tool_behat
|
||||
|
||||
#### Added
|
||||
|
||||
- The `behat_session_trait::ensure_element_[does_not_]exists(...)` methods now accept optional `$container` parameter to define the parent node to look within
|
||||
|
||||
For more information see [MDL-75067](https://tracker.moodle.org/browse/MDL-75067)
|
||||
|
||||
## 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
|
||||
);
|
||||
|
||||
+54
-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,8 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
$output .= $this->mobile_configuration_warning($mobileconfigured);
|
||||
$output .= $this->forgotten_password_url_warning($invalidforgottenpasswordurl);
|
||||
$output .= $this->mnet_deprecation_warning($xmlrpcwarning);
|
||||
$output .= $this->moodlenet_removal_warning();
|
||||
$output .= $this->marketplace_integration_notice();
|
||||
$output .= $this->userfeedback_encouragement($showfeedbackencouragement);
|
||||
$output .= $this->services_and_support_content($showservicesandsupport);
|
||||
$output .= $this->campaign_content($showcampaigncontent);
|
||||
@@ -2218,7 +2237,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 +2286,33 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
return $this->warning($xmlrpcwarning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a warning about the removal of MoodleNet integration.
|
||||
*
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
protected function moodlenet_removal_warning(): string {
|
||||
$moodlenetenabled = get_config('tool_moodlenet', 'enablemoodlenet');
|
||||
if (!empty($moodlenetenabled)) {
|
||||
$moodlenetwarning = get_string('moodlenetremovalwarning', 'admin');
|
||||
return $this->warning($moodlenetwarning);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a notice about Moodle Marketplace integration.
|
||||
*
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
protected function marketplace_integration_notice(): string {
|
||||
$installer = tool_installaddon_installer::instance();
|
||||
$url = $installer->get_marketplace_url();
|
||||
$notice = get_string('marketplaceavailablenotice', 'admin', $url->out());
|
||||
return $this->warning($notice, 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the theme selector list.
|
||||
*
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# tool_behat Upgrade notes
|
||||
|
||||
## 4.5.10+
|
||||
|
||||
### Added
|
||||
|
||||
- The `behat_session_trait::ensure_element_[does_not_]exists(...)` methods now accept optional `$container` parameter to define the parent node to look within
|
||||
|
||||
For more information see [MDL-75067](https://tracker.moodle.org/browse/MDL-75067)
|
||||
|
||||
## 4.5.2
|
||||
|
||||
### Added
|
||||
|
||||
@@ -224,6 +224,11 @@ if (empty($parallelrun)) {
|
||||
$cmds['singlerun'] = $runtestscommand;
|
||||
|
||||
echo "Running single behat site:" . PHP_EOL;
|
||||
// The inner PHP process is not marked as having an interactive terminal even if it's passed
|
||||
// through from this one which does, so we need to pass it through as an environment variable.
|
||||
if (function_exists('posix_isatty') && posix_isatty(STDOUT)) {
|
||||
putenv('MOODLE_BEHAT_RUNNING_IN_TTY=1');
|
||||
}
|
||||
passthru("php $runtestscommand", $status);
|
||||
$exitcodes['singlerun'] = $status;
|
||||
chdir($cwd);
|
||||
@@ -525,4 +530,3 @@ function get_status_lines_from_run_op(Symfony\Component\Process\Process $process
|
||||
|
||||
return $statusstr;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 |
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
define("tool_installaddon/footer",["exports","jquery","core_course/local/activitychooser/selectors"],(function(_exports,_jquery,_selectors){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
|
||||
/**
|
||||
* Activity chooser footer handlers for tool_installaddon.
|
||||
*
|
||||
* @module tool_installaddon/footer
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.footerClickListener=void 0,_jquery=_interopRequireDefault(_jquery),_selectors=_interopRequireDefault(_selectors);_exports.footerClickListener=(e,footerData,modal)=>{const closeOption=e.target.closest(_selectors.default.actions.closeOption);if(!closeOption)return;const moduleName=closeOption.dataset.modname;if(!moduleName)return;const carousel=(0,_jquery.default)(modal.getBody()[0].querySelector(_selectors.default.regions.carousel));carousel.carousel("prev"),modal.setFooter(footerData.customfootertemplate),carousel.one("slid.bs.carousel",(()=>{const caller=modal.getBody()[0].querySelector(_selectors.default.regions.modules).querySelector(_selectors.default.regions.getModuleSelector(moduleName));caller&&caller.focus()}))}}));
|
||||
|
||||
//# sourceMappingURL=footer.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"footer.min.js","sources":["../src/footer.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Activity chooser footer handlers for tool_installaddon.\n *\n * @module tool_installaddon/footer\n * @copyright 2026 Safat Shahin <safat.shahin@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport selectors from 'core_course/local/activitychooser/selectors';\n\n/**\n * Handle clicks in the chooser footer when installaddon is the active footer plugin.\n *\n * @param {Event} e The event being triggered\n * @param {Object} footerData The footer data generated for the chooser\n * @param {Object} modal The chooser modal\n */\nexport const footerClickListener = (e, footerData, modal) => {\n const closeOption = e.target.closest(selectors.actions.closeOption);\n\n if (!closeOption) {\n return;\n }\n\n const moduleName = closeOption.dataset.modname;\n\n if (!moduleName) {\n return;\n }\n\n const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));\n\n // Trigger the transition between 'pages'.\n carousel.carousel('prev');\n modal.setFooter(footerData.customfootertemplate);\n carousel.one('slid.bs.carousel', () => {\n const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);\n const caller = allModules.querySelector(selectors.regions.getModuleSelector(moduleName));\n if (caller) {\n caller.focus();\n }\n });\n};\n"],"names":["e","footerData","modal","closeOption","target","closest","selectors","actions","moduleName","dataset","modname","carousel","getBody","querySelector","regions","setFooter","customfootertemplate","one","caller","modules","getModuleSelector","focus"],"mappings":";;;;;;;oNAiCmC,CAACA,EAAGC,WAAYC,eACzCC,YAAcH,EAAEI,OAAOC,QAAQC,mBAAUC,QAAQJ,iBAElDA,yBAICK,WAAaL,YAAYM,QAAQC,YAElCF,wBAICG,UAAW,mBAAET,MAAMU,UAAU,GAAGC,cAAcP,mBAAUQ,QAAQH,WAGtEA,SAASA,SAAS,QAClBT,MAAMa,UAAUd,WAAWe,sBAC3BL,SAASM,IAAI,oBAAoB,WAEvBC,OADahB,MAAMU,UAAU,GAAGC,cAAcP,mBAAUQ,QAAQK,SAC5CN,cAAcP,mBAAUQ,QAAQM,kBAAkBZ,aACxEU,QACAA,OAAOG"}
|
||||
@@ -0,0 +1,59 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Activity chooser footer handlers for tool_installaddon.
|
||||
*
|
||||
* @module tool_installaddon/footer
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import $ from 'jquery';
|
||||
import selectors from 'core_course/local/activitychooser/selectors';
|
||||
|
||||
/**
|
||||
* Handle clicks in the chooser footer when installaddon is the active footer plugin.
|
||||
*
|
||||
* @param {Event} e The event being triggered
|
||||
* @param {Object} footerData The footer data generated for the chooser
|
||||
* @param {Object} modal The chooser modal
|
||||
*/
|
||||
export const footerClickListener = (e, footerData, modal) => {
|
||||
const closeOption = e.target.closest(selectors.actions.closeOption);
|
||||
|
||||
if (!closeOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleName = closeOption.dataset.modname;
|
||||
|
||||
if (!moduleName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
|
||||
|
||||
// Trigger the transition between 'pages'.
|
||||
carousel.carousel('prev');
|
||||
modal.setFooter(footerData.customfootertemplate);
|
||||
carousel.one('slid.bs.carousel', () => {
|
||||
const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);
|
||||
const caller = allModules.querySelector(selectors.regions.getModuleSelector(moduleName));
|
||||
if (caller) {
|
||||
caller.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -70,6 +70,25 @@ class tool_installaddon_installer {
|
||||
$url = 'https://moodle.org/plugins/get.php';
|
||||
}
|
||||
|
||||
return $this->get_external_service_url($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URL to Moodle Marketplace.
|
||||
*
|
||||
* @return moodle_url
|
||||
*/
|
||||
public function get_marketplace_url(): moodle_url {
|
||||
return $this->get_external_service_url('https://marketplace.moodle.com/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URL to external service with optional encoded site information.
|
||||
*
|
||||
* @param string $url The external service base URL.
|
||||
* @return moodle_url
|
||||
*/
|
||||
protected function get_external_service_url(string $url): moodle_url {
|
||||
if (!$this->should_send_site_info()) {
|
||||
return new moodle_url($url);
|
||||
}
|
||||
|
||||
+9
-6
@@ -16,20 +16,23 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tool_moodlenet\task;
|
||||
namespace tool_installaddon\task;
|
||||
|
||||
/**
|
||||
* Ad-hoc task to perform post install tasks.
|
||||
* We use this to set the active activity chooser footer plugin to tool_moodlenet.
|
||||
* We use this to set the active activity chooser footer plugin to tool_installaddon.
|
||||
* We couldn't do this directly in install.php, because there is an admin_apply_default_settings() call after all plugins are
|
||||
* installed and that would reset whatever value we had set earlier to 'hidden'.
|
||||
*
|
||||
* @package tool_moodlenet
|
||||
* @copyright 2022 Shamim Rezaie <shamim@moodle.com>
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class post_install extends \core\task\adhoc_task {
|
||||
public function execute() {
|
||||
set_config('activitychooseractivefooter', 'tool_moodlenet');
|
||||
/**
|
||||
* Sets the active activity chooser footer plugin after install defaults are applied.
|
||||
*/
|
||||
public function execute(): void {
|
||||
set_config('activitychooseractivefooter', 'tool_installaddon');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Install script for tool_installaddon.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Perform the post-install procedures.
|
||||
*/
|
||||
function xmldb_tool_installaddon_install(): void {
|
||||
// Use an ad-hoc task to set the active activity chooser footer plugin to tool_installaddon.
|
||||
// We couldn't do this in admin/settings/courses.php for 2 reasons:
|
||||
// - First, because it would be a breach of component communications principles to do so there.
|
||||
// - Second, because we can't call get_plugins_with_function() during install and upgrade (or it will return []).
|
||||
// We couldn't do this directly here either, because there is an admin_apply_default_settings() call after all plugins are
|
||||
// installed and that would reset whatever value we set here to 'hidden'.
|
||||
$postinstall = new tool_installaddon\task\post_install();
|
||||
core\task\manager::queue_adhoc_task($postinstall);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Upgrade script for tool_installaddon.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Upgrade the plugin.
|
||||
*
|
||||
* @param int $oldversion
|
||||
* @return bool always true
|
||||
*/
|
||||
function xmldb_tool_installaddon_upgrade(int $oldversion): bool {
|
||||
if ($oldversion < 2024100701) {
|
||||
// Set the activity chooser active footer to include marketplace regardless of the previous setting.
|
||||
// We are deliberately setting this to increase awareness of marketplace.
|
||||
set_config('activitychooseractivefooter', 'tool_installaddon');
|
||||
|
||||
upgrade_plugin_savepoint(true, 2024100701, 'tool', 'installaddon');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['acknowledgement'] = 'Acknowledgement';
|
||||
$string['acknowledgementtext'] = 'I understand that it is my responsibility to have full backups of this site prior to installing additional plugins. I accept and understand that plugins (especially but not only those originating in unofficial sources) may contain security holes, can make the site unavailable, or cause private data leaks or loss.';
|
||||
$string['activitychooserfootertext'] = 'Browse more activities on';
|
||||
$string['featuredisabled'] = 'The plugin installer is disabled on this site.';
|
||||
$string['installaddon'] = 'Install plugin!';
|
||||
$string['installaddons'] = 'Install plugins';
|
||||
@@ -44,6 +45,8 @@ $string['installfromzipsubmit'] = 'Install plugin from the ZIP file';
|
||||
$string['installfromziptype'] = 'Plugin type';
|
||||
$string['installfromziptype_help'] = 'For plugins that correctly declare their component name, the installer is able to detect the plugin type automatically. If the auto-detection fails, choose the correct type of plugin manually. Warning: The installation procedure can fail badly if an incorrect plugin type is specified.';
|
||||
$string['installfromziptype_link'] = 'Development:Plugins';
|
||||
$string['marketplaceadminlinktext'] = 'Browse new plugins';
|
||||
$string['marketplacelink'] = 'Moodle Marketplace';
|
||||
$string['permcheck'] = 'Make sure the plugin type root location is writable by the web server process.';
|
||||
$string['permcheckerror'] = 'Error while checking for write permission';
|
||||
$string['permcheckprogress'] = 'Checking for write permission ...';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Public API for tool_installaddon.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use core_course\local\entity\activity_chooser_footer;
|
||||
|
||||
/**
|
||||
* Build activity chooser footer content for Marketplace.
|
||||
*
|
||||
* @param int $courseid The selected course id.
|
||||
* @param int $sectionid The selected section id.
|
||||
* @return activity_chooser_footer
|
||||
*/
|
||||
function tool_installaddon_custom_chooser_footer(int $courseid, int $sectionid): activity_chooser_footer {
|
||||
global $OUTPUT;
|
||||
|
||||
$installer = tool_installaddon_installer::instance();
|
||||
$marketplaceurl = $installer->get_marketplace_url();
|
||||
|
||||
$renderedfooter = $OUTPUT->render_from_template('tool_installaddon/chooser_footer', [
|
||||
'url' => $marketplaceurl->out(false),
|
||||
]);
|
||||
|
||||
return new activity_chooser_footer(
|
||||
'tool_installaddon/footer',
|
||||
$renderedfooter
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,12 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
if ($hassiteconfig and empty($CFG->disableupdateautodeploy)) {
|
||||
$installer = tool_installaddon_installer::instance();
|
||||
$ADMIN->add('modules', new admin_externalpage(
|
||||
'tool_installaddon_marketplace',
|
||||
get_string('marketplaceadminlinktext', 'tool_installaddon'),
|
||||
$installer->get_marketplace_url()->out(false)
|
||||
), 'modsettings');
|
||||
|
||||
$ADMIN->add('modules', new admin_externalpage('tool_installaddon_index',
|
||||
get_string('installaddons', 'tool_installaddon'),
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template tool_installaddon/chooser_footer
|
||||
|
||||
Activity chooser footer link to Marketplace.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"url": "https://marketplace.moodle.com/?site=hash"
|
||||
}
|
||||
}}
|
||||
<div class="w-100 d-flex px-2">
|
||||
<span class="my-auto me-1">{{#str}} activitychooserfootertext, tool_installaddon {{/str}}</span>
|
||||
{{> tool_installaddon/marketplace_link }}
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template tool_installaddon/marketplace_link
|
||||
|
||||
Reusable link to Moodle Marketplace.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"url": "https://marketplace.moodle.com/?site=hash"
|
||||
}
|
||||
}}
|
||||
<a class="d-inline my-auto" href="{{url}}" target="_blank" rel="noopener noreferrer">
|
||||
{{#str}} marketplacelink, tool_installaddon {{/str}}
|
||||
<i
|
||||
class="fa fa-external-link"
|
||||
role="img"
|
||||
aria-label="{{#str}} opensinnewwindow, core {{/str}}"
|
||||
title="{{#str}} opensinnewwindow, core {{/str}}"
|
||||
>
|
||||
</i>
|
||||
</a>
|
||||
@@ -0,0 +1,37 @@
|
||||
@tool @tool_installaddon @javascript
|
||||
Feature: Marketplace activity chooser footer
|
||||
In order to browse plugins from the activity chooser
|
||||
As a teacher
|
||||
I need to see the Marketplace footer link when installaddon is the active footer plugin
|
||||
|
||||
Background:
|
||||
Given the following config values are set as admin:
|
||||
| activitychooseractivefooter | tool_installaddon |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category | format |
|
||||
| Course 1 | C1 | 0 | topics |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
|
||||
Scenario: Activity chooser footer includes Marketplace link
|
||||
Given I log in as "teacher1"
|
||||
When I am on "Course 1" course homepage with editing mode on
|
||||
And I open the activity chooser
|
||||
Then "Browse more activities on" "text" should exist in the "Add an activity or resource" "dialogue"
|
||||
And "Marketplace" "link" should exist in the "Add an activity or resource" "dialogue"
|
||||
|
||||
Scenario: Back button works from activity help when the Marketplace footer is active
|
||||
Given I log in as "teacher1"
|
||||
When I am on "Course 1" course homepage with editing mode on
|
||||
And I open the activity chooser
|
||||
And I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
|
||||
Then "help" "core_course > Activity chooser screen" should be visible
|
||||
When I click on "Back" "button" in the "Add an activity or resource" "dialogue"
|
||||
Then "modules" "core_course > Activity chooser screen" should be visible
|
||||
And "help" "core_course > Activity chooser screen" should not be visible
|
||||
And "Marketplace" "link" should exist in the "Add an activity or resource" "dialogue"
|
||||
And "Back" "button" should not exist in the "Add an activity or resource" "dialogue"
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Provides a mock testable_tool_installaddon_installer_without_site_info class.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @subpackage fixtures
|
||||
* @category test
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once(__DIR__ . '/testable_installer.php');
|
||||
|
||||
/**
|
||||
* Testable subclass with site-info sharing disabled.
|
||||
*
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class testable_tool_installaddon_installer_without_site_info extends testable_tool_installaddon_installer {
|
||||
/**
|
||||
* Disable site info sharing.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function should_send_site_info() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,14 @@
|
||||
namespace tool_installaddon;
|
||||
|
||||
use testable_tool_installaddon_installer;
|
||||
use testable_tool_installaddon_installer_without_site_info;
|
||||
use tool_installaddon_installer;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once(__DIR__.'/fixtures/testable_installer.php');
|
||||
require_once(__DIR__ . '/fixtures/testable_installer.php');
|
||||
require_once(__DIR__ . '/fixtures/testable_installer_without_site_info.php');
|
||||
|
||||
/**
|
||||
* Unit tests for the {@link tool_installaddon_installer} class
|
||||
@@ -32,16 +34,14 @@ require_once(__DIR__.'/fixtures/testable_installer.php');
|
||||
* @category test
|
||||
* @copyright 2013 David Mudrak <david@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \tool_installaddon_installer
|
||||
*/
|
||||
final class installer_test extends \advanced_testcase {
|
||||
|
||||
public function test_get_addons_repository_url(): void {
|
||||
$installer = testable_tool_installaddon_installer::instance();
|
||||
$url = $installer->get_addons_repository_url();
|
||||
$query = parse_url($url, PHP_URL_QUERY);
|
||||
$this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches));
|
||||
$site = rawurldecode($matches[1]);
|
||||
$site = json_decode(base64_decode($site), true);
|
||||
$site = $this->decode_site_info_from_url($url);
|
||||
$this->assertIsArray($site);
|
||||
$this->assertEquals(3, count($site));
|
||||
$this->assertSame('Nasty site', $site['fullname']);
|
||||
@@ -49,6 +49,34 @@ final class installer_test extends \advanced_testcase {
|
||||
$this->assertSame("2.5'; DROP TABLE mdl_user; --", $site['majorversion']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that marketplace URL includes expected host, scheme and site payload.
|
||||
*/
|
||||
public function test_get_marketplace_url(): void {
|
||||
$installer = testable_tool_installaddon_installer::instance();
|
||||
$addonsurl = $installer->get_addons_repository_url();
|
||||
$marketplaceurl = $installer->get_marketplace_url();
|
||||
|
||||
$this->assertSame('marketplace.moodle.com', parse_url($marketplaceurl, PHP_URL_HOST));
|
||||
$this->assertSame('https', parse_url($marketplaceurl, PHP_URL_SCHEME));
|
||||
$this->assertSame(
|
||||
$this->decode_site_info_from_url($addonsurl),
|
||||
$this->decode_site_info_from_url($marketplaceurl),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that marketplace URL has no query string when site info is unavailable.
|
||||
*/
|
||||
public function test_get_marketplace_url_without_site_info(): void {
|
||||
$installer = testable_tool_installaddon_installer_without_site_info::instance();
|
||||
$marketplaceurl = $installer->get_marketplace_url();
|
||||
|
||||
$this->assertSame('marketplace.moodle.com', parse_url($marketplaceurl, PHP_URL_HOST));
|
||||
$this->assertSame('https', parse_url($marketplaceurl, PHP_URL_SCHEME));
|
||||
$this->assertEmpty(parse_url($marketplaceurl, PHP_URL_QUERY));
|
||||
}
|
||||
|
||||
public function test_decode_remote_request(): void {
|
||||
$installer = testable_tool_installaddon_installer::instance();
|
||||
|
||||
@@ -132,7 +160,7 @@ $plugin->version = 2014121300;
|
||||
$versionphp = file_get_contents($fixtures.'/github/moodle-repository_mahara-master/version.php');
|
||||
$this->assertEquals('repository_mahara', $installer->testable_detect_plugin_component_from_versionphp($versionphp));
|
||||
|
||||
$versionphp = file_get_contents($fixtures.'/nocomponent/baz/version.php');
|
||||
$versionphp = file_get_contents($fixtures . '/nocomponent/baz/version.php');
|
||||
$this->assertFalse($installer->testable_detect_plugin_component_from_versionphp($versionphp));
|
||||
}
|
||||
|
||||
@@ -143,15 +171,28 @@ $plugin->version = 2014121300;
|
||||
$storage1 = $installer->make_installfromzip_storage();
|
||||
$this->assertTrue(is_dir($storage1));
|
||||
$this->assertTrue(is_writable($storage1));
|
||||
file_put_contents($storage1.'/hello.txt', 'Find me if you can!');
|
||||
file_put_contents($storage1 . '/hello.txt', 'Find me if you can!');
|
||||
|
||||
// Check we get unique directory on each call.
|
||||
$storage2 = $installer->make_installfromzip_storage();
|
||||
$this->assertTrue(is_dir($storage2));
|
||||
$this->assertTrue(is_writable($storage2));
|
||||
$this->assertFalse(file_exists($storage2.'/hello.txt'));
|
||||
$this->assertFalse(file_exists($storage2 . '/hello.txt'));
|
||||
|
||||
// Check both are in the same parent directory.
|
||||
$this->assertEquals(dirname($storage1), dirname($storage2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the encoded 'site' URL query parameter.
|
||||
*
|
||||
* @param moodle_url $url
|
||||
* @return array
|
||||
*/
|
||||
private function decode_site_info_from_url(\moodle_url $url): array {
|
||||
$query = parse_url($url, PHP_URL_QUERY);
|
||||
$this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches));
|
||||
$site = rawurldecode($matches[1]);
|
||||
return json_decode(base64_decode($site), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for tool_installaddon lib.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_installaddon;
|
||||
|
||||
use core_course\local\entity\activity_chooser_footer;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/admin/tool/installaddon/lib.php');
|
||||
|
||||
/**
|
||||
* Test installaddon lib functions.
|
||||
*/
|
||||
final class lib_test extends \advanced_testcase {
|
||||
/**
|
||||
* Tests chooser footer generation for marketplace link content.
|
||||
*
|
||||
* @covers ::tool_installaddon_custom_chooser_footer
|
||||
*/
|
||||
public function test_tool_installaddon_custom_chooser_footer(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$footer = \tool_installaddon_custom_chooser_footer(1, 1);
|
||||
|
||||
$this->assertInstanceOf(activity_chooser_footer::class, $footer);
|
||||
$this->assertSame('tool_installaddon/footer', $footer->get_footer_js_file());
|
||||
$this->assertStringContainsString('https://marketplace.moodle.com/', $footer->get_footer_template());
|
||||
$this->assertStringContainsString('site=', $footer->get_footer_template());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tool_installaddon\task;
|
||||
|
||||
/**
|
||||
* Unit tests for the post install task.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @category test
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \tool_installaddon\task\post_install
|
||||
*/
|
||||
final class post_install_test extends \advanced_testcase {
|
||||
/**
|
||||
* Tests that the task sets the activity chooser footer plugin to tool_installaddon.
|
||||
*/
|
||||
public function test_execute_sets_activitychooseractivefooter_config(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
set_config('activitychooseractivefooter', 'hidden');
|
||||
|
||||
$task = new post_install();
|
||||
$task->execute();
|
||||
|
||||
$this->assertSame(
|
||||
'tool_installaddon',
|
||||
get_config('core', 'activitychooseractivefooter'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,6 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'tool_installaddon';
|
||||
$plugin->version = 2024100700;
|
||||
$plugin->version = 2024100701;
|
||||
$plugin->requires = 2024100100;
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -28,12 +28,5 @@ declare(strict_types=1);
|
||||
* Perform the post-install procedures.
|
||||
*/
|
||||
function xmldb_tool_moodlenet_install() {
|
||||
// Use an ad-hoc task to set the active activity chooser footer plugin to tool_moodlenet.
|
||||
// We couldn't do this in admin/settings/courses.php for 2 reasons:
|
||||
// - First, because it would be a breach of component communications principles to do so there.
|
||||
// - Second, because we can't call get_plugins_with_function() during install and upgrade (or it will return []).
|
||||
// We couldn't do this directly here either, because there is an admin_apply_default_settings() call after all plugins are
|
||||
// installed and that would reset whatever value we set here to 'hidden'.
|
||||
$postinstall = new tool_moodlenet\task\post_install();
|
||||
core\task\manager::queue_adhoc_task($postinstall);
|
||||
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ $string['defaultmoodlenet_desc'] = 'The URL of the MoodleNet instance available
|
||||
$string['defaultmoodlenetname'] = "MoodleNet instance name";
|
||||
$string['defaultmoodlenetnamevalue'] = 'MoodleNet Central';
|
||||
$string['defaultmoodlenetname_desc'] = 'The name of the MoodleNet instance available via the activity chooser.';
|
||||
$string['removalwarning_feature'] = 'If you need to continue using MoodleNet, contact your site administrator about setting up a self-hosted MoodleNet instance.';
|
||||
$string['removalwarning_service'] = 'After the date, you will no longer be able to browse or add content from MoodleNet Central.';
|
||||
$string['removalwarning_title'] = 'The MoodleNet service will be shut down on 20 April 2026.';
|
||||
$string['enablemoodlenet'] = 'Enable MoodleNet integration (inbound)';
|
||||
$string['enablemoodlenet_desc'] = 'If enabled, a user with the capability to create and manage activities can browse MoodleNet via the activity chooser and import MoodleNet resources into their course. In addition, a user with the capability to restore backups can select a backup file on MoodleNet and restore it into Moodle.';
|
||||
$string['errorduringdownload'] = 'An error occurred while downloading the file: {$a}';
|
||||
|
||||
@@ -27,8 +27,16 @@
|
||||
<div class="content text-start mb-5 px-5 py-4" data-region="chooser-option-summary-content-container">
|
||||
<div data-region="moodle-net">
|
||||
<div class="overlay-icon-container z-index-1 d-none" data-region="spinner"></div>
|
||||
<img class="w-25 mb-4" aria-hidden="true" src="{{{img}}}">
|
||||
<img class="w-25 mb-4" alt="" aria-hidden="true" src="{{{img}}}">
|
||||
<p>{{#str}} instancedescription, tool_moodlenet {{/str}}</p>
|
||||
|
||||
{{! Removal warning - always visible when MoodleNet integration is enabled }}
|
||||
<div class="alert alert-warning w-75 mx-auto mb-3" role="alert">
|
||||
<strong>{{#str}}removalwarning_title, tool_moodlenet{{/str}}</strong>
|
||||
<p class="mb-1">{{#str}}removalwarning_service, tool_moodlenet{{/str}}</p>
|
||||
<p class="mb-0">{{#str}}removalwarning_feature, tool_moodlenet{{/str}}</p>
|
||||
</div>
|
||||
|
||||
<p class="w-75 mx-auto mb-1 mt-5">{{#str}} connectandbrowse, tool_moodlenet {{/str}}</p>
|
||||
<a class="btn btn-secondary d-block w-75 mx-auto mb-4"
|
||||
data-action="browse"
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -35,10 +35,12 @@ Feature: Test if the login form provides the correct feedback
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Appearance > Logos" in site administration
|
||||
And I upload "course/tests/fixtures/image.jpg" file to "Logo" filemanager
|
||||
And I upload "course/tests/fixtures/image.jpg" file to "Favicon" filemanager
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
And I follow "Log in"
|
||||
Then "//img[@id='logoimage']" "xpath_element" should exist
|
||||
Then "//img[@id='logoimage' and contains(@src, '/image.jpg')]" "xpath_element" should exist
|
||||
And "//link[@rel='shortcut icon' and contains(@href, '/image.jpg')]" "xpath_element" should exist
|
||||
|
||||
Scenario: Add a custom welcome message
|
||||
Given the following config values are set as admin:
|
||||
@@ -96,13 +98,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));
|
||||
}
|
||||
}
|
||||
@@ -107,17 +107,12 @@ final class async_backup_test extends \advanced_testcase {
|
||||
$asynctask = new \core\task\asynchronous_backup_task();
|
||||
$asynctask->set_custom_data(['backupid' => $backupid]);
|
||||
$asynctask->set_userid($USER->id);
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// We are expecting trace output during this test.
|
||||
$this->expectOutputRegex("/$backupid/");
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$this->assertInstanceOf('\\core\\task\\asynchronous_backup_task', $task);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
$asynctask->execute();
|
||||
|
||||
$postbackuprec = $DB->get_record('backup_controllers', ['backupid' => $backupid]);
|
||||
|
||||
@@ -151,13 +146,9 @@ final class async_backup_test extends \advanced_testcase {
|
||||
// Create the adhoc task.
|
||||
$asynctask = new \core\task\asynchronous_backup_task();
|
||||
$asynctask->set_custom_data(['backupid' => $backupid]);
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
$asynctask->execute();
|
||||
|
||||
$postbackuprec = $DB->get_record('backup_controllers', ['backupid' => $backupid]);
|
||||
|
||||
@@ -241,19 +232,11 @@ final class async_backup_test extends \advanced_testcase {
|
||||
// Now queue an adhoc task and check it handles and completes gracefully.
|
||||
$asynctask = new \core\task\asynchronous_backup_task();
|
||||
$asynctask->set_custom_data(array('backupid' => $backupid));
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// We are expecting a specific message output during this test.
|
||||
$this->expectOutputRegex('/invalid controller/');
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$this->assertInstanceOf('\\core\\task\\asynchronous_backup_task', $task);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
|
||||
// Check the task record is removed.
|
||||
$this->assertEquals(0, $DB->count_records('task_adhoc'));
|
||||
$asynctask->execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,20 +223,12 @@ final class async_restore_test extends \advanced_testcase {
|
||||
// Create the adhoc task.
|
||||
$asynctask = new \core\task\asynchronous_restore_task();
|
||||
$asynctask->set_custom_data(['backupid' => $restoreid]);
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// We are expecting a specific message output during this test.
|
||||
$this->expectOutputRegex('/invalid controller/');
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
|
||||
// Check the task record is removed.
|
||||
$this->assertEquals(0, $DB->count_records('task_adhoc'));
|
||||
$asynctask->execute();
|
||||
|
||||
// Now delete the record and confirm an entirely missing controller is handled.
|
||||
$DB->delete_records('backup_controllers');
|
||||
@@ -244,19 +236,11 @@ final class async_restore_test extends \advanced_testcase {
|
||||
// Create the adhoc task.
|
||||
$asynctask = new \core\task\asynchronous_restore_task();
|
||||
$asynctask->set_custom_data(['backupid' => $restoreid]);
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// We are expecting a specific message output during this test.
|
||||
$this->expectOutputRegex('/Unable to find restore controller/');
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
|
||||
// Check the task record is removed.
|
||||
$this->assertEquals(0, $DB->count_records('task_adhoc'));
|
||||
$asynctask->execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user