Compare commits
608 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 952a894a97 | |||
| 058e70e9a2 | |||
| 4144ba4ee6 | |||
| 9a37320726 | |||
| 647b675ee6 | |||
| eec91baff9 | |||
| 775b9a363e | |||
| 87f67eccef | |||
| 0442cf991c | |||
| f216c901e1 | |||
| cc3e741801 | |||
| 8d44fd1950 | |||
| 84388a2e30 | |||
| 23a8270d36 | |||
| a4d4a0b280 | |||
| 07ade46473 | |||
| 19fa09219f | |||
| cb51c8aeea | |||
| 9c882617dd | |||
| a9140d703a | |||
| 118a1e784e | |||
| 30983313d5 | |||
| e92d72531d | |||
| 31ea6dee4b | |||
| 83e9f358ec | |||
| ea5dfe6569 | |||
| 1dd4007796 | |||
| 6032981688 | |||
| f39dc20d43 | |||
| 462e37413f | |||
| 3fe3913c54 | |||
| be58ae7572 | |||
| 67753d625e | |||
| bb05910a30 | |||
| 7ff28ff941 | |||
| da736374e9 | |||
| c2724bce31 | |||
| 4762e10fb0 | |||
| 334bee3de7 | |||
| c11fbc518c | |||
| 42197e952d | |||
| 65adf66a24 | |||
| bb3d73727f | |||
| 3ab30c7f20 | |||
| 56c40e0b40 | |||
| 980ae98809 | |||
| 2d6f00d381 | |||
| ebdf85d638 | |||
| 048d7eec36 | |||
| fa1eab6447 | |||
| 657f0381d8 | |||
| 617df25335 | |||
| 5611585eec | |||
| 5df5711bd0 | |||
| 7a82a0e426 | |||
| 868f9fa5bd | |||
| 894d847d9d | |||
| 304fa6defe | |||
| 22665452a3 | |||
| 7ca59eba57 | |||
| ad06a6f7df | |||
| f2fcb75384 | |||
| 3d1f43126e | |||
| ddd2f21247 | |||
| bf2f4704c9 | |||
| bff7ea0435 | |||
| 267c26d6d7 | |||
| 591c98d71c | |||
| c9a94cbc9a | |||
| 392741bd32 | |||
| 97f2f72902 | |||
| 065c5a1c2f | |||
| f6e0e09f20 | |||
| 3c3e5a5f71 | |||
| 82258882da | |||
| 3d9e7a3071 | |||
| db233ce7e2 | |||
| 6e7fdac8f0 | |||
| 0393dc3357 | |||
| 727ba5d9c0 | |||
| 045d650ae3 | |||
| 890e60c709 | |||
| 7738116fd5 | |||
| 183b8f556b | |||
| c696b5e6b4 | |||
| 22c276dd00 | |||
| ae1f2982ff | |||
| 4e2dd1a3a8 | |||
| bce5596ff8 | |||
| ce6735fb29 | |||
| f77412b119 | |||
| ed69fa429f | |||
| 99f889d2af | |||
| ef8de2333b | |||
| 7a2a464728 | |||
| a3e94bc0e8 | |||
| cf687f4357 | |||
| e32ae38439 | |||
| 4e09dc64b7 | |||
| fcefa38092 | |||
| 61f218ae47 | |||
| 6bafc320c0 | |||
| a22a46a689 | |||
| 9e6f90f1cf | |||
| 62ed52fdc4 | |||
| ad45df4dc2 | |||
| 1d8729004a | |||
| c65e8a5900 | |||
| 796a7ea458 | |||
| 3c5d8d6602 | |||
| 2ad366c869 | |||
| 55ba3eece2 | |||
| fb990d26d4 | |||
| 96dbdc2ad9 | |||
| d976ae38f5 | |||
| 675c0451f9 | |||
| f5e8e0b08a | |||
| c9da87aa50 | |||
| c070acd2eb | |||
| 647aa0625f | |||
| a4e65c3276 | |||
| 225e176962 | |||
| 75b5301c9a | |||
| 6d780ff6d5 | |||
| 5ed7751049 | |||
| 8d21ca65d4 | |||
| 20644f8c6a | |||
| 22c36cd1b6 | |||
| 014fd48298 | |||
| 6006347f62 | |||
| fc1d54365e | |||
| 41536c1586 | |||
| 5b52f17db3 | |||
| a44b5cbd84 | |||
| 3eca8fedf0 | |||
| 6136ba0221 | |||
| 9a6874fee7 | |||
| d5483a9d21 | |||
| e4079005ef | |||
| 9c077351e5 | |||
| abe082493e | |||
| 5ea56fd371 | |||
| 2cd1ef8c97 | |||
| 35cf368176 | |||
| ed3a03b439 | |||
| 0b7412e092 | |||
| 3f19c3cfb4 | |||
| 2c2232ce23 | |||
| 1bd4817843 | |||
| 5673b3b48a | |||
| 686468bae6 | |||
| 30d3dad3bb | |||
| 4fa12efce0 | |||
| 38f595a0df | |||
| cdaae17f19 | |||
| 6a0f3a8736 | |||
| 201946c82c | |||
| 9950de034a | |||
| e43a539e10 | |||
| 80474ee9e8 | |||
| cce1fe41a6 | |||
| ae0d7f7089 | |||
| 77d5634729 | |||
| f2d2912241 | |||
| 28de453200 | |||
| d571624920 | |||
| 12659cd7f1 | |||
| 84e737e3dd | |||
| 64f54e0dc4 | |||
| e6568e5a31 | |||
| 06188c7eb6 | |||
| c40fde2eae | |||
| 22ec8c83b5 | |||
| b2507972df | |||
| f524ec2aaa | |||
| 050cede6d8 | |||
| ac1fc5575a | |||
| 536c771a3f | |||
| 3870922e61 | |||
| 9673018068 | |||
| f98f08c00a | |||
| 22120a505a | |||
| 5466990ffc | |||
| ae77ba7af5 | |||
| 3291f83c81 | |||
| 5963365517 | |||
| 3f7b3abaa9 | |||
| a1d6d0ceb2 | |||
| 78f3ac2a67 | |||
| 9ff01f7cb5 | |||
| a4c62ac8c1 | |||
| fa07a148ef | |||
| 4c94e4947a | |||
| 8484ab9ec9 | |||
| 56e572a73e | |||
| 94de68f9b8 | |||
| 89d61861a6 | |||
| f8f1a2dd86 | |||
| 8039c06800 | |||
| b59c6dbd51 | |||
| 7ab1221629 | |||
| 9818289b15 | |||
| 274e050ab4 | |||
| f9aa942b2c | |||
| d4b9e6613f | |||
| 7098d7cb9f | |||
| 945469f0e1 | |||
| 0470c9cc4e | |||
| 1ea8b348dd | |||
| a65a5570a3 | |||
| da15ac0233 | |||
| 07091e866d | |||
| 37d34c2aa4 | |||
| 4934b00dca | |||
| 0f4e00f527 | |||
| d8ba1a5db5 | |||
| 1a5c2aea69 | |||
| e2df259a6a | |||
| c7c4882929 | |||
| bd3600a28e | |||
| 88955a1984 | |||
| 795069eb94 | |||
| e061d00010 | |||
| b6e8c88f0b | |||
| c96e66a40a | |||
| 23e71ce7a5 | |||
| a7c8680ec3 | |||
| 2c6df5b7f0 | |||
| 316575d3b3 | |||
| 9a856f83fe | |||
| c7c98440cf | |||
| 1674e5f87c | |||
| 98af061024 | |||
| b71d42cfb4 | |||
| 337d0eb9e8 | |||
| 316d6c9de5 | |||
| 3e32822706 | |||
| 8314f2e9a2 | |||
| ffe5f336d3 | |||
| 1c45c9200a | |||
| 6c8328eb4e | |||
| 08a62243c5 | |||
| 4feb95d263 | |||
| e3e5bdfbe4 | |||
| c074850dc4 | |||
| 5bfbbb34be | |||
| da231a51e0 | |||
| 47fe981b8e | |||
| a4087a6cab | |||
| 965a8fdfc4 | |||
| ac3c2ec5ba | |||
| 68a72085f2 | |||
| 1131d828f2 | |||
| a7b6456475 | |||
| b9a527603d | |||
| 1e66dd6de0 | |||
| 74b8e6a751 | |||
| c137a7d306 | |||
| 7051341ba6 | |||
| 1db8e1bab4 | |||
| 1da6538756 | |||
| 5d580ac95c | |||
| f76355db90 | |||
| 1cdf348ac5 | |||
| 91e4d8f3d8 | |||
| f8c7167b5d | |||
| 94ec80ae18 | |||
| 6e1a330c81 | |||
| 752bd12fd4 | |||
| 6d7e1cfecb | |||
| daa86448e9 | |||
| 6536a3ff9b | |||
| 604b2a3b58 | |||
| 4e3837eda1 | |||
| 3b7037656e | |||
| fb6cdb3a9f | |||
| 3e718aff10 | |||
| 95fce73f16 | |||
| 07dbce000c | |||
| 9b82b174d0 | |||
| 5d117508d8 | |||
| 02d491fe99 | |||
| 72c744dd22 | |||
| 58fe4d2cbd | |||
| 49361f9a24 | |||
| 129c9d4093 | |||
| 8475b74d0c | |||
| f246d30831 | |||
| 50f32f791b | |||
| 88779cc689 | |||
| f8f3286267 | |||
| b167974308 | |||
| 20ecbc885f | |||
| 4980533b75 | |||
| b6d5a6c619 | |||
| 50e3ae359b | |||
| cf65056e46 | |||
| 6c47187329 | |||
| a471a106c4 | |||
| 5ce5abcd67 | |||
| 73aac2ea23 | |||
| e8ae8a8a76 | |||
| 39901e1577 | |||
| 7a87e8a911 | |||
| 5fd324e17e | |||
| 50889d5519 | |||
| 1cc8177b73 | |||
| 8b33494324 | |||
| 8e2494b4b2 | |||
| 941a4c10cd | |||
| 853fc48867 | |||
| 4ca4651f20 | |||
| 36223fbee8 | |||
| 9d08ae36e9 | |||
| ed34ac0a7c | |||
| ea02bf99d7 | |||
| 52491c6cc7 | |||
| 5c39f8969d | |||
| c86fa207a2 | |||
| 631b1f77f0 | |||
| ec67d8b676 | |||
| 9ceb49dca0 | |||
| 147087cdd2 | |||
| 9800e5ca6f | |||
| ab0f20b63b | |||
| 739a18df53 | |||
| bb66bd562f | |||
| 3aba5ed0ce | |||
| 3ec126df58 | |||
| 59964c5c95 | |||
| 61e4180191 | |||
| 23739a476a | |||
| 8f39f26b9e | |||
| 012dde7fe7 | |||
| 1d77fbd497 | |||
| d3ce732601 | |||
| 838193097d | |||
| 0c90bc8cd3 | |||
| 80011bdf57 | |||
| 17c23edcd1 | |||
| 6f0b92e97c | |||
| e0f6182508 | |||
| d1084b4c26 | |||
| 805361d871 | |||
| 2e3d05e591 | |||
| 8d68b0b251 | |||
| 5b8b45410c | |||
| efc431ba0d | |||
| 9df4aa7210 | |||
| 8481609ba4 | |||
| 194406e923 | |||
| c4665bcf95 | |||
| c70d7d0386 | |||
| 4a3a673d1e | |||
| 31260f8667 | |||
| cb3deb89c1 | |||
| b21aad0837 | |||
| 43b11cfc9e | |||
| 8ba10c6666 | |||
| b7b45f8c0b | |||
| a2ff87ba44 | |||
| 53a3991927 | |||
| 6b502fbc2b | |||
| 8509f1feb4 | |||
| b692832347 | |||
| a5a6a9c91c | |||
| 25c09b1b1d | |||
| f00ddea3fc | |||
| 4caf331102 | |||
| 0705bb0c07 | |||
| 4748f89945 | |||
| f0abaec208 | |||
| 2933c4e061 | |||
| b808efac50 | |||
| 0df30e445f | |||
| d4f03eb0b1 | |||
| 635b4db751 | |||
| 1821997b7c | |||
| a97fde462d | |||
| 9bcfb74256 | |||
| c0fdc91ea3 | |||
| da1d1af843 | |||
| 85d5216840 | |||
| 2337123e89 | |||
| 95eb7d8343 | |||
| 720a26266a | |||
| 1187f828b7 | |||
| 37906d63f9 | |||
| 870e202546 | |||
| 50a72fbcd6 | |||
| b3172cc0fd | |||
| dc39cee4f9 | |||
| f05ff7a66d | |||
| cb2a49009c | |||
| b541ee20fb | |||
| c975694cf5 | |||
| 8c39267d92 | |||
| 45f1ddae59 | |||
| a45fa24362 | |||
| e127e819e6 | |||
| ce5337c902 | |||
| b74030550e | |||
| 2cfcae67a0 | |||
| dbb784d1ab | |||
| be941e5a05 | |||
| 87080161e0 | |||
| 62931543fb | |||
| 3cb6a5a3b2 | |||
| 765aef276c | |||
| 2da65dae97 | |||
| 4be6148785 | |||
| 5e4ffd0559 | |||
| 99d5aff09b | |||
| ae31e9dfa8 | |||
| d077ffd3a5 | |||
| c61f9a4484 | |||
| 37ef74572d | |||
| d08e7b7e8a | |||
| 4441cfe9ce | |||
| 9dd7ac3bc3 | |||
| 2a912052bc | |||
| addc07be5a | |||
| 6aaa78f044 | |||
| 6ffe3ecc9f | |||
| c4b472700b | |||
| 414a461e41 | |||
| 952d572921 | |||
| fc01f863d3 | |||
| 1b43f6b06d | |||
| 86f297c7c2 | |||
| 21bac71bef | |||
| b400040cff | |||
| 0846b4af08 | |||
| d927703799 | |||
| 8fedcb49a5 | |||
| 0c9fe38643 | |||
| 5c7966eacf | |||
| 0ee03dd15a | |||
| 6dbf1dee71 | |||
| a884eea913 | |||
| b41b74aacb | |||
| fa0e63f9da | |||
| 306eaa606f | |||
| 9ff79ab562 | |||
| 4011bfc71f | |||
| dedb1592cb | |||
| 603e31b820 | |||
| 3dab17b872 | |||
| e81b6194e6 | |||
| b54c18392f | |||
| 7960af6567 | |||
| f24cc998f0 | |||
| 07265da01c | |||
| 6b2bbcbb0e | |||
| c1bc5df576 | |||
| 750218868a | |||
| 18aabbd5d5 | |||
| 4f5e2fc266 | |||
| ba801e68d7 | |||
| fbd65fabb2 | |||
| ee0f61cde4 | |||
| 5779862ee7 | |||
| 9a49f51b15 | |||
| 59f947667a | |||
| 6733f41ed1 | |||
| 867d9497f3 | |||
| a9895627a4 | |||
| 1a75be0645 | |||
| 53c3b1bacc | |||
| 730714bc78 | |||
| 4a0abc17d4 | |||
| 7eaadb4bdb | |||
| 2f88edd0c9 | |||
| 03721f06fb | |||
| c0b25af986 | |||
| d869324747 | |||
| 1b1cf1b950 | |||
| d66d85a4d7 | |||
| 67d4f4ea65 | |||
| fe1b4bee20 | |||
| 6292c6299b | |||
| 22177c5c3e | |||
| 9f2e264fd8 | |||
| e1b1a447b5 | |||
| eaf2d6a809 | |||
| a8e2b5cea1 | |||
| 7fda2e9bac | |||
| ea01c38f2c | |||
| ee0a8161dd | |||
| 8adff28414 | |||
| 020810aea0 | |||
| 56930d1d67 | |||
| 2ec76277dc | |||
| bfff57b297 | |||
| 5e7c5b9cec | |||
| caa236c415 | |||
| 52a77683aa | |||
| 6afb3736f8 | |||
| 96582b63db | |||
| a5e149a1b7 | |||
| 7cf3a406ff | |||
| 25d1533b27 | |||
| 462c6e9e2c | |||
| 1db5a1205e | |||
| c3c7b02867 | |||
| 7d1af3ec1d | |||
| 5efefd458e | |||
| ebc198cfb8 | |||
| 1c348b2e8d | |||
| f18444a5d1 | |||
| 6fd48eff30 | |||
| 68082ac364 | |||
| 9edebd4951 | |||
| adfe298af8 | |||
| cd6c7e0068 | |||
| d11806d957 | |||
| 6a5c1ab6f8 | |||
| e453665f7d | |||
| 3f73b4f487 | |||
| 4dbc8e7655 | |||
| 1fb38c5cf6 | |||
| 71b452912a | |||
| 1c9edf3920 | |||
| c4f7a4bc62 | |||
| 9d72d1a9e6 | |||
| 198b653bf7 | |||
| 77a359ca7b | |||
| bf1e44117b | |||
| f8df624e37 | |||
| 704c30b1fb | |||
| a178e09bfb | |||
| 48096011e3 | |||
| 7ef572d39e | |||
| 75ab679716 | |||
| 2dc1afe547 | |||
| 115f4461ce | |||
| c0cf6abf34 | |||
| ff6ea1e369 | |||
| c5c42e96d2 | |||
| 39fd21e4ff | |||
| 49f70858e9 | |||
| 64d75c9a1a | |||
| 695499c681 | |||
| d8c3b7e662 | |||
| 6adc5e89b0 | |||
| c9eeb1a7c1 | |||
| 68e4a7dac8 | |||
| 73bbb3e7a8 | |||
| d00045e8f5 | |||
| 7a8b3702e3 | |||
| 887f9c3fc6 | |||
| 28b0d9256f | |||
| 2e6729e923 | |||
| d3a2788437 | |||
| c04559ccb6 | |||
| a6e4fbe122 | |||
| c1ed11df7a | |||
| 1edd36e905 | |||
| 91aee9939b | |||
| a585ee9432 | |||
| bb0acb3e9c | |||
| 02f08b16c5 | |||
| e206c3b8f2 | |||
| 3246d41cb2 | |||
| 8594f5e8ae | |||
| c0842ca500 | |||
| c670210a1d | |||
| fd908d09fd | |||
| d859e8774c | |||
| a86318bd92 | |||
| 77b19b828b | |||
| d046109d4d | |||
| bc8d03c68d | |||
| f394d9efae | |||
| b67c73c86b | |||
| 9941fbdae4 | |||
| aa23048889 | |||
| c4b8e4ca14 | |||
| e5ec4988b6 | |||
| 35ce17025a | |||
| fc1ddb3289 | |||
| 85b9cf9ce4 | |||
| 05bb501b4f | |||
| 116d41cdfd | |||
| a5cff8a55b | |||
| 06e0263936 | |||
| de98c397f3 | |||
| f091f67f81 | |||
| 7cfbdd0422 | |||
| 089100fdb3 | |||
| 68e85534a8 | |||
| 0f41a0280d | |||
| fd9466fceb | |||
| 6cb1f99459 | |||
| 8bc3576bb3 | |||
| 4158b39809 | |||
| c7afe02401 | |||
| 37524fa1e6 | |||
| 44c7386d16 | |||
| c3f4abed6f | |||
| 2acb24d84e | |||
| 6783597dc0 | |||
| 8c3f81aa69 | |||
| 3dd509b06b | |||
| da1a4189e1 | |||
| 213abaaf97 | |||
| 9e9c0912e3 | |||
| ad7d0f05de |
@@ -65,6 +65,7 @@ lib/validateurlsyntax.php
|
||||
lib/amd/src/popper.js
|
||||
lib/geopattern-php/
|
||||
lib/php-jwt/
|
||||
lib/mdn-polyfills/
|
||||
media/player/videojs/amd/src/video-lazy.js
|
||||
media/player/videojs/amd/src/Youtube-lazy.js
|
||||
media/player/videojs/videojs/
|
||||
|
||||
@@ -66,6 +66,7 @@ lib/validateurlsyntax.php
|
||||
lib/amd/src/popper.js
|
||||
lib/geopattern-php/
|
||||
lib/php-jwt/
|
||||
lib/mdn-polyfills/
|
||||
media/player/videojs/amd/src/video-lazy.js
|
||||
media/player/videojs/amd/src/Youtube-lazy.js
|
||||
media/player/videojs/videojs/
|
||||
|
||||
+12
-4
@@ -2,8 +2,6 @@
|
||||
# process (which uses our internal CI system) this file is here for the benefit
|
||||
# of community developers git clones - see MDL-51458.
|
||||
|
||||
sudo: required
|
||||
|
||||
# We currently disable Travis notifications entirely until https://github.com/travis-ci/travis-ci/issues/4976
|
||||
# is fixed.
|
||||
notifications:
|
||||
@@ -11,10 +9,13 @@ notifications:
|
||||
|
||||
language: php
|
||||
|
||||
os: linux
|
||||
|
||||
dist: xenial
|
||||
|
||||
services:
|
||||
- mysql
|
||||
- docker
|
||||
|
||||
php:
|
||||
# We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
|
||||
@@ -41,7 +42,7 @@ env:
|
||||
# Perform an upgrade test too.
|
||||
- DB=pgsql TASK=UPGRADE
|
||||
|
||||
matrix:
|
||||
jobs:
|
||||
# Enable fast finish.
|
||||
# This will fail the build if a single job fails (except those in allow_failures).
|
||||
# It will not stop the jobs from running.
|
||||
@@ -61,8 +62,9 @@ cache:
|
||||
- $HOME/.npm
|
||||
|
||||
before_install:
|
||||
- docker run -d -p 127.0.0.1:8080:80 --name exttests moodlehq/moodle-exttests
|
||||
# Avoid IPv6 default binding as service (causes redis not to start).
|
||||
sudo service redis-server start --bind 127.0.0.1
|
||||
- sudo service redis-server start --bind 127.0.0.1
|
||||
|
||||
install:
|
||||
- >
|
||||
@@ -169,6 +171,10 @@ before_script:
|
||||
-e "/require_once/i \\\$CFG->phpunit_dataroot = '\/home\/travis\/roots\/phpunit';" \
|
||||
-e "/require_once/i \\\$CFG->phpunit_prefix = 'p_';" \
|
||||
config.php ;
|
||||
# Enable test external resources
|
||||
sed -i \
|
||||
-e "/require_once/i \\define('TEST_EXTERNAL_FILES_HTTP_URL', 'http://127.0.0.1:8080');" \
|
||||
config.php ;
|
||||
# Redis cache store tests
|
||||
sed -i \
|
||||
-e "/require_once/i \\define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1');" \
|
||||
@@ -248,6 +254,8 @@ script:
|
||||
if [ "$TASK" = 'PHPUNIT' ];
|
||||
then
|
||||
vendor/bin/phpunit --fail-on-risky --disallow-test-output --verbose;
|
||||
EXTTESTS_HITS=$(docker logs exttests 2>&1 | grep -Fv -e 'AH00558' -e '[pid 1]' | wc -l)
|
||||
echo -e "\nTest local resources number of hits: ${EXTTESTS_HITS}.\n"
|
||||
fi
|
||||
|
||||
- >
|
||||
|
||||
+30
-13
@@ -112,7 +112,13 @@ module.exports = function(grunt) {
|
||||
eslint: {
|
||||
// Even though warnings dont stop the build we don't display warnings by default because
|
||||
// at this moment we've got too many core warnings.
|
||||
options: {quiet: !grunt.option('show-lint-warnings')},
|
||||
// To display warnings call: grunt eslint --show-lint-warnings
|
||||
// To fail on warnings call: grunt eslint --max-lint-warnings=0
|
||||
// Also --max-lint-warnings=-1 can be used to display warnings but not fail.
|
||||
options: {
|
||||
quiet: (!grunt.option('show-lint-warnings')) && (typeof grunt.option('max-lint-warnings') === 'undefined'),
|
||||
maxWarnings: ((typeof grunt.option('max-lint-warnings') !== 'undefined') ? grunt.option('max-lint-warnings') : -1)
|
||||
},
|
||||
amd: {src: amdSrc},
|
||||
// Check YUI module source files.
|
||||
yui: {src: ['**/yui/src/**/*.js', '!*/**/yui/src/*/meta/*.js']}
|
||||
@@ -151,6 +157,9 @@ module.exports = function(grunt) {
|
||||
tasks: ['yui']
|
||||
},
|
||||
gherkinlint: {
|
||||
options: {
|
||||
nospawn: false,
|
||||
},
|
||||
files: ['**/tests/behat/*.feature'],
|
||||
tasks: ['gherkinlint']
|
||||
}
|
||||
@@ -293,19 +302,27 @@ module.exports = function(grunt) {
|
||||
};
|
||||
|
||||
tasks.gherkinlint = function() {
|
||||
var done = this.async(),
|
||||
options = grunt.config('gherkinlint.options');
|
||||
const done = this.async();
|
||||
const options = grunt.config('gherkinlint.options');
|
||||
|
||||
var args = grunt.file.expand(options.files);
|
||||
args.unshift(path.normalize(__dirname + '/node_modules/.bin/gherkin-lint'));
|
||||
grunt.util.spawn({
|
||||
cmd: 'node',
|
||||
args: args,
|
||||
opts: {stdio: 'inherit', env: process.env}
|
||||
}, function(error, result, code) {
|
||||
// Propagate the exit code.
|
||||
done(code === 0);
|
||||
});
|
||||
// Grab the gherkin-lint linter and required scaffolding.
|
||||
const linter = require('gherkin-lint/src/linter.js');
|
||||
const featureFinder = require('gherkin-lint/src/feature-finder.js');
|
||||
const configParser = require('gherkin-lint/src/config-parser.js');
|
||||
const formatter = require('gherkin-lint/src/formatters/stylish.js');
|
||||
|
||||
// Run the linter.
|
||||
const results = linter.lint(
|
||||
featureFinder.getFeatureFiles(grunt.file.expand(options.files)),
|
||||
configParser.getConfiguration(configParser.defaultConfigFileName)
|
||||
);
|
||||
|
||||
// Print the results out uncondtionally.
|
||||
formatter.printResults(results);
|
||||
|
||||
// Report on the results.
|
||||
// The done function takes a bool whereby a falsey statement causes the task to fail.
|
||||
done(results.every(result => result.errors.length === 0));
|
||||
};
|
||||
|
||||
tasks.startup = function() {
|
||||
|
||||
+25
-8
@@ -226,7 +226,7 @@ if (!core_tables_exist()) {
|
||||
}
|
||||
if (empty($confirmrelease)) {
|
||||
require_once($CFG->libdir.'/environmentlib.php');
|
||||
list($envstatus, $environment_results) = check_moodle_environment(normalize_version($release), ENV_SELECT_RELEASE);
|
||||
list($envstatus, $environmentresults) = check_moodle_environment(normalize_version($release), ENV_SELECT_RELEASE);
|
||||
$strcurrentrelease = get_string('currentrelease');
|
||||
|
||||
$PAGE->navbar->add($strcurrentrelease);
|
||||
@@ -235,7 +235,7 @@ if (!core_tables_exist()) {
|
||||
$PAGE->set_cacheable(false);
|
||||
|
||||
$output = $PAGE->get_renderer('core', 'admin');
|
||||
echo $output->install_environment_page($maturity, $envstatus, $environment_results, $release);
|
||||
echo $output->install_environment_page($maturity, $envstatus, $environmentresults, $release);
|
||||
die();
|
||||
}
|
||||
|
||||
@@ -358,9 +358,9 @@ if (!$cache and $version > $CFG->version) { // upgrade
|
||||
echo $output->upgrade_confirm_page($a->newversion, $maturity, $testsite);
|
||||
die();
|
||||
|
||||
} else if (empty($confirmrelease)){
|
||||
} else if (empty($confirmrelease)) {
|
||||
require_once($CFG->libdir.'/environmentlib.php');
|
||||
list($envstatus, $environment_results) = check_moodle_environment($release, ENV_SELECT_RELEASE);
|
||||
list($envstatus, $environmentresults) = check_moodle_environment($release, ENV_SELECT_RELEASE);
|
||||
$strcurrentrelease = get_string('currentrelease');
|
||||
|
||||
$PAGE->navbar->add($strcurrentrelease);
|
||||
@@ -368,7 +368,7 @@ if (!$cache and $version > $CFG->version) { // upgrade
|
||||
$PAGE->set_heading($strcurrentrelease);
|
||||
$PAGE->set_cacheable(false);
|
||||
|
||||
echo $output->upgrade_environment_page($release, $envstatus, $environment_results);
|
||||
echo $output->upgrade_environment_page($release, $envstatus, $environmentresults);
|
||||
die();
|
||||
|
||||
} else if (empty($confirmplugins)) {
|
||||
@@ -533,7 +533,10 @@ if (!$cache and $branch <> $CFG->branch) { // Update the branch
|
||||
|
||||
if (!$cache and moodle_needs_upgrading()) {
|
||||
|
||||
$PAGE->set_url(new moodle_url($PAGE->url, array('confirmplugincheck' => $confirmplugins)));
|
||||
$PAGE->set_url(new moodle_url($PAGE->url, array(
|
||||
'confirmrelease' => $confirmrelease,
|
||||
'confirmplugincheck' => $confirmplugins,
|
||||
)));
|
||||
|
||||
check_upgrade_key($upgradekeyhash);
|
||||
|
||||
@@ -543,7 +546,21 @@ if (!$cache and moodle_needs_upgrading()) {
|
||||
$pluginman = core_plugin_manager::instance();
|
||||
$output = $PAGE->get_renderer('core', 'admin');
|
||||
|
||||
if (!$confirmplugins) {
|
||||
if (empty($confirmrelease)) {
|
||||
require_once($CFG->libdir . '/environmentlib.php');
|
||||
|
||||
list($envstatus, $environmentresults) = check_moodle_environment($release, ENV_SELECT_RELEASE);
|
||||
$strcurrentrelease = get_string('currentrelease');
|
||||
|
||||
$PAGE->navbar->add($strcurrentrelease);
|
||||
$PAGE->set_title($strcurrentrelease);
|
||||
$PAGE->set_heading($strcurrentrelease);
|
||||
$PAGE->set_cacheable(false);
|
||||
|
||||
echo $output->upgrade_environment_page($release, $envstatus, $environmentresults);
|
||||
die();
|
||||
|
||||
} else if (!$confirmplugins) {
|
||||
$strplugincheck = get_string('plugincheck');
|
||||
|
||||
$PAGE->navbar->add($strplugincheck);
|
||||
@@ -802,7 +819,7 @@ $SESSION->admin_critical_warning = ($insecuredataroot==INSECURE_DATAROOT_ERROR);
|
||||
$adminroot = admin_get_root();
|
||||
|
||||
// Check if there are any new admin settings which have still yet to be set
|
||||
if (any_new_admin_settings($adminroot)){
|
||||
if (any_new_admin_settings($adminroot)) {
|
||||
redirect('upgradesettings.php');
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ if (($form = data_submitted()) && confirm_sesskey()) {
|
||||
}
|
||||
} else {
|
||||
$newsettings = array();
|
||||
if (array_key_exists($componentprovidersetting, $form)) {
|
||||
if (property_exists($form, $componentprovidersetting)) {
|
||||
// We must be processing loggedin or loggedoff checkboxes.
|
||||
// Store defained comma-separated processors as setting value.
|
||||
// Using array_filter eliminates elements set to 0 above.
|
||||
|
||||
@@ -45,7 +45,7 @@ $error = optional_param('error', '', PARAM_ALPHANUM);
|
||||
|
||||
admin_externalpage_setup('registrationmoodleorg');
|
||||
|
||||
if ($url !== HUB_MOODLEORGHUBURL) {
|
||||
if (parse_url($url, PHP_URL_HOST) !== parse_url(HUB_MOODLEORGHUBURL, PHP_URL_HOST)) {
|
||||
// Allow other plugins to confirm registration on custom hubs. Plugins implementing this
|
||||
// callback need to redirect or exit. See https://docs.moodle.org/en/Hub_registration .
|
||||
$callbacks = get_plugins_with_function('hub_registration');
|
||||
|
||||
@@ -39,7 +39,7 @@ $token = optional_param('token', '', PARAM_TEXT);
|
||||
|
||||
admin_externalpage_setup('registrationmoodleorg');
|
||||
|
||||
if ($url !== HUB_MOODLEORGHUBURL) {
|
||||
if (parse_url($url, PHP_URL_HOST) !== parse_url(HUB_MOODLEORGHUBURL, PHP_URL_HOST)) {
|
||||
// Allow other plugins to renew registration on custom hubs. Plugins implementing this
|
||||
// callback need to redirect or exit. See https://docs.moodle.org/en/Hub_registration .
|
||||
$callbacks = get_plugins_with_function('hub_registration');
|
||||
|
||||
+49
-40
@@ -361,14 +361,15 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
$output = '';
|
||||
|
||||
if ($checker->enabled()) {
|
||||
$output .= $this->container_start('checkforupdates');
|
||||
$output .= $this->container_start('checkforupdates mb-4');
|
||||
$output .= $this->single_button(
|
||||
new moodle_url($reloadurl, array('fetchupdates' => 1)),
|
||||
get_string('checkforupdates', 'core_plugin')
|
||||
);
|
||||
if ($timefetched = $checker->get_last_timefetched()) {
|
||||
$timefetched = userdate($timefetched, get_string('strftimedatetime', 'core_langconfig'));
|
||||
$output .= $this->container(get_string('checkforupdateslast', 'core_plugin', $timefetched), 'lasttimefetched');
|
||||
$output .= $this->container(get_string('checkforupdateslast', 'core_plugin', $timefetched),
|
||||
'lasttimefetched small text-muted mt-1');
|
||||
}
|
||||
$output .= $this->container_end();
|
||||
}
|
||||
@@ -823,7 +824,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
}
|
||||
}
|
||||
|
||||
$updateinfo .= $this->container_start('checkforupdates');
|
||||
$updateinfo .= $this->container_start('checkforupdates mt-1');
|
||||
$fetchurl = new moodle_url('/admin/index.php', array('fetchupdates' => 1, 'sesskey' => sesskey(), 'cache' => 0));
|
||||
$updateinfo .= $this->single_button($fetchurl, get_string('checkforupdates', 'core_plugin'));
|
||||
if ($fetch) {
|
||||
@@ -912,7 +913,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
*/
|
||||
protected function moodle_available_update_info(\core\update\info $updateinfo) {
|
||||
|
||||
$boxclasses = 'moodleupdateinfo';
|
||||
$boxclasses = 'moodleupdateinfo mb-2';
|
||||
$info = array();
|
||||
|
||||
if (isset($updateinfo->release)) {
|
||||
@@ -932,7 +933,8 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
}
|
||||
|
||||
if (isset($updateinfo->download)) {
|
||||
$info[] = html_writer::link($updateinfo->download, get_string('download'), array('class' => 'info download'));
|
||||
$info[] = html_writer::link($updateinfo->download, get_string('download'),
|
||||
array('class' => 'info download btn btn-secondary'));
|
||||
}
|
||||
|
||||
if (isset($updateinfo->url)) {
|
||||
@@ -940,9 +942,9 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
array('class' => 'info more'));
|
||||
}
|
||||
|
||||
$box = $this->output->box_start($boxclasses);
|
||||
$box .= $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $info), '');
|
||||
$box .= $this->output->box_end();
|
||||
$box = $this->output->container_start($boxclasses);
|
||||
$box .= $this->output->container(implode(html_writer::tag('span', ' | ', array('class' => 'separator')), $info), '');
|
||||
$box .= $this->output->container_end();
|
||||
|
||||
return $box;
|
||||
}
|
||||
@@ -1054,7 +1056,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
$displayname = new html_table_cell(
|
||||
$icon.
|
||||
html_writer::span($plugin->displayname, 'pluginname').
|
||||
html_writer::div($plugin->get_dir(), 'plugindir')
|
||||
html_writer::div($plugin->get_dir(), 'plugindir text-muted small')
|
||||
);
|
||||
|
||||
$versiondb = new html_table_cell($plugin->versiondb);
|
||||
@@ -1089,7 +1091,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
break;
|
||||
case core_plugin_manager::PLUGIN_STATUS_NODB:
|
||||
case core_plugin_manager::PLUGIN_STATUS_UPTODATE:
|
||||
$statusclass .= $dependenciesok ? '' : 'badge-warning';
|
||||
$statusclass .= $dependenciesok ? 'badge-light' : 'badge-warning';
|
||||
break;
|
||||
}
|
||||
$status = html_writer::span(get_string('status_' . $statuscode, 'core_plugin'), $statusclass);
|
||||
@@ -1099,7 +1101,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
new moodle_url($this->page->url, array('abortinstall' => $plugin->component)),
|
||||
get_string('cancelinstallone', 'core_plugin'),
|
||||
'post',
|
||||
array('class' => 'actionbutton cancelinstallone')
|
||||
array('class' => 'actionbutton cancelinstallone d-block mt-1')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1108,7 +1110,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
new moodle_url($this->page->url, array('abortupgrade' => $plugin->component)),
|
||||
get_string('cancelupgradeone', 'core_plugin'),
|
||||
'post',
|
||||
array('class' => 'actionbutton cancelupgradeone')
|
||||
array('class' => 'actionbutton cancelupgradeone d-block mt-1')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1178,7 +1180,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
}
|
||||
}
|
||||
|
||||
$out .= $this->output->container_start('actions');
|
||||
$out .= $this->output->container_start('actions mb-2');
|
||||
|
||||
$installableupdates = $pluginman->filter_installable($pluginman->available_updates());
|
||||
if ($installableupdates) {
|
||||
@@ -1186,7 +1188,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
new moodle_url($this->page->url, array('installupdatex' => 1)),
|
||||
get_string('updateavailableinstallall', 'core_admin', count($installableupdates)),
|
||||
'post',
|
||||
array('class' => 'singlebutton updateavailableinstallall')
|
||||
array('class' => 'singlebutton updateavailableinstallall mr-1')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1195,7 +1197,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
new moodle_url($this->page->url, array('abortinstallx' => 1)),
|
||||
get_string('cancelinstallall', 'core_plugin', count($installabortable)),
|
||||
'post',
|
||||
array('class' => 'singlebutton cancelinstallall')
|
||||
array('class' => 'singlebutton cancelinstallall mr-1')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1204,15 +1206,17 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
new moodle_url($this->page->url, array('abortupgradex' => 1)),
|
||||
get_string('cancelupgradeall', 'core_plugin', count($upgradeabortable)),
|
||||
'post',
|
||||
array('class' => 'singlebutton cancelupgradeall')
|
||||
array('class' => 'singlebutton cancelupgradeall mr-1')
|
||||
);
|
||||
}
|
||||
|
||||
$out .= html_writer::div(html_writer::link(new moodle_url($this->page->url, array('showallplugins' => 0)),
|
||||
get_string('plugincheckattention', 'core_plugin')).' '.html_writer::span($sumattention, 'badge'));
|
||||
get_string('plugincheckattention', 'core_plugin')).' '.html_writer::span($sumattention, 'badge badge-light'),
|
||||
'btn btn-link mr-1');
|
||||
|
||||
$out .= html_writer::div(html_writer::link(new moodle_url($this->page->url, array('showallplugins' => 1)),
|
||||
get_string('plugincheckall', 'core_plugin')).' '.html_writer::span($sumtotal, 'badge'));
|
||||
get_string('plugincheckall', 'core_plugin')).' '.html_writer::span($sumtotal, 'badge badge-light'),
|
||||
'btn btn-link mr-1');
|
||||
|
||||
$out .= $this->output->container_end(); // End of .actions container.
|
||||
$out .= $this->output->container_end(); // End of #plugins-check-info container.
|
||||
@@ -1280,12 +1284,13 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
}
|
||||
}
|
||||
|
||||
$out = $this->output->container_start('plugins-check-dependencies');
|
||||
$out = $this->output->container_start('plugins-check-dependencies mb-4');
|
||||
|
||||
if ($unavailable or $unknown) {
|
||||
$out .= $this->output->heading(get_string('misdepsunavail', 'core_plugin'));
|
||||
if ($unknown) {
|
||||
$out .= $this->output->notification(get_string('misdepsunknownlist', 'core_plugin', implode($unknown, ', ')));
|
||||
$out .= $this->output->render((new \core\output\notification(get_string('misdepsunknownlist', 'core_plugin',
|
||||
implode(', ', $unknown))))->set_show_closebutton(false));
|
||||
}
|
||||
if ($unavailable) {
|
||||
$unavailablelist = array();
|
||||
@@ -1299,18 +1304,18 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
}
|
||||
$unavailablelist[] = $unavailablelistitem;
|
||||
}
|
||||
$out .= $this->output->notification(get_string('misdepsunavaillist', 'core_plugin',
|
||||
implode($unavailablelist, ', ')));
|
||||
$out .= $this->output->render((new \core\output\notification(get_string('misdepsunavaillist', 'core_plugin',
|
||||
implode(', ', $unavailablelist))))->set_show_closebutton(false));
|
||||
}
|
||||
$out .= $this->output->container_start('plugins-check-dependencies-actions');
|
||||
$out .= $this->output->container_start('plugins-check-dependencies-actions mb-4');
|
||||
$out .= ' '.html_writer::link(new moodle_url('/admin/tool/installaddon/'),
|
||||
get_string('dependencyuploadmissing', 'core_plugin'));
|
||||
get_string('dependencyuploadmissing', 'core_plugin'), array('class' => 'btn btn-secondary'));
|
||||
$out .= $this->output->container_end(); // End of .plugins-check-dependencies-actions container.
|
||||
}
|
||||
|
||||
if ($available) {
|
||||
$out .= $this->output->heading(get_string('misdepsavail', 'core_plugin'));
|
||||
$out .= $this->output->container_start('plugins-check-dependencies-actions');
|
||||
$out .= $this->output->container_start('plugins-check-dependencies-actions mb-2');
|
||||
|
||||
$installable = $pluginman->filter_installable($available);
|
||||
if ($installable) {
|
||||
@@ -1318,12 +1323,13 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
new moodle_url($this->page->url, array('installdepx' => 1)),
|
||||
get_string('dependencyinstallmissing', 'core_plugin', count($installable)),
|
||||
'post',
|
||||
array('class' => 'singlebutton dependencyinstallmissing')
|
||||
array('class' => 'singlebutton dependencyinstallmissing d-inline-block mr-1')
|
||||
);
|
||||
}
|
||||
|
||||
$out .= html_writer::div(html_writer::link(new moodle_url('/admin/tool/installaddon/'),
|
||||
get_string('dependencyuploadmissing', 'core_plugin')), 'dependencyuploadmissing');
|
||||
get_string('dependencyuploadmissing', 'core_plugin'), array('class' => 'btn btn-link')),
|
||||
'dependencyuploadmissing d-inline-block mr-1');
|
||||
|
||||
$out .= $this->output->container_end(); // End of .plugins-check-dependencies-actions container.
|
||||
|
||||
@@ -1364,7 +1370,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
if ($CFG->branch == str_replace('.', '', $moodle->release)) {
|
||||
$supportedmoodles[] = html_writer::span($moodle->release, 'badge badge-success');
|
||||
} else {
|
||||
$supportedmoodles[] = html_writer::span($moodle->release, 'label');
|
||||
$supportedmoodles[] = html_writer::span($moodle->release, 'badge badge-light');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1378,7 +1384,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
}
|
||||
$info = html_writer::div(
|
||||
get_string('requiredby', 'core_plugin', implode(', ', $requriedby)),
|
||||
'requiredby'
|
||||
'requiredby mb-1'
|
||||
);
|
||||
} else {
|
||||
$info = '';
|
||||
@@ -1389,35 +1395,36 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
$info .= html_writer::div(
|
||||
html_writer::link('https://moodle.org/plugins/view.php?plugin='.$plugin->component,
|
||||
get_string('misdepinfoplugin', 'core_plugin')),
|
||||
'misdepinfoplugin'
|
||||
'misdepinfoplugin d-inline-block mr-3 mb-1'
|
||||
);
|
||||
|
||||
$info .= html_writer::div(
|
||||
html_writer::link('https://moodle.org/plugins/pluginversion.php?id='.$plugin->version->id,
|
||||
get_string('misdepinfoversion', 'core_plugin')),
|
||||
'misdepinfoversion'
|
||||
'misdepinfoversion d-inline-block mr-3 mb-1'
|
||||
);
|
||||
|
||||
$info .= html_writer::div(html_writer::link($plugin->version->downloadurl, get_string('download')), 'misdepdownload');
|
||||
$info .= html_writer::div(html_writer::link($plugin->version->downloadurl, get_string('download')),
|
||||
'misdepdownload d-inline-block mr-3 mb-1');
|
||||
|
||||
if ($pluginman->is_remote_plugin_installable($plugin->component, $plugin->version->version, $reason)) {
|
||||
$info .= $this->output->single_button(
|
||||
new moodle_url($this->page->url, array('installdep' => $plugin->component)),
|
||||
get_string('dependencyinstall', 'core_plugin'),
|
||||
'post',
|
||||
array('class' => 'singlebutton dependencyinstall')
|
||||
array('class' => 'singlebutton dependencyinstall mr-3 mb-1')
|
||||
);
|
||||
} else {
|
||||
$reasonhelp = $this->info_remote_plugin_not_installable($reason);
|
||||
if ($reasonhelp) {
|
||||
$info .= html_writer::div($reasonhelp, 'reasonhelp dependencyinstall');
|
||||
$info .= html_writer::div($reasonhelp, 'reasonhelp dependencyinstall d-inline-block mr-3 mb-1');
|
||||
}
|
||||
}
|
||||
|
||||
$info .= $this->output->container_end(); // End of .actions container.
|
||||
|
||||
$table->data[] = array(
|
||||
html_writer::div($plugin->name, 'name').' '.html_writer::div($plugin->component, 'component'),
|
||||
html_writer::div($plugin->name, 'name').' '.html_writer::div($plugin->component, 'component text-muted small'),
|
||||
$plugin->version->release,
|
||||
$plugin->version->version,
|
||||
implode($supportedmoodles, ' '),
|
||||
@@ -1463,7 +1470,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
foreach ($pluginman->resolve_requirements($plugin, $version) as $reqname => $reqinfo) {
|
||||
if ($reqname === 'core') {
|
||||
if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
|
||||
$class = 'requires-ok';
|
||||
$class = 'requires-ok text-muted';
|
||||
$label = '';
|
||||
} else {
|
||||
$class = 'requires-failed';
|
||||
@@ -1480,7 +1487,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
|
||||
if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
|
||||
$label = '';
|
||||
$class = 'requires-ok';
|
||||
$class = 'requires-ok text-muted';
|
||||
|
||||
} else if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_MISSING) {
|
||||
if ($reqinfo->availability == $pluginman::REQUIREMENT_AVAILABLE) {
|
||||
@@ -1534,13 +1541,14 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
return '';
|
||||
}
|
||||
|
||||
$out = html_writer::tag('ul', implode("\n", $requires));
|
||||
$out = html_writer::tag('ul', implode("\n", $requires), array('class' => 'm-0'));
|
||||
|
||||
if ($displayuploadlink) {
|
||||
$out .= html_writer::div(
|
||||
html_writer::link(
|
||||
new moodle_url('/admin/tool/installaddon/'),
|
||||
get_string('dependencyuploadmissing', 'core_plugin')
|
||||
get_string('dependencyuploadmissing', 'core_plugin'),
|
||||
array('class' => 'btn btn-secondary btn-sm m-1')
|
||||
),
|
||||
'dependencyuploadmissing'
|
||||
);
|
||||
@@ -1550,7 +1558,8 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
$out .= html_writer::div(
|
||||
html_writer::link(
|
||||
new moodle_url($this->page->url, array('sesskey' => sesskey(), 'fetchupdates' => 1)),
|
||||
get_string('checkforupdates', 'core_plugin')
|
||||
get_string('checkforupdates', 'core_plugin'),
|
||||
array('class' => 'btn btn-secondary btn-sm m-1')
|
||||
),
|
||||
'checkforupdates'
|
||||
);
|
||||
|
||||
@@ -105,13 +105,6 @@ if (($hassiteconfig || has_any_capability(array(
|
||||
new lang_string('sitebackpack_help', 'badges'),
|
||||
1, $choices));
|
||||
|
||||
$warning = badges_verify_site_backpack();
|
||||
if (!empty($warning)) {
|
||||
$backpacksettings->add(new admin_setting_description('badges_site_backpack_verify',
|
||||
new lang_string('sitebackpackverify', 'badges'),
|
||||
$warning));
|
||||
}
|
||||
|
||||
$ADMIN->add('badges', $backpacksettings);
|
||||
|
||||
$ADMIN->add('badges',
|
||||
|
||||
@@ -97,7 +97,10 @@ $options = array(
|
||||
GETREMOTEADDR_SKIP_HTTP_CLIENT_IP => 'HTTP_X_FORWARDED_FOR, REMOTE_ADDR',
|
||||
GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR => 'HTTP_CLIENT, REMOTE_ADDR',
|
||||
GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP => 'REMOTE_ADDR');
|
||||
$temp->add(new admin_setting_configselect('getremoteaddrconf', new lang_string('getremoteaddrconf', 'admin'), new lang_string('configgetremoteaddrconf', 'admin'), 0, $options));
|
||||
$temp->add(new admin_setting_configselect('getremoteaddrconf', new lang_string('getremoteaddrconf', 'admin'),
|
||||
new lang_string('configgetremoteaddrconf', 'admin'),
|
||||
GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP, $options));
|
||||
$temp->add(new admin_setting_configtext('reverseproxyignore', new lang_string('reverseproxyignore', 'admin'), new lang_string('configreverseproxyignore', 'admin'), ''));
|
||||
|
||||
$temp->add(new admin_setting_heading('webproxy', new lang_string('webproxy', 'admin'), new lang_string('webproxyinfo', 'admin')));
|
||||
$temp->add(new admin_setting_configtext('proxyhost', new lang_string('proxyhost', 'admin'), new lang_string('configproxyhost', 'admin'), '', PARAM_HOST));
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ $renderer = $PAGE->get_renderer('tool_task');
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->render_from_template('core_admin/tasklogs', (object) [
|
||||
'action' => $pageurl->out(),
|
||||
'filter' => $filter,
|
||||
'filter' => htmlentities($filter),
|
||||
'resultfilteroptions' => [
|
||||
(object) [
|
||||
'value' => -1,
|
||||
|
||||
@@ -46,7 +46,8 @@ if ($data) {
|
||||
$emailuser->email = $data->recipient;
|
||||
$emailuser->id = -99;
|
||||
|
||||
$subject = get_string('testoutgoingmailconf_subject', 'admin', $SITE->fullname);
|
||||
$subject = get_string('testoutgoingmailconf_subject', 'admin',
|
||||
format_string($SITE->fullname, true, ['context' => context_system::instance()]));
|
||||
$messagetext = get_string('testoutgoingmailconf_message', 'admin');
|
||||
|
||||
// Manage Moodle debugging options.
|
||||
|
||||
@@ -164,6 +164,8 @@ if ($options['install']) {
|
||||
mtrace("Acceptance tests site installed");
|
||||
}
|
||||
|
||||
// Note: Do not build the themes here. This is done during the 'enable' stage.
|
||||
|
||||
} else if ($options['drop']) {
|
||||
// Ensure no tests are running.
|
||||
test_lock::acquire('behat');
|
||||
@@ -182,6 +184,10 @@ if ($options['install']) {
|
||||
// Enable test mode.
|
||||
behat_util::start_test_mode($options['add-core-features-to-theme'], $options['optimize-runs'], $parallel, $run);
|
||||
|
||||
// Themes are only built in the 'enable' command.
|
||||
behat_util::build_themes();
|
||||
mtrace("Testing environment themes built");
|
||||
|
||||
// This is only displayed once for parallel install.
|
||||
if (empty($run)) {
|
||||
// Notify user that 2.5 profile has been converted to 3.5.
|
||||
|
||||
@@ -32,7 +32,7 @@ require_once($CFG->libdir . '/behat/classes/behat_config_manager.php');
|
||||
// systems, but let's allow room for expansion.
|
||||
core_php_time_limit::raise(300);
|
||||
|
||||
$filter = optional_param('filter', '', PARAM_ALPHANUMEXT);
|
||||
$filter = optional_param('filter', '', PARAM_NOTAGS);
|
||||
$type = optional_param('type', false, PARAM_ALPHAEXT);
|
||||
$component = optional_param('component', '', PARAM_ALPHAEXT);
|
||||
|
||||
|
||||
@@ -55,13 +55,13 @@ class tool_behat {
|
||||
// The loaded steps depends on the component specified.
|
||||
behat_config_manager::update_config_file($component, false);
|
||||
|
||||
// The Moodle\BehatExtension\HelpPrinter\MoodleDefinitionsPrinter will parse this search format.
|
||||
// The Moodle\BehatExtension\Definition\Printer\ConsoleDefinitionInformationPrinter will parse this search format.
|
||||
if ($type) {
|
||||
$filter .= '&&' . $type;
|
||||
}
|
||||
|
||||
if ($filter) {
|
||||
$filteroption = ' -d "' . $filter . '"';
|
||||
$filteroption = ' -d ' . escapeshellarg($filter);
|
||||
} else {
|
||||
$filteroption = ' -di';
|
||||
}
|
||||
|
||||
@@ -44,13 +44,9 @@ class tool_behat_renderer extends plugin_renderer_base {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/behat/classes/behat_selectors.php');
|
||||
|
||||
$html = $this->generic_info();
|
||||
|
||||
// Form.
|
||||
ob_start();
|
||||
$form->display();
|
||||
$html .= ob_get_contents();
|
||||
ob_end_clean();
|
||||
$html = $this->output->header();
|
||||
$html .= $this->output->heading(get_string('pluginname', 'tool_behat'));
|
||||
$html .= $form->render();
|
||||
|
||||
if (empty($stepsdefinitions)) {
|
||||
$stepsdefinitions = get_string('nostepsdefinitions', 'tool_behat');
|
||||
@@ -128,7 +124,9 @@ class tool_behat_renderer extends plugin_renderer_base {
|
||||
*/
|
||||
public function render_error($msg) {
|
||||
|
||||
$html = $this->generic_info();
|
||||
$html = $this->output->header();
|
||||
$html .= $this->output->heading(get_string('pluginname', 'tool_behat'));
|
||||
$html .= $this->generic_info();
|
||||
|
||||
$a = new stdClass();
|
||||
$a->errormsg = $msg;
|
||||
@@ -153,13 +151,7 @@ class tool_behat_renderer extends plugin_renderer_base {
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generic_info() {
|
||||
|
||||
$title = get_string('pluginname', 'tool_behat');
|
||||
|
||||
// Header.
|
||||
$html = $this->output->header();
|
||||
$html .= $this->output->heading($title);
|
||||
public function generic_info() {
|
||||
|
||||
// Info.
|
||||
$installurl = behat_command::DOCS_URL;
|
||||
@@ -175,8 +167,7 @@ class tool_behat_renderer extends plugin_renderer_base {
|
||||
);
|
||||
|
||||
// List of steps.
|
||||
$html .= $this->output->box_start();
|
||||
$html .= html_writer::tag('h3', get_string('infoheading', 'tool_behat'));
|
||||
$html = $this->output->box_start();
|
||||
$html .= html_writer::tag('div', get_string('aim', 'tool_behat'));
|
||||
$html .= html_writer::start_tag('div');
|
||||
$html .= html_writer::start_tag('ul');
|
||||
|
||||
@@ -40,8 +40,14 @@ class steps_definitions_form extends moodleform {
|
||||
* @return void
|
||||
*/
|
||||
public function definition() {
|
||||
global $PAGE;
|
||||
|
||||
$mform = $this->_form;
|
||||
$output = $PAGE->get_renderer('tool_behat');
|
||||
|
||||
$mform->addElement('header', 'info', get_string('infoheading', 'tool_behat'));
|
||||
$mform->setExpanded('info', false);
|
||||
$mform->addElement('html', $output->generic_info());
|
||||
|
||||
$mform->addElement('header', 'filters', get_string('stepsdefinitionsfilters', 'tool_behat'));
|
||||
|
||||
|
||||
+20
-14
@@ -1,25 +1,31 @@
|
||||
.steps-definitions {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: #bbb;
|
||||
padding: 5px;
|
||||
margin: auto;
|
||||
width: 50%;
|
||||
#page-admin-tool-behat-index .steps-definitions {
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.steps-definitions .step {
|
||||
margin: 10px 0 10px 0;
|
||||
#page-admin-tool-behat-index .steps-definitions .step {
|
||||
margin: 1rem 0 0 0;
|
||||
border: 1px solid #eee;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.steps-definitions .stepdescription {
|
||||
color: #bf8c12;
|
||||
#page-admin-tool-behat-index .steps-definitions .stepdescription {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.steps-definitions .steptype {
|
||||
#page-admin-tool-behat-index .steps-definitions .stepcontent {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
#page-admin-tool-behat-index .steps-definitions .steptype {
|
||||
color: #1467a6;
|
||||
margin-right: 5px;
|
||||
margin-right: 1ex;
|
||||
}
|
||||
|
||||
.steps-definitions .stepregex {
|
||||
#page-admin-tool-behat-index .steps-definitions .stepapipath {
|
||||
font-family: monospace;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
#page-admin-tool-behat-index .steps-definitions .stepregex {
|
||||
color: #060;
|
||||
}
|
||||
|
||||
@@ -26,3 +26,11 @@ Feature: List the system steps definitions
|
||||
Given I set the field "Contains" to "homepage"
|
||||
When I press "Filter"
|
||||
Then I should see "Opens Moodle homepage."
|
||||
|
||||
@javascript
|
||||
Scenario: Filtering by the multiple words pattern
|
||||
Given I set the field "Contains" to "should exist"
|
||||
When I press "Filter"
|
||||
Then I should not see "There aren't steps definitions matching this filter"
|
||||
And I should see "Checks the provided element and selector type exists in the current page."
|
||||
And I should see "Checks that an element and selector type exists in another element and selector type on the current page."
|
||||
|
||||
@@ -75,6 +75,7 @@ class tool_capability_renderer extends plugin_renderer_base {
|
||||
* @return string
|
||||
*/
|
||||
public function capability_comparison_table(array $capabilities, $contextid, array $roles) {
|
||||
static $capabilitycontexts = array();
|
||||
|
||||
$strpermissions = $this->get_permission_strings();
|
||||
$permissionclasses = $this->get_permission_classes();
|
||||
@@ -92,8 +93,13 @@ class tool_capability_renderer extends plugin_renderer_base {
|
||||
}
|
||||
$table->data = array();
|
||||
|
||||
$childcontextsids = [];
|
||||
foreach ($capabilities as $capability) {
|
||||
$contexts = tool_capability_calculate_role_data($capability, $roles);
|
||||
if (empty($capabilitycontexts[$capability])) {
|
||||
$capabilitycontexts[$capability] = tool_capability_calculate_role_data($capability, $roles);
|
||||
}
|
||||
$contexts = $capabilitycontexts[$capability];
|
||||
|
||||
$captitle = new html_table_cell(get_capability_string($capability) . html_writer::span($capability));
|
||||
$captitle->header = true;
|
||||
|
||||
@@ -111,6 +117,10 @@ class tool_capability_renderer extends plugin_renderer_base {
|
||||
}
|
||||
|
||||
$table->data[] = $row;
|
||||
if (!empty($contexts[$contextid]->children)) {
|
||||
$childcontextsids = array_merge($childcontextsids, $contexts[$contextid]->children);
|
||||
$childcontextsids = array_unique($childcontextsids);
|
||||
}
|
||||
}
|
||||
|
||||
// Start the list item, and print the context name as a link to the place to make changes.
|
||||
@@ -127,9 +137,9 @@ class tool_capability_renderer extends plugin_renderer_base {
|
||||
$html = $this->output->heading(html_writer::link($url, $context->get_context_name(), ['title' => $title]), 3);
|
||||
$html .= html_writer::table($table);
|
||||
// If there are any child contexts, print them recursively.
|
||||
if (!empty($contexts[$contextid]->children)) {
|
||||
foreach ($contexts[$contextid]->children as $childcontextid) {
|
||||
$html .= $this->capability_comparison_table($capabilities, $childcontextid, $roles, true);
|
||||
if (!empty($childcontextsids)) {
|
||||
foreach ($childcontextsids as $childcontextid) {
|
||||
$html .= $this->capability_comparison_table($capabilities, $childcontextid, $roles);
|
||||
}
|
||||
}
|
||||
return $html;
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
@tool @tool_capability
|
||||
Feature: Show capabilities for multiple contexts
|
||||
In order to check roles capabilities
|
||||
As an admin
|
||||
I need to be able to see capability overrides on several contexts
|
||||
|
||||
Background:
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
| Course 2 | C2 | 0 |
|
||||
And the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| enrol/category:config | Allow | student | Course | C1 |
|
||||
| enrol/cohort:unenrol | Allow | student | Course | C2 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Users > Permissions > Capability overview" in site administration
|
||||
|
||||
Scenario: Show capabilities table with one capability with overrides
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/category:config |
|
||||
| Roles: | Student |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should see "Category: Miscellaneous"
|
||||
And I should see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with one capability without overrides
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/cohort:config |
|
||||
| Roles: | Student |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should not see "Category: Miscellaneous"
|
||||
And I should not see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with two capabilities, 1st without overrides and 2nd with
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/category:synchronised, enrol/category:config |
|
||||
| Roles: | Student |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should see "Category: Miscellaneous"
|
||||
And I should see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with two capabilities, 1st with overrides and 2nd without
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/category:config, enrol/cohort:config |
|
||||
| Roles: | Student |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should see "Category: Miscellaneous"
|
||||
And I should see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with two capabilities, none with overrides
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/category:synchronised, enrol/cohort:config |
|
||||
| Roles: | Student |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should not see "Category: Miscellaneous"
|
||||
And I should not see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with capability with override and no role selected
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/category:config |
|
||||
| Roles: | |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should see "Category: Miscellaneous"
|
||||
And I should see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with capability without override and no role selected
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/cohort:config |
|
||||
| Roles: | |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should not see "Category: Miscellaneous"
|
||||
And I should not see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with capability with two overrides on different contexts
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/category:config, enrol/cohort:unenrol |
|
||||
| Roles: | |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should see "Category: Miscellaneous"
|
||||
And I should see "Course: Course 1"
|
||||
And I should see "Course: Course 2"
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Plugin upgrade code
|
||||
*
|
||||
* @package tool_cohortroles
|
||||
* @copyright 2020 Paul Holden <paulh@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Function to upgrade tool_cohortroles.
|
||||
*
|
||||
* @param int $oldversion the version we are upgrading from
|
||||
* @return bool result
|
||||
*/
|
||||
function xmldb_tool_cohortroles_upgrade($oldversion) {
|
||||
global $DB;
|
||||
|
||||
if ($oldversion < 2019052001) {
|
||||
// Delete any tool_cohortroles mappings for roles which no longer exist.
|
||||
$DB->delete_records_select('tool_cohortroles', 'roleid NOT IN (SELECT id FROM {role})');
|
||||
|
||||
// Cohortroles savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2019052001, 'tool', 'cohortroles');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
$plugin->version = 2019052000; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2019052001; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2019051100; // Requires this Moodle version.
|
||||
$plugin->component = 'tool_cohortroles'; // Full name of the plugin (used for diagnostics).
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates"],function(a,b,c,d,e,f,g){function h(a){var e={methodname:"tool_dataprivacy_contact_dpo",args:{message:a}},f="success";b.call([e])[0].then(function(a){return a.result?d.get_string("requestsubmitted","tool_dataprivacy"):(f="error",a.warnings.join("<br>"))}).done(function(a){c.addNotification({message:a,type:f})}).fail(c.exception)}var i={CANCEL_REQUEST:'[data-action="cancel"]',CONTACT_DPO:'[data-action="contactdpo"]'},j=function(){this.registerEvents()};return j.prototype.registerEvents=function(){a(i.CANCEL_REQUEST).click(function(g){g.preventDefault();var h=a(this).data("requestid"),i=[{key:"cancelrequest",component:"tool_dataprivacy"},{key:"cancelrequestconfirmation",component:"tool_dataprivacy"}];d.get_strings(i).then(function(a){var d=a[0],g=a[1];return e.create({title:d,body:g,type:e.types.SAVE_CANCEL}).then(function(a){return a.setSaveButtonText(d),a.getRoot().on(f.save,function(){var a={requestid:h},d={methodname:"tool_dataprivacy_cancel_data_request",args:a};b.call([d])[0].done(function(a){a.result?window.location.reload():c.addNotification({message:a.warnings[0].message,type:"error"})}).fail(c.exception)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a})}).done(function(a){a.show()}).fail(c.exception)}),a(i.CONTACT_DPO).click(function(b){b.preventDefault();var i=a(this).data("replytoemail"),j=[{key:"contactdataprotectionofficer",component:"tool_dataprivacy"},{key:"send",component:"tool_dataprivacy"}],k="";d.get_strings(j).then(function(a){var b=a[0];k=a[1];var c={replytoemail:i};return e.create({title:b,body:g.render("tool_dataprivacy/contact_dpo",c),type:e.types.SAVE_CANCEL,large:!0})}).done(function(b){b.setSaveButtonText(k),b.getRoot().on(f.save,function(b){var c=a("#message").val().trim();0===c.length?(b.preventDefault(),a('[data-region="messageinput"]').addClass("has-danger notifyproblem"),a("#id_error_message").removeAttr("hidden")):h(c)}),b.getRoot().on(f.hidden,function(){b.destroy()}),b.show()}).fail(c.exception)})},{init:function(){return new j}}});
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates","core/pending"],function(a,b,c,d,e,f,g,h){function i(a){var e={methodname:"tool_dataprivacy_contact_dpo",args:{message:a}},f="success";b.call([e])[0].then(function(a){return a.result?d.get_string("requestsubmitted","tool_dataprivacy"):(f="error",a.warnings.join("<br>"))}).done(function(a){c.addNotification({message:a,type:f})}).fail(c.exception)}var j={CANCEL_REQUEST:'[data-action="cancel"]',CONTACT_DPO:'[data-action="contactdpo"]'},k=function(){this.registerEvents()};return k.prototype.registerEvents=function(){a(j.CANCEL_REQUEST).click(function(g){g.preventDefault();var h=a(this).data("requestid"),i=[{key:"cancelrequest",component:"tool_dataprivacy"},{key:"cancelrequestconfirmation",component:"tool_dataprivacy"}];d.get_strings(i).then(function(a){var d=a[0],g=a[1];return e.create({title:d,body:g,type:e.types.SAVE_CANCEL}).then(function(a){return a.setSaveButtonText(d),a.getRoot().on(f.save,function(){var a={requestid:h},d={methodname:"tool_dataprivacy_cancel_data_request",args:a};b.call([d])[0].done(function(a){a.result?window.location.reload():c.addNotification({message:a.warnings[0].message,type:"error"})}).fail(c.exception)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a})}).done(function(a){a.show()}).fail(c.exception)}),a(j.CONTACT_DPO).click(function(b){var j=new h("dataprivacy/crud:initModal:contactdpo");b.preventDefault();var k=a(this).data("replytoemail"),l=[{key:"contactdataprotectionofficer",component:"tool_dataprivacy"},{key:"send",component:"tool_dataprivacy"}],m="";d.get_strings(l).then(function(a){var b=a[0];m=a[1];var c={replytoemail:k};return e.create({title:b,body:g.render("tool_dataprivacy/contact_dpo",c),type:e.types.SAVE_CANCEL,large:!0})}).then(function(b){b.setSaveButtonText(m),b.show(),b.getRoot().on(f.save,function(b){var c=a("#message").val().trim();0===c.length?(b.preventDefault(),a('[data-region="messageinput"]').addClass("has-danger notifyproblem"),a("#id_error_message").removeAttr("hidden")):i(c)}),b.getRoot().on(f.hidden,function(){b.destroy()})}).then(j.resolve)["catch"](c.exception)})},{init:function(){return new k}}});
|
||||
@@ -28,8 +28,9 @@ define([
|
||||
'core/str',
|
||||
'core/modal_factory',
|
||||
'core/modal_events',
|
||||
'core/templates'],
|
||||
function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
|
||||
'core/templates',
|
||||
'core/pending'],
|
||||
function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Pending) {
|
||||
|
||||
/**
|
||||
* List of action selectors.
|
||||
@@ -118,6 +119,7 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
|
||||
});
|
||||
|
||||
$(ACTIONS.CONTACT_DPO).click(function(e) {
|
||||
var pendingPromise = new Pending('dataprivacy/crud:initModal:contactdpo');
|
||||
e.preventDefault();
|
||||
|
||||
var replyToEmail = $(this).data('replytoemail');
|
||||
@@ -146,9 +148,12 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
|
||||
type: ModalFactory.types.SAVE_CANCEL,
|
||||
large: true
|
||||
});
|
||||
}).done(function(modal) {
|
||||
}).then(function(modal) {
|
||||
modal.setSaveButtonText(sendButtonText);
|
||||
|
||||
// Show the modal!
|
||||
modal.show();
|
||||
|
||||
// Handle send event.
|
||||
modal.getRoot().on(ModalEvents.save, function(e) {
|
||||
var message = $('#message').val().trim();
|
||||
@@ -169,9 +174,9 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
// Show the modal!
|
||||
modal.show();
|
||||
}).fail(Notification.exception);
|
||||
return;
|
||||
}).then(pendingPromise.resolve)
|
||||
.catch(Notification.exception);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -45,13 +45,22 @@ if ($manage) {
|
||||
$context = context_system::instance();
|
||||
// Make sure the user has the proper capability.
|
||||
require_capability('tool/dataprivacy:managedatarequests', $context);
|
||||
navigation_node::override_active_url($returnurl);
|
||||
} else {
|
||||
// For the case where a user makes request for themselves (or for their children if they are the parent).
|
||||
$returnurl = new moodle_url($CFG->wwwroot . '/admin/tool/dataprivacy/mydatarequests.php');
|
||||
$context = context_user::instance($USER->id);
|
||||
}
|
||||
|
||||
$PAGE->set_context($context);
|
||||
|
||||
if (!$manage && $profilenode = $PAGE->settingsnav->find('myprofile', null)) {
|
||||
$profilenode->make_active();
|
||||
}
|
||||
|
||||
$title = get_string('createnewdatarequest', 'tool_dataprivacy');
|
||||
$PAGE->navbar->add($title);
|
||||
|
||||
// If contactdataprotectionofficer is disabled, send the user back to the profile page, or the privacy policy page.
|
||||
// That is, unless you have sufficient capabilities to perform this on behalf of a user.
|
||||
if (!$manage && !\tool_dataprivacy\api::can_contact_dpo()) {
|
||||
@@ -100,7 +109,6 @@ if ($data = $mform->get_data()) {
|
||||
redirect($returnurl, $redirectmessage);
|
||||
}
|
||||
|
||||
$title = get_string('createnewdatarequest', 'tool_dataprivacy');
|
||||
$PAGE->set_heading($SITE->fullname);
|
||||
$PAGE->set_title($title);
|
||||
echo $OUTPUT->header();
|
||||
|
||||
@@ -319,5 +319,28 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v3.7.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2019052001) {
|
||||
|
||||
// Define field commentsformat to be added to tool_dataprivacy_request.
|
||||
$table = new xmldb_table('tool_dataprivacy_request');
|
||||
$field = new xmldb_field('commentsformat', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'comments');
|
||||
|
||||
// Conditionally launch add field commentsformat.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Define field dpocommentformat to be added to tool_dataprivacy_request.
|
||||
$field = new xmldb_field('dpocommentformat', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'dpocomment');
|
||||
|
||||
// Conditionally launch add field dpocommentformat.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Dataprivacy savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2019052001, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,13 @@ if (isguestuser()) {
|
||||
$usercontext = context_user::instance($USER->id);
|
||||
$PAGE->set_context($usercontext);
|
||||
|
||||
if ($profilenode = $PAGE->settingsnav->find('myprofile', null)) {
|
||||
$profilenode->make_active();
|
||||
}
|
||||
|
||||
$title = get_string('datarequests', 'tool_dataprivacy');
|
||||
$PAGE->navbar->add($title);
|
||||
|
||||
// Return URL.
|
||||
$params = ['id' => $USER->id];
|
||||
if ($courseid) {
|
||||
@@ -49,7 +56,6 @@ if ($courseid) {
|
||||
}
|
||||
$returnurl = new moodle_url('/user/profile.php', $params);
|
||||
|
||||
$title = get_string('datarequests', 'tool_dataprivacy');
|
||||
$PAGE->set_heading($title);
|
||||
$PAGE->set_title($title);
|
||||
echo $OUTPUT->header();
|
||||
|
||||
@@ -27,12 +27,10 @@ require_once('../../../config.php');
|
||||
$requestid = required_param('requestid', PARAM_INT);
|
||||
$confirm = optional_param('confirm', null, PARAM_INT);
|
||||
|
||||
$PAGE->set_url(new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', ['requestid' => $requestid]));
|
||||
$url = new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', ['requestid' => $requestid]);
|
||||
$title = get_string('resubmitrequestasnew', 'tool_dataprivacy');
|
||||
|
||||
require_login();
|
||||
|
||||
$PAGE->set_context(\context_system::instance());
|
||||
require_capability('tool/dataprivacy:managedatarequests', $PAGE->context);
|
||||
\tool_dataprivacy\page_helper::setup($url, $title, 'datarequests', 'tool/dataprivacy:managedatarequests');
|
||||
|
||||
$manageurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
|
||||
|
||||
@@ -53,10 +51,6 @@ if (null !== $confirm && confirm_sesskey()) {
|
||||
redirect($manageurl, get_string('resubmittedrequest', 'tool_dataprivacy', $stringparams));
|
||||
}
|
||||
|
||||
$heading = get_string('resubmitrequest', 'tool_dataprivacy', $stringparams);
|
||||
$PAGE->set_title($heading);
|
||||
$PAGE->set_heading($heading);
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
$confirmstring = get_string('confirmrequestresubmit', 'tool_dataprivacy', $stringparams);
|
||||
|
||||
@@ -34,6 +34,13 @@ $PAGE->set_context($context);
|
||||
$PAGE->set_title($title);
|
||||
$PAGE->set_heading($SITE->fullname);
|
||||
|
||||
// If user is logged in, then use profile navigation in breadcrumbs.
|
||||
if ($profilenode = $PAGE->settingsnav->find('myprofile', null)) {
|
||||
$profilenode->make_active();
|
||||
}
|
||||
|
||||
$PAGE->navbar->add($title);
|
||||
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->header();
|
||||
$summarypage = new \tool_dataprivacy\output\summary_page();
|
||||
|
||||
@@ -12,7 +12,7 @@ Feature: Manage data categories
|
||||
And I press "Add category"
|
||||
And I set the field "Name" to "Category 1"
|
||||
And I set the field "Description" to "Category 1 description"
|
||||
When I click on "Save" "button" in the "Delete category" "dialogue"
|
||||
When I click on "Save" "button" in the "Add category" "dialogue"
|
||||
Then I should see "Category 1" in the "List of data categories" "table"
|
||||
And I should see "Category 1 description" in the "Category 1" "table_row"
|
||||
|
||||
@@ -30,5 +30,5 @@ Feature: Manage data categories
|
||||
And I choose "Delete" in the open action menu
|
||||
And I should see "Delete category"
|
||||
And I should see "Are you sure you want to delete the category 'Category 1'?"
|
||||
When I click on "Delete" "button" in the "Confirm" "dialogue"
|
||||
When I click on "Delete" "button" in the "Delete category" "dialogue"
|
||||
Then I should not see "Category 1" in the "List of data categories" "table"
|
||||
|
||||
@@ -52,5 +52,5 @@ Feature: Manage data storage purposes
|
||||
And I choose "Delete" in the open action menu
|
||||
And I should see "Delete purpose"
|
||||
And I should see "Are you sure you want to delete the purpose 'Purpose 1'?"
|
||||
When I click on "Delete" "button" in the "Confirm" "dialogue"
|
||||
When I click on "Delete" "button" in the "Delete purpose" "dialogue"
|
||||
Then I should not see "Purpose 1" in the "List of data purposes" "table"
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Tests for the plugin privacy provider
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2020 Paul Holden <paulh@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\tests\provider_testcase;
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\local\helper;
|
||||
use tool_dataprivacy\privacy\provider;
|
||||
|
||||
/**
|
||||
* Privacy provider tests
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2020 Paul Holden <paulh@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_dataprivacy_privacy_provider_testcase extends provider_testcase {
|
||||
|
||||
/**
|
||||
* Test provider get_contexts_for_userid method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_get_contexts_for_userid() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_user::instance($user->id);
|
||||
|
||||
// Returned context list should contain a single item.
|
||||
$contextlist = $this->get_contexts_for_userid($user->id, 'tool_dataprivacy');
|
||||
$this->assertCount(1, $contextlist);
|
||||
|
||||
// We should have the user context of our test user.
|
||||
$this->assertSame($context, $contextlist->current());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test provider get_users_in_context method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_user::instance($user->id);
|
||||
|
||||
$userlist = new userlist($context, 'tool_dataprivacy');
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertEquals([$user->id], $userlist->get_userids());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test provider get_users_in_context method for a non-user context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_get_users_in_context_non_user_context() {
|
||||
$context = context_system::instance();
|
||||
|
||||
$userlist = new userlist($context, 'tool_dataprivacy');
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertEmpty($userlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test provider export_user_data method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_export_user_data() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_user::instance($user->id);
|
||||
|
||||
$this->setUser($user);
|
||||
|
||||
// Create an export request, approve it.
|
||||
$requestexport = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT,
|
||||
'Please export my stuff');
|
||||
api::update_request_status($requestexport->get('id'), api::DATAREQUEST_STATUS_APPROVED);
|
||||
|
||||
// Create a deletion request, reject it.
|
||||
$requestdelete = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
api::update_request_status($requestdelete->get('id'), api::DATAREQUEST_STATUS_REJECTED, 0, 'Nope');
|
||||
|
||||
$this->export_context_data_for_user($user->id, $context, 'tool_dataprivacy');
|
||||
|
||||
/** @var \core_privacy\tests\request\content_writer $writer */
|
||||
$writer = writer::with_context($context);
|
||||
$this->assertTrue($writer->has_any_data());
|
||||
|
||||
/** @var stdClass[] $data */
|
||||
$data = (array) $writer->get_data([
|
||||
get_string('privacyandpolicies', 'admin'),
|
||||
get_string('datarequests', 'tool_dataprivacy'),
|
||||
]);
|
||||
|
||||
$this->assertCount(2, $data);
|
||||
|
||||
$strs = get_strings(['requesttypeexportshort', 'requesttypedeleteshort',
|
||||
'statusapproved', 'statusrejected', 'creationmanual'], 'tool_dataprivacy');
|
||||
|
||||
// First item is the approved export request.
|
||||
$this->assertEquals($strs->requesttypeexportshort, $data[0]->type);
|
||||
$this->assertEquals($strs->statusapproved, $data[0]->status);
|
||||
$this->assertEquals($strs->creationmanual, $data[0]->creationmethod);
|
||||
$this->assertEquals($requestexport->get('comments'), $data[0]->comments);
|
||||
$this->assertEmpty($data[0]->dpocomment);
|
||||
$this->assertNotEmpty($data[0]->timecreated);
|
||||
|
||||
// Next is the rejected deletion request.
|
||||
$this->assertEquals($strs->requesttypedeleteshort, $data[1]->type);
|
||||
$this->assertEquals($strs->statusrejected, $data[1]->status);
|
||||
$this->assertEquals($strs->creationmanual, $data[1]->creationmethod);
|
||||
$this->assertEmpty($data[1]->comments);
|
||||
$this->assertContains('Nope', $data[1]->dpocomment);
|
||||
$this->assertNotEmpty($data[1]->timecreated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test class export_user_preferences method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_export_user_preferences() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user);
|
||||
|
||||
// Set filters preference.
|
||||
$filters = [
|
||||
helper::FILTER_TYPE . ':' . api::DATAREQUEST_TYPE_EXPORT,
|
||||
helper::FILTER_STATUS . ':' . api::DATAREQUEST_STATUS_PENDING,
|
||||
];
|
||||
set_user_preference(helper::PREF_REQUEST_FILTERS, json_encode($filters), $user);
|
||||
|
||||
// Set paging preference.
|
||||
set_user_preference(helper::PREF_REQUEST_PERPAGE, 6, $user);
|
||||
|
||||
provider::export_user_preferences($user->id);
|
||||
|
||||
/** @var \core_privacy\tests\request\content_writer $writer */
|
||||
$writer = writer::with_context(context_system::instance());
|
||||
$this->assertTrue($writer->has_any_data());
|
||||
|
||||
/** @var stdClass[] $preferences */
|
||||
$preferences = (array) $writer->get_user_preferences('tool_dataprivacy');
|
||||
$this->assertCount(2, $preferences);
|
||||
|
||||
$this->assertEquals((object) [
|
||||
'value' => '1:1, 2:0',
|
||||
'description' => 'Type: Export, Status: Pending',
|
||||
], $preferences[helper::PREF_REQUEST_FILTERS]);
|
||||
|
||||
$this->assertEquals(6, $preferences[helper::PREF_REQUEST_PERPAGE]->value);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
$plugin->version = 2019052000;
|
||||
$plugin->version = 2019052001;
|
||||
$plugin->requires = 2019051100; // Moodle 3.5dev (Build 2018031600) and upwards.
|
||||
$plugin->component = 'tool_dataprivacy';
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
define(["core/yui"],function(a){var b=function(b,c,d,e,f){this.yuiDialogue=null;var g=this;"undefined"==typeof f&&(f=!1),a.use("moodle-core-notification","timers",function(){var h="480px";f&&(h="800px"),g.yuiDialogue=new M.core.dialogue({headerContent:b,bodyContent:c,draggable:!0,visible:!1,center:!0,modal:!0,width:h}),g.yuiDialogue.after("visibleChange",function(b){b.newVal?"undefined"!=typeof d&&a.soon(function(){d(g),g.yuiDialogue.centerDialogue()}):"undefined"!=typeof e&&a.soon(function(){e(g)})}),g.yuiDialogue.show()})};return b.prototype.close=function(){this.yuiDialogue.hide(),this.yuiDialogue.destroy()},b.prototype.getContent=function(){return this.yuiDialogue.bodyNode.getDOMNode()},b});
|
||||
define(["core/yui"],function(a){var b=function(b,c,d,e,f){M.util.js_pending("tool_lp/dialogue:dialogue"),this.yuiDialogue=null;var g=this;"undefined"==typeof f&&(f=!1),a.use("moodle-core-notification","timers",function(){var h="480px";f&&(h="800px"),g.yuiDialogue=new M.core.dialogue({headerContent:b,bodyContent:c,draggable:!0,visible:!1,center:!0,modal:!0,width:h}),g.yuiDialogue.before("visibleChange",function(){M.util.js_pending("tool_lp/dialogue:before:visibleChange")}),g.yuiDialogue.after("visibleChange",function(b){b.newVal?"undefined"!=typeof d?a.soon(function(){d(g),g.yuiDialogue.centerDialogue(),M.util.js_complete("tool_lp/dialogue:before:visibleChange")}):M.util.js_complete("tool_lp/dialogue:before:visibleChange"):"undefined"!=typeof e?a.soon(function(){e(g),M.util.js_complete("tool_lp/dialogue:before:visibleChange")}):M.util.js_complete("tool_lp/dialogue:before:visibleChange")}),g.yuiDialogue.show(),M.util.js_complete("tool_lp/dialogue:dialogue")})};return b.prototype.close=function(){this.yuiDialogue.hide(),this.yuiDialogue.destroy()},b.prototype.getContent=function(){return this.yuiDialogue.bodyNode.getDOMNode()},b});
|
||||
+1
-1
@@ -1 +1 @@
|
||||
define(["jquery","core/notification","core/templates","tool_lp/dialogue","tool_lp/event_base","core/str"],function(a,b,c,d,e,f){var g=function(a){e.prototype.constructor.apply(this,[]),this._ratingOptions=a};return g.prototype=Object.create(e.prototype),g.prototype._popup=null,g.prototype._ratingOptions=null,g.prototype._afterRender=function(){var b=this._find('[data-action="rate"]'),c=this._find('[name="rating"]'),d=this._find('[name="comment"]');this._find('[data-action="cancel"]').click(function(a){a.preventDefault(),this._trigger("cancelled"),this.close()}.bind(this)),c.change(function(){var c=a(this);c.val()?b.prop("disabled",!1):b.prop("disabled",!0)}).change(),b.click(function(a){a.preventDefault();var b=c.val();b&&(this._trigger("rated",{rating:b,note:d.val()}),this.close())}.bind(this))},g.prototype.close=function(){this._popup.close(),this._popup=null},g.prototype.display=function(){return a.when(f.get_string("rate","tool_lp"),this._render()).then(function(a,b){return this._popup=new d(a,b[0],this._afterRender.bind(this)),this._popup}.bind(this))["catch"](b.exception)},g.prototype._find=function(b){return a(this._popup.getContent()).find(b)},g.prototype._render=function(){var a={cangrade:this._canGrade,ratings:this._ratingOptions};return c.render("tool_lp/competency_grader",a)},g});
|
||||
define(["jquery","core/notification","core/templates","tool_lp/dialogue","tool_lp/event_base","core/str"],function(a,b,c,d,e,f){var g=function(a){e.prototype.constructor.apply(this,[]),this._ratingOptions=a};return g.prototype=Object.create(e.prototype),g.prototype._popup=null,g.prototype._ratingOptions=null,g.prototype._afterRender=function(){var b=this._find('[data-action="rate"]'),c=this._find('[name="rating"]'),d=this._find('[name="comment"]');this._find('[data-action="cancel"]').click(function(a){a.preventDefault(),this._trigger("cancelled"),this.close()}.bind(this)),c.change(function(){var c=a(this);c.val()?b.prop("disabled",!1):b.prop("disabled",!0)}).change(),b.click(function(a){a.preventDefault();var b=c.val();b&&(this._trigger("rated",{rating:b,note:d.val()}),this.close())}.bind(this))},g.prototype.close=function(){this._popup.close(),this._popup=null},g.prototype.display=function(){return M.util.js_pending("tool_lp/grade_dialogue:display"),a.when(f.get_string("rate","tool_lp"),this._render()).then(function(a,b){return this._popup=new d(a,b[0],function(){this._afterRender(),M.util.js_complete("tool_lp/grade_dialogue:display")}.bind(this)),this._popup}.bind(this))["catch"](b.exception)},g.prototype._find=function(b){return a(this._popup.getContent()).find(b)},g.prototype._render=function(){var a={cangrade:this._canGrade,ratings:this._ratingOptions};return c.render("tool_lp/competency_grader",a)},g});
|
||||
@@ -1 +1 @@
|
||||
define(["jquery","core/notification","core/ajax","core/log","tool_lp/grade_dialogue","tool_lp/event_base","tool_lp/scalevalues"],function(a,b,c,d,e,f,g){var h=function(b,c,d,e,g,h,i){f.prototype.constructor.apply(this,[]);var j=a(b);if(!j.length)throw new Error("Could not find the trigger");this._scaleId=c,this._competencyId=d,this._userId=e,this._planId=g,this._courseId=h,this._chooseStr=i,this._setUp(),j.click(function(a){a.preventDefault(),this._dialogue.display()}.bind(this)),this._planId?(this._methodName="core_competency_grade_competency_in_plan",this._args={competencyid:this._competencyId,planid:this._planId}):this._courseId?(this._methodName="core_competency_grade_competency_in_course",this._args={competencyid:this._competencyId,courseid:this._courseId,userid:this._userId}):(this._methodName="core_competency_grade_competency",this._args={userid:this._userId,competencyid:this._competencyId})};return h.prototype=Object.create(f.prototype),h.prototype._setUp=function(){var a=[],d=this,f=g.get_values(d._scaleId);f.then(function(b){a.push({value:"",name:d._chooseStr});for(var c=0;c<b.length;c++){var e=b[c];a.push({value:e.id,name:e.name})}return a}).then(function(a){return new e(a)}).then(function(a){return a.on("rated",function(a,e){var f=d._args;f.grade=e.rating,f.note=e.note,c.call([{methodname:d._methodName,args:f,done:function(a){d._trigger("competencyupdated",{args:f,evidence:a})},fail:b.exception}])}),a}).then(function(a){d._dialogue=a}).fail(b.exception)},h.prototype._scaleId=null,h.prototype._competencyId=null,h.prototype._userId=null,h.prototype._planId=null,h.prototype._courseId=null,h.prototype._chooseStr=null,h.prototype._dialogue=null,h});
|
||||
define(["jquery","core/notification","core/ajax","core/log","tool_lp/grade_dialogue","tool_lp/event_base","tool_lp/scalevalues"],function(a,b,c,d,e,f,g){var h=function(b,c,d,e,g,h,i){f.prototype.constructor.apply(this,[]);var j=a(b);if(!j.length)throw new Error("Could not find the trigger");this._scaleId=c,this._competencyId=d,this._userId=e,this._planId=g,this._courseId=h,this._chooseStr=i,this._setUp(),j.click(function(a){a.preventDefault(),this._dialogue.display()}.bind(this)),this._planId?(this._methodName="core_competency_grade_competency_in_plan",this._args={competencyid:this._competencyId,planid:this._planId}):this._courseId?(this._methodName="core_competency_grade_competency_in_course",this._args={competencyid:this._competencyId,courseid:this._courseId,userid:this._userId}):(this._methodName="core_competency_grade_competency",this._args={userid:this._userId,competencyid:this._competencyId})};return h.prototype=Object.create(f.prototype),h.prototype._setUp=function(){var a=[],d=this;M.util.js_pending("tool_lp/grade_user_competency_inline:_setUp");var f=g.get_values(d._scaleId);f.then(function(b){a.push({value:"",name:d._chooseStr});for(var c=0;c<b.length;c++){var e=b[c];a.push({value:e.id,name:e.name})}return a}).then(function(a){return new e(a)}).then(function(a){return a.on("rated",function(a,e){var f=d._args;f.grade=e.rating,f.note=e.note,c.call([{methodname:d._methodName,args:f,done:function(a){d._trigger("competencyupdated",{args:f,evidence:a})},fail:b.exception}])}),a}).then(function(a){d._dialogue=a,M.util.js_complete("tool_lp/grade_user_competency_inline:_setUp")}).fail(b.exception)},h.prototype._scaleId=null,h.prototype._competencyId=null,h.prototype._userId=null,h.prototype._planId=null,h.prototype._courseId=null,h.prototype._chooseStr=null,h.prototype._dialogue=null,h});
|
||||
@@ -35,6 +35,8 @@ define(['core/yui'], function(Y) {
|
||||
* @param {Boolean} wide Specify we want an extra wide dialogue (the size is standard, but wider than the default).
|
||||
*/
|
||||
var dialogue = function(title, content, afterShow, afterHide, wide) {
|
||||
M.util.js_pending('tool_lp/dialogue:dialogue');
|
||||
|
||||
this.yuiDialogue = null;
|
||||
var parent = this;
|
||||
|
||||
@@ -59,6 +61,10 @@ define(['core/yui'], function(Y) {
|
||||
width: width
|
||||
});
|
||||
|
||||
parent.yuiDialogue.before('visibleChange', function() {
|
||||
M.util.js_pending('tool_lp/dialogue:before:visibleChange');
|
||||
});
|
||||
|
||||
parent.yuiDialogue.after('visibleChange', function(e) {
|
||||
if (e.newVal) {
|
||||
// Delay the callback call to the next tick, otherwise it can happen that it is
|
||||
@@ -67,18 +73,25 @@ define(['core/yui'], function(Y) {
|
||||
Y.soon(function() {
|
||||
afterShow(parent);
|
||||
parent.yuiDialogue.centerDialogue();
|
||||
M.util.js_complete('tool_lp/dialogue:before:visibleChange');
|
||||
});
|
||||
} else {
|
||||
M.util.js_complete('tool_lp/dialogue:before:visibleChange');
|
||||
}
|
||||
} else {
|
||||
if ((typeof afterHide !== 'undefined')) {
|
||||
Y.soon(function() {
|
||||
afterHide(parent);
|
||||
M.util.js_complete('tool_lp/dialogue:before:visibleChange');
|
||||
});
|
||||
} else {
|
||||
M.util.js_complete('tool_lp/dialogue:before:visibleChange');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
parent.yuiDialogue.show();
|
||||
M.util.js_complete('tool_lp/dialogue:dialogue');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ define(['jquery',
|
||||
* @return {Promise}
|
||||
*/
|
||||
Grade.prototype.display = function() {
|
||||
M.util.js_pending('tool_lp/grade_dialogue:display');
|
||||
return $.when(
|
||||
Str.get_string('rate', 'tool_lp'),
|
||||
this._render()
|
||||
@@ -110,7 +111,10 @@ define(['jquery',
|
||||
this._popup = new Dialogue(
|
||||
title,
|
||||
templateResult[0],
|
||||
this._afterRender.bind(this)
|
||||
function() {
|
||||
this._afterRender();
|
||||
M.util.js_complete('tool_lp/grade_dialogue:display');
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
return this._popup;
|
||||
|
||||
@@ -94,6 +94,7 @@ define(['jquery',
|
||||
var options = [],
|
||||
self = this;
|
||||
|
||||
M.util.js_pending('tool_lp/grade_user_competency_inline:_setUp');
|
||||
var promise = ScaleValues.get_values(self._scaleId);
|
||||
promise.then(function(scalevalues) {
|
||||
options.push({
|
||||
@@ -134,6 +135,7 @@ define(['jquery',
|
||||
.then(function(dialogue) {
|
||||
self._dialogue = dialogue;
|
||||
|
||||
M.util.js_complete('tool_lp/grade_user_competency_inline:_setUp');
|
||||
return;
|
||||
})
|
||||
.fail(notification.exception);
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
$string['classname'] = 'Class name';
|
||||
$string['component'] = 'Component';
|
||||
$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:587. If a port isn\'t specified, the default port for the type of mail server will be used.';
|
||||
$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:993. If a port isn\'t specified, the default port for the type of mail server will be used.';
|
||||
$string['defaultexpiration'] = 'Default address expiry period';
|
||||
$string['defaultexpiration_help'] = 'When an email address is generated by the handler, it can be set to automatically expire after a period of time, so that it can no longer be used. It is advisable to set an expiry period.';
|
||||
$string['description'] = 'Description';
|
||||
|
||||
@@ -49,6 +49,8 @@ class api {
|
||||
const LOGIN_VIA_EMBEDDED_BROWSER = 3;
|
||||
/** @var int seconds an auto-login key will expire. */
|
||||
const LOGIN_KEY_TTL = 60;
|
||||
/** @var string URL of the Moodle Apps Portal */
|
||||
const MOODLE_APPS_PORTAL_URL = 'https://apps.moodle.com';
|
||||
|
||||
/**
|
||||
* Returns a list of Moodle plugins supporting the mobile app.
|
||||
@@ -346,6 +348,7 @@ class api {
|
||||
*/
|
||||
public static function get_features_list() {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/authlib.php');
|
||||
|
||||
$general = new lang_string('general');
|
||||
$mainmenu = new lang_string('mainmenu', 'tool_mobile');
|
||||
@@ -355,6 +358,7 @@ class api {
|
||||
$user = new lang_string('user');
|
||||
$files = new lang_string('files');
|
||||
$remoteaddons = new lang_string('remoteaddons', 'tool_mobile');
|
||||
$identityproviders = new lang_string('oauth2identityproviders', 'tool_mobile');
|
||||
|
||||
$availablemods = core_plugin_manager::instance()->get_plugins_of_type('mod');
|
||||
$coursemodules = array();
|
||||
@@ -421,6 +425,7 @@ class api {
|
||||
'NoDelegate_CoreTag' => new lang_string('tags'),
|
||||
'$mmLoginEmailSignup' => new lang_string('startsignup'),
|
||||
'NoDelegate_ResponsiveMainMenuItems' => new lang_string('responsivemainmenuitems', 'tool_mobile'),
|
||||
'NoDelegate_DarkMode' => new lang_string('darkmode', 'tool_mobile'),
|
||||
),
|
||||
"$mainmenu" => array(
|
||||
'$mmSideMenuDelegate_mmaFrontpage' => new lang_string('sitehome'),
|
||||
@@ -473,6 +478,31 @@ class api {
|
||||
$features["$remoteaddons"] = $remoteaddonslist;
|
||||
}
|
||||
|
||||
// Display OAuth 2 identity providers.
|
||||
if (is_enabled_auth('oauth2')) {
|
||||
$identityproviderslist = array();
|
||||
$idps = \auth_plugin_base::get_identity_providers(['oauth2']);
|
||||
|
||||
foreach ($idps as $idp) {
|
||||
// Only add identity providers that have an ID.
|
||||
$id = isset($idp['url']) ? $idp['url']->get_param('id') : null;
|
||||
if ($id != null) {
|
||||
$identityproviderslist['NoDelegate_IdentityProvider_' . $id] = $idp['name'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($identityproviderslist)) {
|
||||
$features["$identityproviders"] = array();
|
||||
|
||||
if (count($identityproviderslist) > 1) {
|
||||
// Include an option to disable them all.
|
||||
$features["$identityproviders"]['NoDelegate_IdentityProviders'] = new lang_string('all');
|
||||
}
|
||||
|
||||
$features["$identityproviders"] = array_merge($features["$identityproviders"], $identityproviderslist);
|
||||
}
|
||||
}
|
||||
|
||||
return $features;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,12 +45,16 @@ $string['custommenuitems_desc'] = 'Additional items can be added to the app\'s m
|
||||
|
||||
Link-opening methods are: app (for linking to an activity supported by the app), inappbrowser (for opening a link in a browser without leaving the app), browser (for opening the link in the device default browser outside the app) and embedded (for displaying the link in an iframe in a new page in the app).
|
||||
|
||||
When items are missing a translation for a given language, they will use other languages as fallback unless "_only" is appended to the language code.
|
||||
|
||||
For example:
|
||||
<pre>
|
||||
App help|https://someurl.xyz/help|inappbrowser
|
||||
My grades|https://someurl.xyz/local/mygrades/index.php|embedded|en
|
||||
Mis calificaciones|https://someurl.xyz/local/mygrades/index.php|embedded|es
|
||||
You will only see this in English|https://someurl.xyz/english|browser|en_only
|
||||
</pre>';
|
||||
$string['darkmode'] = 'Dark mode';
|
||||
$string['disabledfeatures'] = 'Disabled features';
|
||||
$string['disabledfeatures_desc'] = 'Select here the features you want to disable in the Mobile app for your site. Please note that some features listed here could be already disabled via other site settings. You will have to log out and log in again in the app to see the changes.';
|
||||
$string['displayerrorswarning'] = 'Display debug messages (debugdisplay) is enabled. It should be disabled.';
|
||||
@@ -84,6 +88,8 @@ $string['mobilecssurl'] = 'CSS';
|
||||
$string['mobilefeatures'] = 'Mobile features';
|
||||
$string['mobilenotificationsdisabledwarning'] = 'Mobile notifications are not enabled. They should be enabled in Notification settings.';
|
||||
$string['mobilesettings'] = 'Mobile settings';
|
||||
$string['moodleappsportalfeatureswarning'] = 'Please note that some features may be restricted depending on your Moodle app subscription. For details, visit the <a href="{$a}" target="_blank">Moodle Apps Portal</a>.';
|
||||
$string['oauth2identityproviders'] = 'OAuth 2 identity providers';
|
||||
$string['offlineuse'] = 'Offline use';
|
||||
$string['pluginname'] = 'Moodle app tools';
|
||||
$string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
|
||||
|
||||
@@ -30,10 +30,16 @@ require_once($CFG->libdir . '/externallib.php');
|
||||
|
||||
$serviceshortname = required_param('service', PARAM_ALPHANUMEXT);
|
||||
$passport = required_param('passport', PARAM_RAW); // Passport send from the app to validate the response URL.
|
||||
$urlscheme = optional_param('urlscheme', 'moodlemobile', PARAM_ALPHANUM); // The URL scheme the app supports.
|
||||
$urlscheme = optional_param('urlscheme', 'moodlemobile', PARAM_NOTAGS); // The URL scheme the app supports.
|
||||
$confirmed = optional_param('confirmed', false, PARAM_BOOL); // If we are being redirected after user confirmation.
|
||||
$oauthsso = optional_param('oauthsso', 0, PARAM_INT); // Id of the OpenID issuer (for OAuth direct SSO).
|
||||
|
||||
// Validate that the urlscheme is valid.
|
||||
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9-\+\.]*$/', $urlscheme)) {
|
||||
throw new moodle_exception('Invalid parameter: the value of urlscheme isn\'t valid. ' .
|
||||
'It should start with a letter and can only contain letters, numbers and the characters "." "+" "-".');
|
||||
}
|
||||
|
||||
// Check web services enabled.
|
||||
if (!$CFG->enablewebservices) {
|
||||
throw new moodle_exception('enablewsdescription', 'webservice');
|
||||
|
||||
@@ -50,6 +50,12 @@ if ($hassiteconfig) {
|
||||
|
||||
// Show only mobile settings if the mobile service is enabled.
|
||||
if (!empty($CFG->enablemobilewebservice)) {
|
||||
// General notification about limited features due to app restrictions.
|
||||
$notify = new \core\output\notification(
|
||||
get_string('moodleappsportalfeatureswarning', 'tool_mobile', tool_mobile\api::MOODLE_APPS_PORTAL_URL),
|
||||
\core\output\notification::NOTIFY_WARNING);
|
||||
$featuresnotice = $OUTPUT->render($notify);
|
||||
|
||||
// Type of login.
|
||||
$temp = new admin_settingpage('mobileauthentication', new lang_string('mobileauthentication', 'tool_mobile'));
|
||||
$options = array(
|
||||
@@ -63,13 +69,15 @@ if ($hassiteconfig) {
|
||||
|
||||
$temp->add(new admin_setting_configtext('tool_mobile/forcedurlscheme',
|
||||
new lang_string('forcedurlscheme_key', 'tool_mobile'),
|
||||
new lang_string('forcedurlscheme', 'tool_mobile'), '', PARAM_ALPHANUM));
|
||||
new lang_string('forcedurlscheme', 'tool_mobile'), '', PARAM_NOTAGS));
|
||||
|
||||
$ADMIN->add('mobileapp', $temp);
|
||||
|
||||
// Appearance related settings.
|
||||
$temp = new admin_settingpage('mobileappearance', new lang_string('mobileappearance', 'tool_mobile'));
|
||||
|
||||
$temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesappearance', '', $featuresnotice));
|
||||
|
||||
$temp->add(new admin_setting_configtext('mobilecssurl', new lang_string('mobilecssurl', 'tool_mobile'),
|
||||
new lang_string('configmobilecssurl', 'tool_mobile'), '', PARAM_URL));
|
||||
|
||||
@@ -102,6 +110,8 @@ if ($hassiteconfig) {
|
||||
// Features related settings.
|
||||
$temp = new admin_settingpage('mobilefeatures', new lang_string('mobilefeatures', 'tool_mobile'));
|
||||
|
||||
$temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeatures', '', $featuresnotice));
|
||||
|
||||
$temp->add(new admin_setting_heading('tool_mobile/logout',
|
||||
new lang_string('logout'), ''));
|
||||
|
||||
|
||||
@@ -131,11 +131,12 @@ if ($execute) {
|
||||
tool_phpunit_problem('Can not create configuration file');
|
||||
}
|
||||
}
|
||||
$configdir = escapeshellarg($configdir);
|
||||
// no cleanup of path - this is tricky because we can not use escapeshellarg and friends for escaping,
|
||||
// this is from admin user so PARAM_PATH must be enough
|
||||
chdir($CFG->dirroot);
|
||||
passthru("php $CFG->admin/tool/phpunit/cli/util.php --run -c $configdir $testclass $testpath", $code);
|
||||
$configdir = escapeshellarg($configdir);
|
||||
$cleanclass = escapeshellarg($testclass);
|
||||
$cleanpath = escapeshellarg($testpath);
|
||||
|
||||
passthru("php $CFG->admin/tool/phpunit/cli/util.php --run -c $configdir $cleanclass $cleanpath", $code);
|
||||
chdir($oldcwd);
|
||||
|
||||
echo '</pre>';
|
||||
@@ -146,7 +147,7 @@ if ($execute) {
|
||||
}
|
||||
|
||||
echo $OUTPUT->box_start('generalbox boxwidthwide boxaligncenter');
|
||||
echo '<form method="get" action="webrunner.php">';
|
||||
echo '<form method="post" action="webrunner.php">';
|
||||
echo '<fieldset class="invisiblefieldset">';
|
||||
echo '<label for="testpath">Test one file</label> ';
|
||||
echo '<input type="text" id="testpath" name="testpath" value="'.s($testpath).'" size="50" /> (all test cases from webrunner.xml if empty)';
|
||||
|
||||
@@ -39,15 +39,29 @@ if ($userid != $USER->id) {
|
||||
if (!has_capability('tool/policy:acceptbehalf', $context)) {
|
||||
require_capability('tool/policy:viewacceptances', $context);
|
||||
}
|
||||
|
||||
$user = core_user::get_user($userid);
|
||||
$PAGE->navigation->extend_for_user($user);
|
||||
}
|
||||
|
||||
$title = get_string('policiesagreements', 'tool_policy');
|
||||
|
||||
$PAGE->set_context($context);
|
||||
$PAGE->set_pagelayout('standard');
|
||||
$PAGE->set_url(new moodle_url('/admin/tool/policy/user.php', ['userid' => $userid]));
|
||||
$PAGE->set_title($title);
|
||||
|
||||
if ($userid == $USER->id &&
|
||||
($profilenode = $PAGE->settingsnav->find('myprofile', null))) {
|
||||
|
||||
$profilenode->make_active();
|
||||
}
|
||||
|
||||
$PAGE->navbar->add($title);
|
||||
|
||||
$output = $PAGE->get_renderer('tool_policy');
|
||||
echo $output->header();
|
||||
echo $output->heading(get_string('policiesagreements', 'tool_policy'));
|
||||
echo $output->heading($title);
|
||||
$acceptances = new \tool_policy\output\acceptances($userid, $returnurl);
|
||||
echo $output->render($acceptances);
|
||||
$PAGE->requires->js_call_amd('tool_policy/acceptmodal', 'getInstance', [context_system::instance()->id]);
|
||||
|
||||
@@ -127,3 +127,36 @@ Feature: Basic recycle bin functionality
|
||||
And I press "Yes"
|
||||
And I should see "Recycle bin has been emptied"
|
||||
And I should see "There are no items in the recycle bin."
|
||||
|
||||
@javascript
|
||||
Scenario: Show recycle bin on category action menu
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Courses > Manage courses and categories" in site administration
|
||||
And I click on "Actions menu" "link"
|
||||
And I click on "Recycle bin" "link"
|
||||
Then I should see "There are no items in the recycle bin."
|
||||
|
||||
@javascript
|
||||
Scenario: Not show recycle bin empty on category action menu whit autohide enable
|
||||
Given I log in as "admin"
|
||||
And the following config values are set as admin:
|
||||
| categorybinenable | 0 | tool_recyclebin |
|
||||
And I navigate to "Courses > Manage courses and categories" in site administration
|
||||
And I click on "Actions menu" "link"
|
||||
Then I should not see "Recycle bin"
|
||||
|
||||
@javascript
|
||||
Scenario: Show recycle bin not empty on category action menu whit autohide enable
|
||||
Given I log in as "admin"
|
||||
And the following config values are set as admin:
|
||||
| autohide | 1 | tool_recyclebin |
|
||||
And I navigate to "Courses > Manage courses and categories" in site administration
|
||||
And I click on "Actions menu" "link"
|
||||
Then I should not see "Recycle bin"
|
||||
And I click on "delete" action for "Course 2" in management course listing
|
||||
And I press "Delete"
|
||||
And I should see "Deleting C2"
|
||||
And I should see "C2 has been completely deleted"
|
||||
And I press "Continue"
|
||||
When I click on "Actions menu" "link"
|
||||
Then I should see "Recycle bin"
|
||||
|
||||
@@ -100,9 +100,9 @@ if (!$problems) {
|
||||
$count = $problem->racount;
|
||||
$edit = array();
|
||||
$aurl = new moodle_url('/admin/roles/define.php', array('roleid'=>$problem->roleid, 'action'=>'edit'));
|
||||
$edit[] = html_writer::link($aurl, $OUTPUT->pix_icon('t/edit', 'core', get_string('edit')));
|
||||
$edit[] = html_writer::link($aurl, $OUTPUT->pix_icon('t/edit', get_string('edit')));
|
||||
$aurl = new moodle_url($PAGE->url, array('roleid'=>$problem->roleid, 'contextlevel'=>$problem->contextlevel, 'action'=>'delete'));
|
||||
$edit[] = html_writer::link($aurl, $OUTPUT->pix_icon('t/delete', 'core', get_string('delete')));
|
||||
$edit[] = html_writer::link($aurl, $OUTPUT->pix_icon('t/delete', get_string('delete')));
|
||||
$data[] = array($levelname, $rolename, $count, implode(' ', $edit));
|
||||
}
|
||||
$table = new html_table();
|
||||
|
||||
@@ -412,6 +412,12 @@ class tool_uploadcourse_course {
|
||||
$this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure we don't overflow the maximum length of the shortname field.
|
||||
if (core_text::strlen($this->shortname) > 255) {
|
||||
$this->error('invalidshortnametoolong', new lang_string('invalidshortnametoolong', 'tool_uploadcourse', 255));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$exists = $this->exists();
|
||||
@@ -479,6 +485,12 @@ class tool_uploadcourse_course {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure we don't overflow the maximum length of the fullname field.
|
||||
if (!empty($coursedata['fullname']) && core_text::strlen($coursedata['fullname']) > 254) {
|
||||
$this->error('invalidfullnametoolong', new lang_string('invalidfullnametoolong', 'tool_uploadcourse', 254));
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the course does not exist, or will be forced created.
|
||||
if (!$exists || $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) {
|
||||
|
||||
|
||||
@@ -93,6 +93,8 @@ $string['invalideupdatemode'] = 'Invalid update mode selected';
|
||||
$string['invalidvisibilitymode'] = 'Invalid visible mode';
|
||||
$string['invalidroles'] = 'Invalid role names: {$a}';
|
||||
$string['invalidshortname'] = 'Invalid shortname';
|
||||
$string['invalidfullnametoolong'] = 'The fullname field is limited to {$a} characters';
|
||||
$string['invalidshortnametoolong'] = 'The shortname field is limited to {$a} characters';
|
||||
$string['missingmandatoryfields'] = 'Missing value for mandatory fields: {$a}';
|
||||
$string['missingshortnamenotemplate'] = 'Missing shortname and shortname template not set';
|
||||
$string['mode'] = 'Upload mode';
|
||||
|
||||
@@ -82,6 +82,37 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
|
||||
$this->assertArrayHasKey('invalidshortname', $co->get_errors());
|
||||
}
|
||||
|
||||
public function test_invalid_shortname_too_long() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
|
||||
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
|
||||
|
||||
$upload = new tool_uploadcourse_course($mode, $updatemode, [
|
||||
'category' => 1,
|
||||
'fullname' => 'New course',
|
||||
'shortname' => str_repeat('X', 2000),
|
||||
]);
|
||||
|
||||
$this->assertFalse($upload->prepare());
|
||||
$this->assertArrayHasKey('invalidshortnametoolong', $upload->get_errors());
|
||||
}
|
||||
|
||||
public function test_invalid_fullname_too_long() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
|
||||
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
|
||||
|
||||
$upload = new tool_uploadcourse_course($mode, $updatemode, [
|
||||
'category' => 1,
|
||||
'fullname' => str_repeat('X', 2000),
|
||||
]);
|
||||
|
||||
$this->assertFalse($upload->prepare());
|
||||
$this->assertArrayHasKey('invalidfullnametoolong', $upload->get_errors());
|
||||
}
|
||||
|
||||
public function test_invalid_visibility() {
|
||||
$this->resetAfterTest(true);
|
||||
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
|
||||
|
||||
@@ -720,6 +720,13 @@ class manager {
|
||||
* @param int $direction
|
||||
*/
|
||||
protected static function _move_tour(tour $tour, $direction) {
|
||||
// We can't move the first tour higher, nor the last tour any lower.
|
||||
if (($tour->is_first_tour() && $direction == helper::MOVE_UP) ||
|
||||
($tour->is_last_tour() && $direction == helper::MOVE_DOWN)) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$currentsortorder = $tour->get_sortorder();
|
||||
$targetsortorder = $currentsortorder + $direction;
|
||||
|
||||
@@ -848,6 +855,9 @@ class manager {
|
||||
}
|
||||
$existingtourrecords->close();
|
||||
|
||||
// Ensure we correct the sortorder in any existing tours, prior to adding latest shipped tours.
|
||||
helper::reset_tour_sortorder();
|
||||
|
||||
foreach (array_reverse($shippedtours) as $filename => $version) {
|
||||
$filepath = $CFG->dirroot . "/{$CFG->admin}/tool/usertours/tours/" . $filename;
|
||||
$tourjson = file_get_contents($filepath);
|
||||
|
||||
@@ -64,5 +64,12 @@ function xmldb_tool_usertours_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v3.7.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2019052001) {
|
||||
// Updating shipped tours will fix broken sortorder records in existing tours.
|
||||
manager::update_shipped_tours();
|
||||
|
||||
upgrade_plugin_savepoint(true, 2019052001, 'tool', 'usertours');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -122,6 +122,83 @@ class tool_usertours_manager_testcase extends advanced_testcase {
|
||||
$rcm->invokeArgs($manager, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_move_tour
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function move_tour_provider() {
|
||||
$alltours = [
|
||||
['name' => 'Tour 1'],
|
||||
['name' => 'Tour 2'],
|
||||
['name' => 'Tour 3'],
|
||||
];
|
||||
|
||||
return [
|
||||
'Move up' => [
|
||||
$alltours,
|
||||
'Tour 2',
|
||||
\tool_usertours\helper::MOVE_UP,
|
||||
0,
|
||||
],
|
||||
'Move down' => [
|
||||
$alltours,
|
||||
'Tour 2',
|
||||
\tool_usertours\helper::MOVE_DOWN,
|
||||
2,
|
||||
],
|
||||
'Move up (first)' => [
|
||||
$alltours,
|
||||
'Tour 1',
|
||||
\tool_usertours\helper::MOVE_UP,
|
||||
0,
|
||||
],
|
||||
'Move down (last)' => [
|
||||
$alltours,
|
||||
'Tour 3',
|
||||
\tool_usertours\helper::MOVE_DOWN,
|
||||
2,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test moving tours (changing sortorder)
|
||||
*
|
||||
* @dataProvider move_tour_provider
|
||||
*
|
||||
* @param array $alltours
|
||||
* @param string $movetourname
|
||||
* @param int $direction
|
||||
* @param int $expectedsortorder
|
||||
* @return void
|
||||
*/
|
||||
public function test_move_tour($alltours, $movetourname, $direction, $expectedsortorder) {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Clear out existing tours so ours are the only ones, otherwise we can't predict the sortorder.
|
||||
$DB->delete_records('tool_usertours_tours');
|
||||
|
||||
foreach ($alltours as $tourconfig) {
|
||||
$this->helper_create_tour((object) $tourconfig);
|
||||
}
|
||||
|
||||
// Load our tour to move.
|
||||
$record = $DB->get_record('tool_usertours_tours', ['name' => $movetourname]);
|
||||
$tour = \tool_usertours\tour::load_from_record($record);
|
||||
|
||||
// Call protected method via reflection.
|
||||
$class = new ReflectionClass(\tool_usertours\manager::class);
|
||||
$method = $class->getMethod('_move_tour');
|
||||
$method->setAccessible(true);
|
||||
$method->invokeArgs(null, [$tour, $direction]);
|
||||
|
||||
// Assert expected sortorder.
|
||||
$this->assertEquals($expectedsortorder, $tour->get_sortorder());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data Provider for get_matching_tours tests.
|
||||
*
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2019052000; // The current module version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2019052001; // The current module version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2019051100; // Requires this Moodle version.
|
||||
$plugin->component = 'tool_usertours'; // Full name of the plugin (used for diagnostics).
|
||||
|
||||
@@ -68,13 +68,13 @@ class view_xml extends XMLDBAction {
|
||||
|
||||
// Get the file parameter
|
||||
$file = required_param('file', PARAM_PATH);
|
||||
$file = $CFG->dirroot . $file;
|
||||
// File must be under $CFG->wwwroot and
|
||||
// under one db directory (simple protection)
|
||||
if (substr($file, 0, strlen($CFG->dirroot)) == $CFG->dirroot &&
|
||||
substr(dirname($file), -2, 2) == 'db') {
|
||||
|
||||
$fullpath = $CFG->dirroot . $file;
|
||||
// File param must start with / and end with /db/install.xml to be safe.
|
||||
if (substr($file, 0, 1) == '/' &&
|
||||
substr($file, -15, 15) == '/db/install.xml') {
|
||||
// Everything is ok. Load the file to memory
|
||||
$this->output = file_get_contents($file);
|
||||
$this->output = file_get_contents($fullpath);
|
||||
} else {
|
||||
// Switch to HTML and error
|
||||
$this->does_generate = ACTION_GENERATE_HTML;
|
||||
|
||||
@@ -149,7 +149,7 @@ class auth_email_external extends external_api {
|
||||
new external_single_structure(
|
||||
array(
|
||||
'id' => new external_value(PARAM_INT, 'Profile field id', VALUE_OPTIONAL),
|
||||
'shortname' => new external_value(PARAM_ALPHANUM, 'Password policy', VALUE_OPTIONAL),
|
||||
'shortname' => new external_value(PARAM_ALPHANUMEXT, 'Profile field shortname', VALUE_OPTIONAL),
|
||||
'name' => new external_value(PARAM_TEXT, 'Profield field name', VALUE_OPTIONAL),
|
||||
'datatype' => new external_value(PARAM_ALPHANUMEXT, 'Profield field datatype', VALUE_OPTIONAL),
|
||||
'description' => new external_value(PARAM_RAW, 'Profield field description', VALUE_OPTIONAL),
|
||||
|
||||
@@ -263,7 +263,8 @@ class auth_plugin_shibboleth extends auth_plugin_base {
|
||||
global $OUTPUT;
|
||||
|
||||
if (!isset($this->config->user_attribute) || empty($this->config->user_attribute)) {
|
||||
echo $OUTPUT->notification(get_string("shib_not_set_up_error", "auth_shibboleth"), 'notifyproblem');
|
||||
echo $OUTPUT->notification(get_string("shib_not_set_up_error", "auth_shibboleth",
|
||||
(new moodle_url('/auth/shibboleth/README.txt'))->out()), 'notifyproblem');
|
||||
return;
|
||||
}
|
||||
if ($this->config->convert_data and $this->config->convert_data != '' and !is_readable($this->config->convert_data)) {
|
||||
|
||||
@@ -32,8 +32,9 @@
|
||||
$shibbolethauth = get_auth_plugin('shibboleth');
|
||||
|
||||
// Check whether Shibboleth is configured properly
|
||||
$readmeurl = (new moodle_url('/auth/shibboleth/README.txt'))->out();
|
||||
if (empty($pluginconfig->user_attribute)) {
|
||||
print_error('shib_not_set_up_error', 'auth_shibboleth');
|
||||
print_error('shib_not_set_up_error', 'auth_shibboleth', '', $readmeurl);
|
||||
}
|
||||
|
||||
/// If we can find the Shibboleth attribute, save it in session and return to main login page
|
||||
@@ -91,7 +92,7 @@
|
||||
elseif (!empty($_SERVER['HTTP_SHIB_APPLICATION_ID']) || !empty($_SERVER['Shib-Application-ID'])) {
|
||||
print_error('shib_no_attributes_error', 'auth_shibboleth' , '', '\''.$pluginconfig->user_attribute.'\', \''.$pluginconfig->field_map_firstname.'\', \''.$pluginconfig->field_map_lastname.'\' and \''.$pluginconfig->field_map_email.'\'');
|
||||
} else {
|
||||
print_error('shib_not_set_up_error', 'auth_shibboleth');
|
||||
print_error('shib_not_set_up_error', 'auth_shibboleth', '', $readmeurl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ $string['auth_shib_auth_method_description'] = 'Provide a name for the Shibbolet
|
||||
$string['auth_shib_auth_logo'] = 'Authentication method logo';
|
||||
$string['auth_shib_auth_logo_description'] = 'Provide a logo for the Shibboleth authentication method that is familiar to your users. This could be the logo of your Shibboleth federation, e.g. <tt>SWITCHaai Login</tt> or <tt>InCommon Login</tt> or similar.';
|
||||
$string['auth_shib_contact_administrator'] = 'In case you are not associated with the given organizations and you need access to a course on this server, please contact the <a href="mailto:{$a}">Moodle Administrator</a>.';
|
||||
$string['auth_shibbolethdescription'] = 'Using this method users are created and authenticated using Shibboleth. For set-up details, see the <a href="../auth/shibboleth/README.txt">Shibboleth README</a>.';
|
||||
$string['auth_shibbolethdescription'] = 'Using this method users are created and authenticated using Shibboleth. For set-up details, see the <a href="{$a}">Shibboleth README</a>.';
|
||||
$string['auth_shibboleth_errormsg'] = 'Please select the organization you are member of!';
|
||||
$string['auth_shibboleth_login'] = 'Shibboleth login';
|
||||
$string['auth_shibboleth_login_long'] = 'Login to Moodle via Shibboleth';
|
||||
@@ -36,7 +36,7 @@ $string['auth_shibboleth_manual_login'] = 'Manual login';
|
||||
$string['auth_shibboleth_select_member'] = 'I\'m a member of ...';
|
||||
$string['auth_shibboleth_select_organization'] = 'For authentication via Shibboleth, please select your organisation from the drop-down menu:';
|
||||
$string['auth_shib_convert_data'] = 'Data modification API';
|
||||
$string['auth_shib_convert_data_description'] = 'You can use this API to further modify the data provided by Shibboleth. Read the <a href="../auth/shibboleth/README.txt">README</a> for further instructions.';
|
||||
$string['auth_shib_convert_data_description'] = 'You can use this API to further modify the data provided by Shibboleth. Read the <a href="{$a}">README</a> for further instructions.';
|
||||
$string['auth_shib_convert_data_warning'] = 'The file does not exist or is not readable by the webserver process!';
|
||||
$string['auth_shib_changepasswordurl'] = 'Password-change URL';
|
||||
$string['auth_shib_idp_list'] = 'Identity providers';
|
||||
@@ -57,6 +57,6 @@ $string['auth_shib_username_description'] = 'Name of the webserver Shibboleth en
|
||||
$string['shib_invalid_account_error'] = 'You seem to be Shibboleth authenticated but Moodle has no valid account for your username. Your account may not exist or it may have been suspended.';
|
||||
$string['shib_no_attributes_error'] = 'You seem to be Shibboleth authenticated but Moodle didn\'t receive any user attributes. Please check that your Identity Provider releases the necessary attributes ({$a}) to the Service Provider Moodle is running on or inform the webmaster of this server.';
|
||||
$string['shib_not_all_attributes_error'] = 'Moodle needs certain Shibboleth attributes which are not present in your case. The attributes are: {$a}<br />Please contact the webmaster of this server or your Identity Provider.';
|
||||
$string['shib_not_set_up_error'] = 'Shibboleth authentication doesn\'t seem to be set up correctly because no Shibboleth environment variables are present for this page. Please consult the <a href="README.txt">README</a> for further instructions on how to set up Shibboleth authentication or contact the webmaster of this Moodle installation.';
|
||||
$string['shib_not_set_up_error'] = 'Shibboleth authentication doesn\'t seem to be set up correctly because no Shibboleth environment variables are present for this page. Please consult the <a href="{$a}">README</a> for further instructions on how to set up Shibboleth authentication or contact the webmaster of this Moodle installation.';
|
||||
$string['pluginname'] = 'Shibboleth';
|
||||
$string['privacy:metadata'] = 'The Shibboleth authentication plugin does not store any personal data.';
|
||||
|
||||
@@ -30,8 +30,9 @@ if ($ADMIN->fulltree) {
|
||||
require_once($CFG->dirroot.'/auth/shibboleth/classes/admin_setting_special_idp_configtextarea.php');
|
||||
|
||||
// Introductory explanation.
|
||||
$readmeurl = (new moodle_url('/auth/shibboleth/README.txt'))->out();
|
||||
$settings->add(new admin_setting_heading('auth_shibboleth/pluginname', '',
|
||||
new lang_string('auth_shibbolethdescription', 'auth_shibboleth')));
|
||||
new lang_string('auth_shibbolethdescription', 'auth_shibboleth', $readmeurl)));
|
||||
|
||||
// Username.
|
||||
$settings->add(new admin_setting_configtext('auth_shibboleth/user_attribute', get_string('username'),
|
||||
@@ -40,7 +41,7 @@ if ($ADMIN->fulltree) {
|
||||
// COnvert Data configuration file.
|
||||
$settings->add(new admin_setting_configfile('auth_shibboleth/convert_data',
|
||||
get_string('auth_shib_convert_data', 'auth_shibboleth'),
|
||||
get_string('auth_shib_convert_data_description', 'auth_shibboleth'), ''));
|
||||
get_string('auth_shib_convert_data_description', 'auth_shibboleth', $readmeurl), ''));
|
||||
|
||||
// WAYF.
|
||||
$settings->add(new auth_shibboleth_admin_setting_special_wayf_select());
|
||||
|
||||
@@ -91,7 +91,6 @@ class core_backup_external extends external_api {
|
||||
require_capability('moodle/backup:backupactivity', $context);
|
||||
} else {
|
||||
require_capability('moodle/backup:backupcourse', $context);
|
||||
$instanceid = $course->id;
|
||||
}
|
||||
|
||||
$results = array();
|
||||
|
||||
@@ -955,6 +955,52 @@ class core_backup_moodle2_testcase extends advanced_testcase {
|
||||
$this->assertEquals('', $requests[2]->searcharea);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test restoring courses based on the backup plan. Primarily used with
|
||||
* the import functionality
|
||||
*/
|
||||
public function test_restore_course_using_plan_defaults() {
|
||||
global $DB, $CFG, $USER;
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
$this->setAdminUser();
|
||||
$CFG->enableglobalsearch = true;
|
||||
|
||||
// Set admin config setting so that activities are not restored by default.
|
||||
set_config('restore_general_activities', 0, 'restore');
|
||||
|
||||
// Create a course.
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$course2 = $generator->create_course();
|
||||
$course3 = $generator->create_course();
|
||||
|
||||
// Add a forum.
|
||||
$forum = $generator->create_module('forum', ['course' => $course->id]);
|
||||
|
||||
// Backup course...
|
||||
$CFG->backup_file_logger_level = backup::LOG_NONE;
|
||||
$bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
|
||||
backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
|
||||
$USER->id);
|
||||
$backupid = $bc->get_backupid();
|
||||
$bc->execute_plan();
|
||||
$bc->destroy();
|
||||
|
||||
// Restore it on top of course2 (should duplicate the forum).
|
||||
$rc = new restore_controller($backupid, $course2->id,
|
||||
backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id,
|
||||
backup::TARGET_EXISTING_ADDING, null);
|
||||
$this->assertTrue($rc->execute_precheck());
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
// Get the forums now on the old course.
|
||||
$modinfo = get_fast_modinfo($course2->id);
|
||||
$forums = $modinfo->get_instances_of('forum');
|
||||
$this->assertCount(0, $forums);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Question category hierarchical structure was changed in Moodle 3.5.
|
||||
* From 3.5, all question categories in each context are a child of a single top level question category for that context.
|
||||
|
||||
+2
-2
@@ -55,9 +55,11 @@ require_capability('moodle/restore:restorecourse', $context);
|
||||
if (is_null($course)) {
|
||||
$coursefullname = $SITE->fullname;
|
||||
$courseshortname = $SITE->shortname;
|
||||
$courseurl = new moodle_url('/');
|
||||
} else {
|
||||
$coursefullname = $course->fullname;
|
||||
$courseshortname = $course->shortname;
|
||||
$courseurl = course_get_url($course->id);
|
||||
}
|
||||
|
||||
// Show page header.
|
||||
@@ -173,7 +175,6 @@ if ($restore->get_stage() != restore_ui::STAGE_PROCESS) {
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// Add ajax progress bar and initiate ajax via a template.
|
||||
$courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
|
||||
$restoreurl = new moodle_url('/backup/restorefile.php', array('contextid' => $contextid));
|
||||
$progresssetup = array(
|
||||
'backupid' => $restoreid,
|
||||
@@ -182,7 +183,6 @@ if ($restore->get_stage() != restore_ui::STAGE_PROCESS) {
|
||||
'restoreurl' => $restoreurl->out()
|
||||
);
|
||||
echo $renderer->render_from_template('core/async_backup_status', $progresssetup);
|
||||
|
||||
}
|
||||
|
||||
$restore->destroy();
|
||||
|
||||
@@ -282,7 +282,14 @@ abstract class restore_controller_dbops extends restore_dbops {
|
||||
if ($plan->setting_exists($settingname)) {
|
||||
$setting = $plan->get_setting($settingname);
|
||||
$value = self::get_setting_default($config, $setting);
|
||||
$locked = (get_config('restore', $config . '_locked') == true);
|
||||
$locked = (get_config('restore',$config . '_locked') == true);
|
||||
|
||||
// Use the original value when this is an import and the setting is unlocked.
|
||||
if ($controller->get_mode() == backup::MODE_IMPORT && $controller->get_interactive()) {
|
||||
if (!$uselocks || !$locked) {
|
||||
$value = $setting->get_value();
|
||||
}
|
||||
}
|
||||
|
||||
// We can only update the setting if it isn't already locked by config or permission.
|
||||
if ($setting->get_status() != base_setting::LOCKED_BY_CONFIG
|
||||
|
||||
@@ -1371,7 +1371,9 @@ abstract class restore_dbops {
|
||||
// Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
|
||||
// hence we are looking there for usernames if not empty. See delete_user()
|
||||
// If match by id and mnethost and user is deleted in DB and
|
||||
// match by username LIKE 'backup_email.%' or by non empty email = md5(username) => ok, return target user
|
||||
// match by username LIKE 'substring(backup_email).%' where the substr length matches the retained data in the
|
||||
// username field (100 - (timestamp + 1) characters), or by non empty email = md5(username) => ok, return target user.
|
||||
$usernamelookup = core_text::substr($user->email, 0, 89) . '.%';
|
||||
if ($rec = $DB->get_record_sql("SELECT *
|
||||
FROM {user} u
|
||||
WHERE id = ?
|
||||
@@ -1384,13 +1386,14 @@ abstract class restore_dbops {
|
||||
AND email = ?
|
||||
)
|
||||
)",
|
||||
array($user->id, $user->mnethostid, $user->email.'.%', md5($user->username)))) {
|
||||
array($user->id, $user->mnethostid, $usernamelookup, md5($user->username)))) {
|
||||
return $rec; // Matching user, deleted in DB found, return it
|
||||
}
|
||||
|
||||
// 1D - Handle users deleted in backup file and "alive" in DB
|
||||
// If match by id and mnethost and user is deleted in backup file
|
||||
// and match by email = email_without_time(backup_email) => ok, return target user
|
||||
// and match by substring(email) = email_without_time(backup_email) where the substr length matches the retained data
|
||||
// in the username field (100 - (timestamp + 1) characters) => ok, return target user.
|
||||
if ($user->deleted) {
|
||||
// Note: for DB deleted users email is stored in username field, hence we
|
||||
// are looking there for emails. See delete_user()
|
||||
@@ -1400,7 +1403,7 @@ abstract class restore_dbops {
|
||||
FROM {user} u
|
||||
WHERE id = ?
|
||||
AND mnethostid = ?
|
||||
AND UPPER(email) = UPPER(?)",
|
||||
AND " . $DB->sql_substr('UPPER(email)', 1, 89) . " = UPPER(?)",
|
||||
array($user->id, $user->mnethostid, $trimemail))) {
|
||||
return $rec; // Matching user, deleted in backup file found, return it
|
||||
}
|
||||
@@ -1447,7 +1450,8 @@ abstract class restore_dbops {
|
||||
// Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
|
||||
// hence we are looking there for usernames if not empty. See delete_user()
|
||||
// 2B1 - If match by mnethost and user is deleted in DB and not empty email = md5(username) and
|
||||
// (by username LIKE 'backup_email.%' or non-zero firstaccess) => ok, return target user
|
||||
// (by username LIKE 'substring(backup_email).%' or non-zero firstaccess) => ok, return target user.
|
||||
$usernamelookup = core_text::substr($user->email, 0, 89) . '.%';
|
||||
if ($rec = $DB->get_record_sql("SELECT *
|
||||
FROM {user} u
|
||||
WHERE mnethostid = ?
|
||||
@@ -1461,14 +1465,15 @@ abstract class restore_dbops {
|
||||
AND firstaccess = ?
|
||||
)
|
||||
)",
|
||||
array($user->mnethostid, md5($user->username), $user->email.'.%', $user->firstaccess))) {
|
||||
array($user->mnethostid, md5($user->username), $usernamelookup, $user->firstaccess))) {
|
||||
return $rec; // Matching user found, return it
|
||||
}
|
||||
|
||||
// 2B2 - If match by mnethost and user is deleted in DB and
|
||||
// username LIKE 'backup_email.%' and non-zero firstaccess) => ok, return target user
|
||||
// username LIKE 'substring(backup_email).%' and non-zero firstaccess) => ok, return target user
|
||||
// (this covers situations where md5(username) wasn't being stored so we require both
|
||||
// the email & non-zero firstaccess to match)
|
||||
$usernamelookup = core_text::substr($user->email, 0, 89) . '.%';
|
||||
if ($rec = $DB->get_record_sql("SELECT *
|
||||
FROM {user} u
|
||||
WHERE mnethostid = ?
|
||||
@@ -1476,13 +1481,13 @@ abstract class restore_dbops {
|
||||
AND UPPER(username) LIKE UPPER(?)
|
||||
AND firstaccess != 0
|
||||
AND firstaccess = ?",
|
||||
array($user->mnethostid, $user->email.'.%', $user->firstaccess))) {
|
||||
array($user->mnethostid, $usernamelookup, $user->firstaccess))) {
|
||||
return $rec; // Matching user found, return it
|
||||
}
|
||||
|
||||
// 2C - Handle users deleted in backup file and "alive" in DB
|
||||
// If match mnethost and user is deleted in backup file
|
||||
// and match by email = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user
|
||||
// and match by substring(email) = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user.
|
||||
if ($user->deleted) {
|
||||
// Note: for DB deleted users email is stored in username field, hence we
|
||||
// are looking there for emails. See delete_user()
|
||||
@@ -1491,7 +1496,7 @@ abstract class restore_dbops {
|
||||
if ($rec = $DB->get_record_sql("SELECT *
|
||||
FROM {user} u
|
||||
WHERE mnethostid = ?
|
||||
AND UPPER(email) = UPPER(?)
|
||||
AND " . $DB->sql_substr('UPPER(email)', 1, 89) . " = UPPER(?)
|
||||
AND firstaccess != 0
|
||||
AND firstaccess = ?",
|
||||
array($user->mnethostid, $trimemail, $user->firstaccess))) {
|
||||
|
||||
@@ -119,4 +119,246 @@ class restore_dbops_testcase extends advanced_testcase {
|
||||
$this->assertSame('Table "backup_ids_temp" does not exist', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for {@link test_precheck_user()}
|
||||
*/
|
||||
public function precheck_user_provider() {
|
||||
|
||||
$emailmultiplier = [
|
||||
'shortmail' => 'normalusername@example.com',
|
||||
'longmail' => str_repeat('a', 100) // It's not validated, hence any string is ok.
|
||||
];
|
||||
|
||||
$providercases = [];
|
||||
|
||||
foreach ($emailmultiplier as $emailk => $email) {
|
||||
// Get the related cases.
|
||||
$cases = $this->precheck_user_cases($email);
|
||||
// Rename them (keys).
|
||||
foreach ($cases as $key => $case) {
|
||||
$providercases[$key . ' - ' . $emailk] = $case;
|
||||
}
|
||||
}
|
||||
|
||||
return $providercases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the cases implemented in {@link restore_dbops::precheck_users()}
|
||||
*
|
||||
* @param string $email
|
||||
*/
|
||||
private function precheck_user_cases($email) {
|
||||
global $CFG;
|
||||
|
||||
$baseuserarr = [
|
||||
'username' => 'normalusername',
|
||||
'email' => $email,
|
||||
'mnethostid' => $CFG->mnet_localhost_id,
|
||||
'firstaccess' => 123456789,
|
||||
'deleted' => 0,
|
||||
'forceemailcleanup' => false, // Hack to force the DB record to have empty mail.
|
||||
'forceduplicateadminallowed' => false]; // Hack to enable import_general_duplicate_admin_allowed.
|
||||
|
||||
return [
|
||||
// Cases with samesite = true.
|
||||
'samesite match existing (1A)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => $baseuserarr,
|
||||
'samesite' => true,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'samesite match existing anon (1B)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'username' => 'anon01']),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'id' => -1, 'username' => 'anon01', 'firstname' => 'anonfirstname01',
|
||||
'lastname' => 'anonlastname01', 'email' => 'anon01@doesntexist.invalid']),
|
||||
'samesite' => true,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'samesite match existing deleted in db, alive in backup, by db username (1C)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1]),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'username' => 'this_wont_match']),
|
||||
'samesite' => true,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'samesite match existing deleted in db, alive in backup, by db email (1C)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1]),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'email' => 'this_wont_match']),
|
||||
'samesite' => true,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'samesite match existing alive in db, deleted in backup (1D)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1]),
|
||||
'samesite' => true,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'samesite conflict (1E)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, ['id' => -1]),
|
||||
'samesite' => true,
|
||||
'outcome' => false
|
||||
],
|
||||
'samesite create user (1F)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'username' => 'newusername']),
|
||||
'samesite' => false,
|
||||
'outcome' => true
|
||||
],
|
||||
|
||||
// Cases with samesite = false.
|
||||
'no samesite match existing, by db email (2A1)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'firstaccess' => 0]),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match existing, by db firstaccess (2A1)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'email' => 'this_wont_match@example.con']),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match existing anon (2A1 too)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'username' => 'anon01']),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'id' => -1, 'username' => 'anon01', 'firstname' => 'anonfirstname01',
|
||||
'lastname' => 'anonlastname01', 'email' => 'anon01@doesntexist.invalid']),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match dupe admin (2A2)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'username' => 'admin_old_site_id',
|
||||
'forceduplicateadminallowed' => true]),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'username' => 'admin']),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match existing deleted in db, alive in backup, by db username (2B1)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1]),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'firstaccess' => 0]),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match existing deleted in db, alive in backup, by db firstaccess (2B1)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1]),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'mail' => 'this_wont_match']),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match existing deleted in db, alive in backup (2B2)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1,
|
||||
'forceemailcleanup' => true]),
|
||||
'backupuser' => $baseuserarr,
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match existing alive in db, deleted in backup (2C)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1]),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite conflict (2D)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'email' => 'anotheruser@example.com', 'firstaccess' => 0]),
|
||||
'samesite' => false,
|
||||
'outcome' => false
|
||||
],
|
||||
'no samesite create user (2E)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'username' => 'newusername']),
|
||||
'samesite' => false,
|
||||
'outcome' => true
|
||||
],
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test restore precheck_user method
|
||||
*
|
||||
* @dataProvider precheck_user_provider
|
||||
* @covers restore_dbops::precheck_user()
|
||||
*
|
||||
* @param array $dbuser
|
||||
* @param array $backupuser
|
||||
* @param bool $samesite
|
||||
* @param mixed $outcome
|
||||
**/
|
||||
public function test_precheck_user($dbuser, $backupuser, $samesite, $outcome) {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$dbuser = (object)$dbuser;
|
||||
$backupuser = (object)$backupuser;
|
||||
|
||||
$siteid = null;
|
||||
|
||||
// If the backup user must be deleted, simulate it (by temp inserting to DB, deleting and fetching it back).
|
||||
if ($backupuser->deleted) {
|
||||
$backupuser->id = $DB->insert_record('user', array_merge((array)$backupuser, ['deleted' => 0]));
|
||||
delete_user($backupuser);
|
||||
$backupuser = $DB->get_record('user', ['id' => $backupuser->id]);
|
||||
$DB->delete_records('user', ['id' => $backupuser->id]);
|
||||
unset($backupuser->id);
|
||||
}
|
||||
|
||||
// Create the db user, normally.
|
||||
$dbuser->id = $DB->insert_record('user', array_merge((array)$dbuser, ['deleted' => 0]));
|
||||
$backupuser->id = $backupuser->id ?? $dbuser->id;
|
||||
|
||||
// We may want to enable the import_general_duplicate_admin_allowed setting and look for old admin records.
|
||||
if ($dbuser->forceduplicateadminallowed) {
|
||||
set_config('import_general_duplicate_admin_allowed', true, 'backup');
|
||||
$siteid = 'old_site_id';
|
||||
}
|
||||
|
||||
// If the DB user must be deleted, do it and fetch it back.
|
||||
if ($dbuser->deleted) {
|
||||
delete_user($dbuser);
|
||||
// We may want to clean the mail field (old behavior, not containing the current md5(username).
|
||||
if ($dbuser->forceemailcleanup) {
|
||||
$DB->set_field('user', 'email', '', ['id' => $dbuser->id]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the dbuser record, because we may have changed it above.
|
||||
$dbuser = $DB->get_record('user', ['id' => $dbuser->id]);
|
||||
|
||||
$method = (new ReflectionClass('restore_dbops'))->getMethod('precheck_user');
|
||||
$method->setAccessible(true);
|
||||
$result = $method->invoke(null, $backupuser, $samesite, $siteid);
|
||||
|
||||
if (is_bool($result)) {
|
||||
$this->assertSame($outcome, $result);
|
||||
} else {
|
||||
$outcome = $dbuser; // Outcome is not bool, matching found, so it must be the dbuser,
|
||||
// Just check ids, it means the expected match has been found in database.
|
||||
$this->assertSame($outcome->id, $result->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,10 +305,17 @@ class async_helper {
|
||||
$tabledata = array();
|
||||
|
||||
// Get relevant backup ids based on context instance id.
|
||||
$select = 'itemid = ? AND execution = ? AND status < ? AND status > ?';
|
||||
$params = array($instanceid, backup::EXECUTION_DELAYED, backup::STATUS_FINISHED_ERR, backup::STATUS_NEED_PRECHECK);
|
||||
$backups = $DB->get_records_select('backup_controllers', $select, $params, 'timecreated DESC', 'id, backupid, timecreated');
|
||||
$select = 'itemid = :itemid AND execution = :execution AND status < :status1 AND status > :status2 ' .
|
||||
'AND operation = :operation';
|
||||
$params = [
|
||||
'itemid' => $instanceid,
|
||||
'execution' => backup::EXECUTION_DELAYED,
|
||||
'status1' => backup::STATUS_FINISHED_ERR,
|
||||
'status2' => backup::STATUS_NEED_PRECHECK,
|
||||
'operation' => 'backup',
|
||||
];
|
||||
|
||||
$backups = $DB->get_records_select('backup_controllers', $select, $params, 'timecreated DESC', 'id, backupid, timecreated');
|
||||
foreach ($backups as $backup) {
|
||||
$bc = \backup_controller::load_controller($backup->backupid); // Get the backup controller.
|
||||
$filename = $bc->get_plan()->get_setting('filename')->get_value();
|
||||
|
||||
@@ -67,6 +67,14 @@ if ($action == 'edit') {
|
||||
echo $output->heading(get_string('managebackpacks', 'badges'));
|
||||
|
||||
$form->display();
|
||||
} else if ($action == 'test') {
|
||||
// If no backpack has been selected, there isn't anything to test.
|
||||
if (empty($id)) {
|
||||
redirect($url);
|
||||
}
|
||||
|
||||
echo $OUTPUT->header();
|
||||
echo $output->render_test_backpack_result($id);
|
||||
} else {
|
||||
echo $OUTPUT->header();
|
||||
echo $output->heading(get_string('managebackpacks', 'badges'));
|
||||
|
||||
@@ -199,7 +199,7 @@ class core_badges_assertion {
|
||||
$class['criteria'] = $this->_url->out(false); // Currently issued badge URL.
|
||||
if ($issued) {
|
||||
if ($this->_obversion == OPEN_BADGES_V2) {
|
||||
$issuerurl = new moodle_url('/badges/badge_json.php', array('id' => $this->get_badge_id(), 'action' => 0));
|
||||
$issuerurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->get_badge_id()));
|
||||
} else {
|
||||
$issuerurl = new moodle_url('/badges/assertion.php', array('b' => $this->_data->uniquehash, 'action' => 0));
|
||||
}
|
||||
@@ -233,7 +233,8 @@ class core_badges_assertion {
|
||||
$issuer['email'] = $CFG->badges_defaultissuercontact;
|
||||
}
|
||||
} else {
|
||||
$issuer = badges_get_default_issuer();
|
||||
$badge = new badge($this->get_badge_id());
|
||||
$issuer = $badge->get_badge_issuer();
|
||||
}
|
||||
}
|
||||
$this->embed_data_badge_version2($issuer, OPEN_BADGES_V2_TYPE_ISSUER);
|
||||
@@ -341,8 +342,7 @@ class core_badges_assertion {
|
||||
'/badges/badge_json.php',
|
||||
array('id' => $this->get_badge_id())
|
||||
);
|
||||
$issuerurl = new moodle_url('/badges/badge_json.php', array('id' => $this->get_badge_id(), 'action' => 0,
|
||||
'obversion' => $this->_obversion));
|
||||
$issuerurl = new moodle_url('/badges/issuer_json.php', ['id' => $this->get_badge_id()]);
|
||||
// For assertion.
|
||||
if ($type == OPEN_BADGES_V2_TYPE_ASSERTION) {
|
||||
$json['@context'] = OPEN_BADGES_V2_CONTEXT;
|
||||
|
||||
@@ -929,12 +929,12 @@ class badge {
|
||||
*/
|
||||
public function get_badge_issuer() {
|
||||
$issuer = array();
|
||||
$issuerurl = new moodle_url('/badges/badge_json.php', array('id' => $this->id, 'action' => 0));
|
||||
$issuer['name'] = $this->issuername;
|
||||
$issuer['url'] = $this->issuerurl;
|
||||
$issuer['email'] = $this->issuercontact;
|
||||
$issuer['@context'] = OPEN_BADGES_V2_CONTEXT;
|
||||
$issuer['id'] = $this->issuerurl;
|
||||
$issueridurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->id));
|
||||
$issuer['id'] = $issueridurl->out(false);
|
||||
$issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
|
||||
return $issuer;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ class badge extends moodleform {
|
||||
$mform = $this->_form;
|
||||
$badge = (isset($this->_customdata['badge'])) ? $this->_customdata['badge'] : false;
|
||||
$action = $this->_customdata['action'];
|
||||
$languages = get_string_manager()->get_list_of_languages();
|
||||
|
||||
$mform->addElement('header', 'badgedetails', get_string('badgedetails', 'badges'));
|
||||
$mform->addElement('text', 'name', get_string('name'), array('size' => '70'));
|
||||
@@ -61,6 +60,8 @@ class badge extends moodleform {
|
||||
$mform->addElement('text', 'version', get_string('version', 'badges'), array('size' => '70'));
|
||||
$mform->setType('version', PARAM_TEXT);
|
||||
$mform->addHelpButton('version', 'version', 'badges');
|
||||
|
||||
$languages = get_string_manager()->get_list_of_languages();
|
||||
$mform->addElement('select', 'language', get_string('language'), $languages);
|
||||
$mform->addHelpButton('language', 'language', 'badges');
|
||||
|
||||
@@ -157,7 +158,16 @@ class badge extends moodleform {
|
||||
$mform->setType('action', PARAM_TEXT);
|
||||
|
||||
if ($action == 'new') {
|
||||
$mform->setDefault('language', $CFG->lang);
|
||||
// Try to set default badge language to that of current language, or it's parent.
|
||||
$language = current_language();
|
||||
if (isset($languages[$language])) {
|
||||
$defaultlanguage = $language;
|
||||
} else {
|
||||
// Calling get_parent_language returns an empty string instead of 'en'.
|
||||
$defaultlanguage = get_parent_language($language) ?: 'en';
|
||||
}
|
||||
|
||||
$mform->setDefault('language', $defaultlanguage);
|
||||
$this->add_action_buttons(true, get_string('createbutton', 'badges'));
|
||||
} else {
|
||||
// Add hidden fields.
|
||||
|
||||
@@ -68,9 +68,9 @@ class external_backpacks_page implements \renderable {
|
||||
} else {
|
||||
$backpack->canedit = false;
|
||||
}
|
||||
$backpack->cantest = ($backpack->apiversion == OPEN_BADGES_V2);
|
||||
$data->backpacks[] = $backpack;
|
||||
}
|
||||
$data->warning = badges_verify_site_backpack();
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Serve Issuer JSON for related badge or default Issuer if no badge is defined.
|
||||
*
|
||||
* @package core
|
||||
* @subpackage badges
|
||||
* @copyright 2020 Sara Arjona <sara@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define('AJAX_SCRIPT', true);
|
||||
define('NO_MOODLE_COOKIES', true); // No need for a session here.
|
||||
|
||||
require_once(__DIR__ . '/../config.php');
|
||||
require_once($CFG->libdir . '/badgeslib.php');
|
||||
|
||||
|
||||
$id = optional_param('id', null, PARAM_INT);
|
||||
|
||||
if (empty($id)) {
|
||||
// Get the default issuer for this site.
|
||||
$json = badges_get_default_issuer();
|
||||
} else {
|
||||
// Get the issuer for this badge.
|
||||
$badge = new badge($id);
|
||||
if ($badge->status != BADGE_STATUS_INACTIVE) {
|
||||
$json = $badge->get_badge_issuer();
|
||||
} else {
|
||||
// The badge doen't exist or not accessible for the users.
|
||||
header("HTTP/1.0 410 Gone");
|
||||
$badgeurl = new moodle_url('/badges/issuer_json.php', array('id' => $id));
|
||||
$json = ['id' => $badgeurl->out()];
|
||||
$json['error'] = get_string('error:relatedbadgedoesntexist', 'badges');
|
||||
}
|
||||
}
|
||||
|
||||
echo $OUTPUT->header();
|
||||
echo json_encode($json);
|
||||
+41
-5
@@ -114,23 +114,59 @@ class PNG_MetaDataHandler
|
||||
* @param string $value Currently an assertion URL that is added to an image metadata.
|
||||
*
|
||||
* @return string $result File content with a new chunk as a string. Can be used in file_put_contents() to write to a file.
|
||||
* @throws \moodle_exception when unsupported chunk type is defined.
|
||||
*/
|
||||
public function add_chunks($type, $key, $value) {
|
||||
if (strlen($key) > 79) {
|
||||
debugging('Key is too big');
|
||||
}
|
||||
|
||||
// tEXt Textual data.
|
||||
// Keyword: 1-79 bytes (character string)
|
||||
// Null separator: 1 byte
|
||||
// Text: n bytes (character string)
|
||||
$data = $key . "\0" . $value;
|
||||
$dataparts = [];
|
||||
if ($type === 'iTXt') {
|
||||
// International textual data (iTXt).
|
||||
// Keyword: 1-79 bytes (character string).
|
||||
$dataparts[] = $key;
|
||||
// Null separator: 1 byte.
|
||||
$dataparts[] = "\x00";
|
||||
// Compression flag: 1 byte
|
||||
// A value of 0 means no compression.
|
||||
$dataparts[] = "\x00";
|
||||
// Compression method: 1 byte
|
||||
// If compression is disabled, the method should also be 0.
|
||||
$dataparts[] = "\x00";
|
||||
// Language tag: 0 or more bytes (character string)
|
||||
// When there is no language specified leave empty.
|
||||
|
||||
// Null separator: 1 byte.
|
||||
$dataparts[] = "\x00";
|
||||
// Translated keyword: 0 or more bytes
|
||||
// When there is no translation specified, leave empty.
|
||||
|
||||
// Null separator: 1 byte.
|
||||
$dataparts[] = "\x00";
|
||||
// Text: 0 or more bytes.
|
||||
$dataparts[] = $value;
|
||||
} else if ($type === 'tEXt') {
|
||||
// Textual data (tEXt).
|
||||
// Keyword: 1-79 bytes (character string).
|
||||
$dataparts[] = $key;
|
||||
// Null separator: 1 byte.
|
||||
$dataparts[] = "\0";
|
||||
// Text: n bytes (character string).
|
||||
$dataparts[] = $value;
|
||||
} else {
|
||||
throw new \moodle_exception('Unsupported chunk type: ' . $type);
|
||||
}
|
||||
|
||||
$data = implode($dataparts);
|
||||
|
||||
$crc = pack("N", crc32($type . $data));
|
||||
$len = pack("N", strlen($data));
|
||||
|
||||
// Chunk format: length + type + data + CRC.
|
||||
// CRC is a CRC-32 computed over the chunk type and chunk data.
|
||||
$newchunk = $len . $type . $data . $crc;
|
||||
$this->_chunks[$type] = $data;
|
||||
|
||||
$result = substr($this->_contents, 0, $this->_size - 12)
|
||||
. $newchunk
|
||||
|
||||
@@ -1362,4 +1362,35 @@ class core_badges_renderer extends plugin_renderer_base {
|
||||
$data = $page->export_for_template($this);
|
||||
return parent::render_from_template('core_badges/external_backpacks_page', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the result of a backpack validation with its settings. It returns:
|
||||
* - A informative message if the backpack version is different from OBv2.
|
||||
* - A warning with the error if it's not possible to connect to this backpack.
|
||||
* - A successful message if the connection has worked.
|
||||
*
|
||||
* @param int $backpackid The backpack identifier.
|
||||
* @return string A message with the validation result.
|
||||
*/
|
||||
public function render_test_backpack_result(int $backpackid): string {
|
||||
// Get the backpack.
|
||||
$backpack = badges_get_site_backpack($backpackid);
|
||||
|
||||
// Add the header to the result.
|
||||
$result = $this->heading(get_string('testbackpack', 'badges', $backpack->backpackweburl));
|
||||
|
||||
if ($backpack->apiversion != OPEN_BADGES_V2) {
|
||||
// Only OBv2 supports this validation.
|
||||
$result .= get_string('backpackconnectionnottested', 'badges');
|
||||
} else {
|
||||
$message = badges_verify_backpack($backpackid);
|
||||
if (empty($message)) {
|
||||
$result .= get_string('backpackconnectionok', 'badges');
|
||||
} else {
|
||||
$result .= $message;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,8 @@
|
||||
Example context (json):
|
||||
{
|
||||
"backpacks": [
|
||||
{"backpackweburl": "http://localhost/", "sitebackpack": true, "canedit": false}
|
||||
],
|
||||
"warning": "<span class='text-warning'>Could not login</span>"
|
||||
{"backpackweburl": "http://localhost/", "sitebackpack": true, "canedit": false, "cantest": true}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<table class="generaltable fullwidth">
|
||||
@@ -46,13 +45,15 @@
|
||||
<td> {{#sitebackpack}}Yes{{/sitebackpack}} </td>
|
||||
<td>
|
||||
{{#canedit}}
|
||||
<a href="{{baseurl}}?id={{id}}&action=edit">
|
||||
{{#str}}editsettings, core_badges{{/str}}
|
||||
</a>
|
||||
<a href="{{baseurl}}?id={{id}}&action=edit">{{#pix}}t/edit, core,{{#str}}editsettings{{/str}}{{/pix}}</a>
|
||||
{{/canedit}}
|
||||
{{#cantest}}
|
||||
<a href="{{baseurl}}?id={{id}}&action=test">
|
||||
{{#pix}}t/check, core,{{#str}}testsettings, core_badges{{/str}}{{/pix}}
|
||||
</a>
|
||||
{{/cantest}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/backpacks}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{{warning}}}
|
||||
|
||||
@@ -52,8 +52,8 @@ class block_activity_modules extends block_list {
|
||||
$archetypes = array();
|
||||
|
||||
foreach($modinfo->cms as $cm) {
|
||||
// Exclude activities which are not visible or have no link (=label)
|
||||
if (!$cm->uservisible or !$cm->has_view()) {
|
||||
// Exclude activities that aren't visible or have no view link (e.g. label). Account for folder being displayed inline.
|
||||
if (!$cm->uservisible || (!$cm->has_view() && strcmp($cm->modname, 'folder') !== 0)) {
|
||||
continue;
|
||||
}
|
||||
if (array_key_exists($cm->modname, $modfullnames)) {
|
||||
|
||||
@@ -221,6 +221,11 @@ class block_base {
|
||||
public function get_content_for_output($output) {
|
||||
global $CFG;
|
||||
|
||||
// We can exit early if the current user doesn't have the capability to view the block.
|
||||
if (!has_capability('moodle/block:view', $this->context)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bc = new block_contents($this->html_attributes());
|
||||
$bc->attributes['data-block'] = $this->name();
|
||||
$bc->blockinstanceid = $this->instance->id;
|
||||
|
||||
+8
-5
@@ -37,11 +37,14 @@ $string['privacy:metadata'] = 'The File cache cachestore plugin stores data brie
|
||||
$string['prescan'] = 'Prescan directory';
|
||||
$string['prescan_help'] = 'If enabled the directory is scanned when the cache is first used and requests for files are first checked against the scan data. This can help if you have a slow file system and are finding that file operations are causing you a bottle neck.';
|
||||
$string['singledirectory'] = 'Single directory store';
|
||||
$string['singledirectory_help'] = 'If enabled files (cached items) will be stored in a single directory rather than being broken up into multiple directories.<br />
|
||||
Enabling this will speed up file interactions but comes at the cost of increased risk of hitting file system limitations.<br />
|
||||
It is advisable to only turn this on if the following is true:<br />
|
||||
- If you know the number of items in the cache is going to be small enough that it won\'t cause issues on the file system you are running with.<br />
|
||||
- The data being cached is not expensive to generate. If it is then sticking with the default may still be the better option as it reduces the chance of issues.';
|
||||
$string['singledirectory_help'] = 'If enabled files (cached items) will be stored in a single directory rather than being broken up into multiple directories.
|
||||
|
||||
Enabling this will speed up file interactions but comes at the cost of increased risk of hitting file system limitations.
|
||||
|
||||
It is advisable to only turn this on if the following is true:
|
||||
|
||||
* If you know the number of items in the cache is going to be small enough that it won\'t cause issues on the file system you are running with.
|
||||
* The data being cached is not expensive to generate. If it is then sticking with the default may still be the better option as it reduces the chance of issues.';
|
||||
|
||||
/**
|
||||
* This is is like the file store, but designed for siutations where:
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
define(["jquery","core_calendar/selectors","core_calendar/events","core/str","core/templates"],function(a,b,c,d,e){var f=function(d){d.on("click",b.eventFilterItem,function(b){var c=a(b.currentTarget);g(c),b.preventDefault()}),a("body").on(c.viewUpdated,function(){var c=d.find(b.eventFilterItem);c.each(function(b,c){if(c=a(c),c.data("eventtype-hidden")){var d=i(c);h(d)}})})},g=function(a){var b=i(a);return b.hidden=!b.hidden,d.get_string("eventtype"+b.eventtype,"calendar").then(function(a){return b.name=a,b.icon=!0,b.key="i/"+b.eventtype+"event",b.component="core",b}).then(function(a){return e.render("core_calendar/event_filter_key",a)}).then(function(b,c){return e.replaceNode(a,b,c)}).then(function(){h(b)})},h=function(b){M.util.js_pending("month-mini-filterChanged"),a("body").trigger(c.filterChanged,{type:b.eventtype,hidden:b.hidden}),M.util.js_complete("month-mini-filterChanged")},i=function(a){return{eventtype:a.data("eventtype"),hidden:a.data("eventtype-hidden")}};return{init:function(b){b=a(b),f(b)}}});
|
||||
define(["jquery","core_calendar/selectors","core_calendar/events","core/str","core/templates"],function(a,b,c,d,e){var f=function(d){d.on("click",b.eventFilterItem,function(b){var c=a(b.currentTarget);g(c),b.preventDefault()}),a("body").on(c.viewUpdated,function(){var c=d.find(b.eventFilterItem);c.each(function(b,c){if(c=a(c),c.data("eventtype-hidden")){var d=i(c);h(d)}})})},g=function(a){var b=i(a);return b.hidden=!b.hidden,M.util.js_pending("core_calendar/calendar_filter:toggleFilter"),d.get_string("eventtype"+b.eventtype,"calendar").then(function(a){return b.name=a,b.icon=!0,b.key="i/"+b.eventtype+"event",b.component="core",b}).then(function(a){return e.render("core_calendar/event_filter_key",a)}).then(function(b,c){return e.replaceNode(a,b,c)}).then(function(){h(b),M.util.js_complete("core_calendar/calendar_filter:toggleFilter")})},h=function(b){M.util.js_pending("month-mini-filterChanged"),a("body").trigger(c.filterChanged,{type:b.eventtype,hidden:b.hidden}),M.util.js_complete("month-mini-filterChanged")},i=function(a){return{eventtype:a.data("eventtype"),hidden:a.data("eventtype-hidden")}};return{init:function(b){b=a(b),f(b)}}});
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
define(["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/modal_event_form","core_calendar/repository","core_calendar/events","core_calendar/modal_delete","core_calendar/selectors"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){function n(d,e,f){var i=[{key:"deleteevent",component:"calendar"}];f=parseInt(f,10);var m,n=f>1;n?(i.push({key:"confirmeventseriesdelete",component:"calendar",param:{name:e,count:f}}),m=g.create({type:l.TYPE})):(i.push({key:"confirmeventdelete",component:"calendar",param:e}),m=g.create({type:g.types.SAVE_CANCEL})),m.then(function(a){a.show()}).fail(c.exception);var o=b.get_strings(i),p=a.when(o,m).then(function(b,e){return e.setTitle(b[0]),e.setBody(b[1]),n||e.setSaveButtonText(b[0]),e.getRoot().on(h.save,function(){j.deleteEvent(d,!1).then(function(){a("body").trigger(k.deleted,[d,!1])})["catch"](c.exception)}),e.getRoot().on(k.deleteAll,function(){j.deleteEvent(d,!0).then(function(){a("body").trigger(k.deleted,[d,!0])})["catch"](c.exception)}),e}).fail(c.exception);return p}function o(b){b.on("click",m.actions.remove,function(b){var c=a(this).closest(m.eventItem),d=c.data("eventId"),e=c.data("eventTitle"),f=c.data("eventCount");n(d,e,f),b.preventDefault()})}function p(b,d){return d.then(function(c){a("body").on(k.editEvent,function(a,d){var e=b.find(m.wrapper);c.setEventId(d),c.setContextId(e.data("contextId")),c.show(),a.stopImmediatePropagation()})}).fail(c.exception),d}var q=function(b){var d=g.create({type:i.TYPE,large:!0});return b.on("click",m.actions.create,function(a){d.then(function(a){var c=b.find(m.wrapper),d=c.data("categoryid");"undefined"!=typeof d&&a.setCategoryId(d);var e=b.find(m.today),f=b.find(m.day);!e.length&&f.length&&a.setStartTime(f.data("newEventTimestamp")),a.setContextId(c.data("contextId")),a.setCourseId(c.data("courseid")),a.show()}).fail(c.exception),a.preventDefault()}),b.on("click",m.actions.edit,function(b){b.preventDefault();var e=a(b.currentTarget),f=e.closest(m.wrapper),g=e.closest(m.eventItem);d.then(function(a){a.setEventId(g.data("eventId")),a.setContextId(f.data("contextId")),a.show(),b.stopImmediatePropagation()}).fail(c.exception)}),d};return{registerRemove:o,registerEditListeners:p,registerEventFormModal:q}});
|
||||
define(["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/modal_event_form","core_calendar/repository","core_calendar/events","core_calendar/modal_delete","core_calendar/selectors","core/pending"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n){function o(d,e,f){var i=[{key:"deleteevent",component:"calendar"}];f=parseInt(f,10);var m,o=f>1;o?(i.push({key:"confirmeventseriesdelete",component:"calendar",param:{name:e,count:f}}),m=g.create({type:l.TYPE})):(i.push({key:"confirmeventdelete",component:"calendar",param:e}),m=g.create({type:g.types.SAVE_CANCEL}));var p=b.get_strings(i),q=a.when(p,m).then(function(b,e){return e.setTitle(b[0]),e.setBody(b[1]),o||e.setSaveButtonText(b[0]),e.show(),e.getRoot().on(h.save,function(){var b=new n("calendar/crud:initModal:deletedevent");j.deleteEvent(d,!1).then(function(){a("body").trigger(k.deleted,[d,!1])}).then(b.resolve)["catch"](c.exception)}),e.getRoot().on(k.deleteAll,function(){var b=new n("calendar/crud:initModal:deletedallevent");j.deleteEvent(d,!0).then(function(){a("body").trigger(k.deleted,[d,!0])}).then(b.resolve)["catch"](c.exception)}),e})["catch"](c.exception);return q}function p(b){b.on("click",m.actions.remove,function(b){var c=a(this).closest(m.eventItem),d=c.data("eventId"),e=c.data("eventTitle"),f=c.data("eventCount");o(d,e,f),b.preventDefault()})}function q(b,d){var e=new n("core_calendar/crud:registerEditListeners");return d.then(function(c){return a("body").on(k.editEvent,function(a,d){var e=b.find(m.wrapper);c.setEventId(d),c.setContextId(e.data("contextId")),c.show(),a.stopImmediatePropagation()}),c}).then(function(a){return e.resolve(),a})["catch"](c.exception)}var r=function(b){var d=g.create({type:i.TYPE,large:!0});return b.on("click",m.actions.create,function(a){d.then(function(a){var c=b.find(m.wrapper),d=c.data("categoryid");"undefined"!=typeof d&&a.setCategoryId(d);var e=b.find(m.today),f=b.find(m.day);!e.length&&f.length&&a.setStartTime(f.data("newEventTimestamp")),a.setContextId(c.data("contextId")),a.setCourseId(c.data("courseid")),a.show()}).fail(c.exception),a.preventDefault()}),b.on("click",m.actions.edit,function(b){b.preventDefault();var e=a(b.currentTarget),f=e.closest(m.wrapper),g=e.closest(m.eventItem);d.then(function(a){a.setEventId(g.data("eventId")),a.setContextId(f.data("contextId")),a.show(),b.stopImmediatePropagation()}).fail(c.exception)}),d};return{registerRemove:p,registerEditListeners:q,registerEventFormModal:r}});
|
||||
+1
-1
@@ -1 +1 @@
|
||||
define(["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/repository","core_calendar/events","core_calendar/crud"],function(a,b,c,d,e,f,g,h,i,j,k){var l=!1,m={ROOT:"[data-region='summary-modal-container']",EDIT_BUTTON:'[data-action="edit"]',DELETE_BUTTON:'[data-action="delete"]'},n=function(a){e.call(this,a)};return n.TYPE="core_calendar-event_summary",n.prototype=Object.create(e.prototype),n.prototype.constructor=n,n.prototype.getEditButton=function(){return"undefined"==typeof this.editButton&&(this.editButton=this.getFooter().find(m.EDIT_BUTTON)),this.editButton},n.prototype.getDeleteButton=function(){return"undefined"==typeof this.deleteButton&&(this.deleteButton=this.getFooter().find(m.DELETE_BUTTON)),this.deleteButton},n.prototype.getEventId=function(){return this.getBody().find(m.ROOT).attr("data-event-id")},n.prototype.getEventTitle=function(){return this.getBody().find(m.ROOT).attr("data-event-title")},n.prototype.getEventCount=function(){return this.getBody().find(m.ROOT).attr("data-event-count")},n.prototype.getEditUrl=function(){return this.getBody().find(m.ROOT).attr("data-edit-url")},n.prototype.isActionEvent=function(){return"true"==this.getBody().find(m.ROOT).attr("data-action-event")},n.prototype.registerEventListeners=function(){e.prototype.registerEventListeners.call(this),this.getRoot().on(h.bodyRendered,function(){this.getModal().data({eventTitle:this.getEventTitle(),eventId:this.getEventId(),eventCount:this.getEventCount()}).attr("data-type","event"),k.registerRemove(this.getModal())}.bind(this)),a("body").on(j.deleted,function(){this.hide()}.bind(this)),d.define(this.getEditButton(),[d.events.activate]),this.getEditButton().on(d.events.activate,function(b,c){this.isActionEvent()?a("body").trigger(j.editActionEvent,[this.getEditUrl()]):a("body").trigger(j.editEvent,[this.getEventId()]),this.hide(),b.preventDefault(),b.stopPropagation(),c.originalEvent.preventDefault(),c.originalEvent.stopPropagation()}.bind(this))},l||(f.register(n.TYPE,n,"core_calendar/event_summary_modal"),l=!0),n});
|
||||
define(["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/repository","core_calendar/events","core_calendar/crud"],function(a,b,c,d,e,f,g,h,i,j,k){var l=!1,m={ROOT:"[data-region='summary-modal-container']",EDIT_BUTTON:'[data-action="edit"]',DELETE_BUTTON:'[data-action="delete"]'},n=function(a){e.call(this,a)};return n.TYPE="core_calendar-event_summary",n.prototype=Object.create(e.prototype),n.prototype.constructor=n,n.prototype.getEditButton=function(){return"undefined"==typeof this.editButton&&(this.editButton=this.getFooter().find(m.EDIT_BUTTON)),this.editButton},n.prototype.getDeleteButton=function(){return"undefined"==typeof this.deleteButton&&(this.deleteButton=this.getFooter().find(m.DELETE_BUTTON)),this.deleteButton},n.prototype.getEventId=function(){return this.getBody().find(m.ROOT).attr("data-event-id")},n.prototype.getEventTitle=function(){return this.getBody().find(m.ROOT).attr("data-event-title")},n.prototype.getEventCount=function(){return this.getBody().find(m.ROOT).attr("data-event-count")},n.prototype.getEditUrl=function(){return this.getBody().find(m.ROOT).attr("data-edit-url")},n.prototype.isActionEvent=function(){return"true"==this.getBody().find(m.ROOT).attr("data-action-event")},n.prototype.registerEventListeners=function(){e.prototype.registerEventListeners.call(this),M.util.js_pending("core_calendar/summary_modal:registerEventListeners:bodyRendered"),this.getRoot().on(h.bodyRendered,function(){this.getModal().data({eventTitle:this.getEventTitle(),eventId:this.getEventId(),eventCount:this.getEventCount()}).attr("data-type","event"),k.registerRemove(this.getModal()),M.util.js_complete("core_calendar/summary_modal:registerEventListeners:bodyRendered")}.bind(this)),a("body").on(j.deleted,function(){this.hide()}.bind(this)),d.define(this.getEditButton(),[d.events.activate]),this.getEditButton().on(d.events.activate,function(b,c){this.isActionEvent()?a("body").trigger(j.editActionEvent,[this.getEditUrl()]):a("body").trigger(j.editEvent,[this.getEventId()]),this.hide(),b.preventDefault(),b.stopPropagation(),c.originalEvent.preventDefault(),c.originalEvent.stopPropagation()}.bind(this))},l||(f.register(n.TYPE,n,"core_calendar/event_summary_modal"),l=!0),n});
|
||||
+1
-1
@@ -1 +1 @@
|
||||
define(["jquery","core/templates","core/str","core/notification","core_calendar/repository","core_calendar/events","core_calendar/selectors","core/modal_factory","core/modal_events","core_calendar/summary_modal"],function(a,b,c,d,e,f,g,h,i,j){var k=function(b){b=a(b),b.on("click",g.links.eventLink,function(b){var c,d=a(b.target),e=null;c=d.is(g.actions.viewEvent)?d:d.closest(g.actions.viewEvent),e=c.length?c.data("eventId"):d.find(g.actions.viewEvent).data("eventId"),e&&(b.preventDefault(),b.stopPropagation(),v(e))}),b.on("click",g.links.navLink,function(c){var d=b.find(g.wrapper),e=d.data("view"),f=d.data("courseid"),h=d.data("categoryid"),i=a(c.currentTarget);"month"===e?(m(b,i.attr("href"),i.data("year"),i.data("month"),f,h),c.preventDefault()):"day"===e&&(q(b,i.attr("href"),i.data("year"),i.data("month"),i.data("day"),f,h),c.preventDefault())})},l=function(c,h,i,j,k,l){r(c),l=l||c.find(g.wrapper),M.util.js_pending([c.get("id"),h,i,j].join("-"));var m=c.data("includenavigation"),n=c.data("mini");return e.getCalendarMonthData(h,i,j,k,m,n).then(function(a){return b.render(c.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(l,a,c)}).then(function(){a("body").trigger(f.viewUpdated)}).always(function(){return M.util.js_complete([c.get("id"),h,i,j].join("-")),s(c)}).fail(d.exception)},m=function(b,c,d,e,g,h){return l(b,d,e,g,h).then(function(){return c.length&&"#"!==c&&window.history.pushState({},"",c),arguments}).then(function(){return a("body").trigger(f.monthChanged,[d,e,g,h]),arguments})},n=function(a,b,c){var d=a.find(g.wrapper).data("year"),e=a.find(g.wrapper).data("month");return"undefined"==typeof b&&(b=a.find(g.wrapper).data("courseid")),"undefined"==typeof c&&(c=a.find(g.wrapper).data("categoryid")),l(a,d,e,b,c)},o=function(c,h,i,j,k,l,m){r(c),m=m||c.find(g.wrapper),M.util.js_pending([c.get("id"),h,i,j,k,l].join("-"));var n=c.data("includenavigation");return e.getCalendarDayData(h,i,j,k,l,n).then(function(a){return b.render(c.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(m,a,c)}).then(function(){a("body").trigger(f.viewUpdated)}).always(function(){return M.util.js_complete([c.get("id"),h,i,j,k,l].join("-")),s(c)}).fail(d.exception)},p=function(a,b,c){var d=a.find(g.wrapper),e=d.data("year"),f=d.data("month"),h=d.data("day");return b||(b=a.find(g.wrapper).data("courseid")),"undefined"==typeof c&&(c=a.find(g.wrapper).data("categoryid")),o(a,e,f,h,b,c)},q=function(b,c,d,e,g,h,i){return o(b,d,e,g,h,i).then(function(){return c.length&&"#"!==c&&window.history.pushState({},"",c),arguments}).then(function(){return a("body").trigger(f.dayChanged,[d,e,g,h,i]),arguments})},r=function(a){var b=a.find(g.containers.loadingIcon);b.removeClass("hidden")},s=function(a){var b=a.find(g.containers.loadingIcon);b.addClass("hidden")},t=function(c,h,i){r(c);var j=c.find(g.wrapper);return"undefined"==typeof h&&(h=c.find(g.wrapper).data("courseid")),"undefined"==typeof i&&(i=c.find(g.wrapper).data("categoryid")),e.getCalendarUpcomingData(h,i).then(function(a){return b.render(c.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(j,a,c)}).then(function(){a("body").trigger(f.viewUpdated)}).always(function(){return s(c)}).fail(d.exception)},u=function(a){return"calendar_event_"+a},v=function(a){var c="";e.getEventById(a).then(function(b){if(!b.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+a);var d=b.event;return c=u(d.normalisedeventtype),d}).then(function(a){var d={title:a.name,type:j.TYPE,body:b.render("core_calendar/event_summary_body",a),templateContext:{canedit:a.canedit,candelete:a.candelete,headerclasses:c,isactionevent:a.isactionevent,url:a.url}};return h.create(d)}).done(function(a){a.getRoot().on(i.hidden,function(){a.destroy()}),a.show()}).fail(d.exception)};return{init:function(a){k(a)},reloadCurrentMonth:n,changeMonth:m,refreshMonthContent:l,reloadCurrentDay:p,changeDay:q,refreshDayContent:o,reloadCurrentUpcoming:t}});
|
||||
define(["jquery","core/templates","core/str","core/notification","core_calendar/repository","core_calendar/events","core_calendar/selectors","core/modal_factory","core/modal_events","core_calendar/summary_modal","core/pending"],function(a,b,c,d,e,f,g,h,i,j,k){var l=function(b){b=a(b),b.on("click",g.links.eventLink,function(b){var c,d=new k("core_calendar/view_manager:eventLink:click"),e=a(b.target),f=null;c=e.is(g.actions.viewEvent)?e:e.closest(g.actions.viewEvent),f=c.length?c.data("eventId"):e.find(g.actions.viewEvent).data("eventId"),f?(b.preventDefault(),b.stopPropagation(),w(f).then(d.resolve())["catch"]()):d.resolve()}),b.on("click",g.links.navLink,function(c){var d=b.find(g.wrapper),e=d.data("view"),f=d.data("courseid"),h=d.data("categoryid"),i=a(c.currentTarget);"month"===e?(n(b,i.attr("href"),i.data("year"),i.data("month"),f,h),c.preventDefault()):"day"===e&&(r(b,i.attr("href"),i.data("year"),i.data("month"),i.data("day"),f,h),c.preventDefault())})},m=function(c,h,i,j,k,l){s(c),l=l||c.find(g.wrapper),M.util.js_pending([c.get("id"),h,i,j].join("-"));var m=c.data("includenavigation"),n=c.data("mini");return e.getCalendarMonthData(h,i,j,k,m,n).then(function(a){return b.render(c.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(l,a,c)}).then(function(){a("body").trigger(f.viewUpdated)}).always(function(){return M.util.js_complete([c.get("id"),h,i,j].join("-")),t(c)}).fail(d.exception)},n=function(b,c,d,e,g,h){return m(b,d,e,g,h).then(function(){return c.length&&"#"!==c&&window.history.pushState({},"",c),arguments}).then(function(){return a("body").trigger(f.monthChanged,[d,e,g,h]),arguments})},o=function(a,b,c){var d=a.find(g.wrapper).data("year"),e=a.find(g.wrapper).data("month");return"undefined"==typeof b&&(b=a.find(g.wrapper).data("courseid")),"undefined"==typeof c&&(c=a.find(g.wrapper).data("categoryid")),m(a,d,e,b,c)},p=function(c,h,i,j,k,l,m){s(c),m=m||c.find(g.wrapper),M.util.js_pending([c.get("id"),h,i,j,k,l].join("-"));var n=c.data("includenavigation");return e.getCalendarDayData(h,i,j,k,l,n).then(function(a){return b.render(c.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(m,a,c)}).then(function(){a("body").trigger(f.viewUpdated)}).always(function(){return M.util.js_complete([c.get("id"),h,i,j,k,l].join("-")),t(c)}).fail(d.exception)},q=function(a,b,c){var d=a.find(g.wrapper),e=d.data("year"),f=d.data("month"),h=d.data("day");return b||(b=a.find(g.wrapper).data("courseid")),"undefined"==typeof c&&(c=a.find(g.wrapper).data("categoryid")),p(a,e,f,h,b,c)},r=function(b,c,d,e,g,h,i){return p(b,d,e,g,h,i).then(function(){return c.length&&"#"!==c&&window.history.pushState({},"",c),arguments}).then(function(){return a("body").trigger(f.dayChanged,[d,e,g,h,i]),arguments})},s=function(a){var b=a.find(g.containers.loadingIcon);b.removeClass("hidden")},t=function(a){var b=a.find(g.containers.loadingIcon);b.addClass("hidden")},u=function(c,h,i){s(c);var j=c.find(g.wrapper);return"undefined"==typeof h&&(h=c.find(g.wrapper).data("courseid")),"undefined"==typeof i&&(i=c.find(g.wrapper).data("categoryid")),e.getCalendarUpcomingData(h,i).then(function(a){return b.render(c.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(j,a,c)}).then(function(){a("body").trigger(f.viewUpdated)}).always(function(){return t(c)}).fail(d.exception)},v=function(a){return"calendar_event_"+a},w=function(a){var c=new k("core_calendar/view_manager:renderEventSummaryModal"),f="";return e.getEventById(a).then(function(b){if(!b.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+a);var c=b.event;return f=v(c.normalisedeventtype),c}).then(function(a){var c={title:a.name,type:j.TYPE,body:b.render("core_calendar/event_summary_body",a),templateContext:{canedit:a.canedit,candelete:a.candelete,headerclasses:f,isactionevent:a.isactionevent,url:a.url}};return h.create(c)}).then(function(a){return a.getRoot().on(i.hidden,function(){a.destroy()}),a.show(),a}).then(function(a){return c.resolve(),a})["catch"](d.exception)};return{init:function(a){l(a)},reloadCurrentMonth:o,changeMonth:n,refreshMonthContent:m,reloadCurrentDay:q,changeDay:r,refreshDayContent:p,reloadCurrentUpcoming:u}});
|
||||
@@ -64,6 +64,7 @@ function(
|
||||
// Toggle the hidden. We need to render the template before we change the value.
|
||||
data.hidden = !data.hidden;
|
||||
|
||||
M.util.js_pending("core_calendar/calendar_filter:toggleFilter");
|
||||
return Str.get_string('eventtype' + data.eventtype, 'calendar')
|
||||
.then(function(nameStr) {
|
||||
data.name = nameStr;
|
||||
@@ -81,6 +82,7 @@ function(
|
||||
})
|
||||
.then(function() {
|
||||
fireFilterChangedEvent(data);
|
||||
M.util.js_complete("core_calendar/calendar_filter:toggleFilter");
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* components by listening for and responding to different events
|
||||
* triggered within the calendar UI.
|
||||
*
|
||||
* @module core_calendar/calendar
|
||||
* @module core_calendar/calendar_mini
|
||||
* @package core_calendar
|
||||
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
|
||||
+19
-13
@@ -35,6 +35,7 @@ define([
|
||||
'core_calendar/events',
|
||||
'core_calendar/modal_delete',
|
||||
'core_calendar/selectors',
|
||||
'core/pending',
|
||||
],
|
||||
function(
|
||||
$,
|
||||
@@ -49,7 +50,8 @@ function(
|
||||
CalendarRepository,
|
||||
CalendarEvents,
|
||||
ModalDelete,
|
||||
CalendarSelectors
|
||||
CalendarSelectors,
|
||||
Pending
|
||||
) {
|
||||
|
||||
/**
|
||||
@@ -101,13 +103,6 @@ function(
|
||||
);
|
||||
}
|
||||
|
||||
deletePromise.then(function(deleteModal) {
|
||||
deleteModal.show();
|
||||
|
||||
return;
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
|
||||
var stringsPromise = Str.get_strings(deleteStrings);
|
||||
|
||||
var finalPromise = $.when(stringsPromise, deletePromise)
|
||||
@@ -118,27 +113,33 @@ function(
|
||||
deleteModal.setSaveButtonText(strings[0]);
|
||||
}
|
||||
|
||||
deleteModal.show();
|
||||
|
||||
deleteModal.getRoot().on(ModalEvents.save, function() {
|
||||
var pendingPromise = new Pending('calendar/crud:initModal:deletedevent');
|
||||
CalendarRepository.deleteEvent(eventId, false)
|
||||
.then(function() {
|
||||
$('body').trigger(CalendarEvents.deleted, [eventId, false]);
|
||||
return;
|
||||
})
|
||||
.then(pendingPromise.resolve)
|
||||
.catch(Notification.exception);
|
||||
});
|
||||
|
||||
deleteModal.getRoot().on(CalendarEvents.deleteAll, function() {
|
||||
var pendingPromise = new Pending('calendar/crud:initModal:deletedallevent');
|
||||
CalendarRepository.deleteEvent(eventId, true)
|
||||
.then(function() {
|
||||
$('body').trigger(CalendarEvents.deleted, [eventId, true]);
|
||||
return;
|
||||
})
|
||||
.then(pendingPromise.resolve)
|
||||
.catch(Notification.exception);
|
||||
});
|
||||
|
||||
return deleteModal;
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
.catch(Notification.exception);
|
||||
|
||||
return finalPromise;
|
||||
}
|
||||
@@ -233,7 +234,9 @@ function(
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function registerEditListeners(root, eventFormModalPromise) {
|
||||
eventFormModalPromise
|
||||
var pendingPromise = new Pending('core_calendar/crud:registerEditListeners');
|
||||
|
||||
return eventFormModalPromise
|
||||
.then(function(modal) {
|
||||
// When something within the calendar tells us the user wants
|
||||
// to edit an event then show the event form modal.
|
||||
@@ -245,11 +248,14 @@ function(
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
});
|
||||
return;
|
||||
return modal;
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
.then(function(modal) {
|
||||
pendingPromise.resolve();
|
||||
|
||||
return eventFormModalPromise;
|
||||
return modal;
|
||||
})
|
||||
.catch(Notification.exception);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -165,6 +165,7 @@ function(
|
||||
|
||||
// We have to wait for the modal to finish rendering in order to ensure that
|
||||
// the data-event-title property is available to use as the modal title.
|
||||
M.util.js_pending('core_calendar/summary_modal:registerEventListeners:bodyRendered');
|
||||
this.getRoot().on(ModalEvents.bodyRendered, function() {
|
||||
this.getModal().data({
|
||||
eventTitle: this.getEventTitle(),
|
||||
@@ -173,7 +174,7 @@ function(
|
||||
})
|
||||
.attr('data-type', 'event');
|
||||
CalendarCrud.registerRemove(this.getModal());
|
||||
|
||||
M.util.js_complete('core_calendar/summary_modal:registerEventListeners:bodyRendered');
|
||||
}.bind(this));
|
||||
|
||||
$('body').on(CalendarEvents.deleted, function() {
|
||||
|
||||
@@ -32,6 +32,7 @@ define([
|
||||
'core/modal_factory',
|
||||
'core/modal_events',
|
||||
'core_calendar/summary_modal',
|
||||
'core/pending',
|
||||
], function(
|
||||
$,
|
||||
Templates,
|
||||
@@ -42,7 +43,8 @@ define([
|
||||
CalendarSelectors,
|
||||
ModalFactory,
|
||||
ModalEvents,
|
||||
SummaryModal
|
||||
SummaryModal,
|
||||
Pending
|
||||
) {
|
||||
|
||||
/**
|
||||
@@ -55,6 +57,7 @@ define([
|
||||
|
||||
// Bind click events to event links.
|
||||
root.on('click', CalendarSelectors.links.eventLink, function(e) {
|
||||
var pendingPromise = new Pending('core_calendar/view_manager:eventLink:click');
|
||||
var target = $(e.target);
|
||||
var eventId = null;
|
||||
|
||||
@@ -79,7 +82,11 @@ define([
|
||||
// and causing the day click handler to fire.
|
||||
e.stopPropagation();
|
||||
|
||||
renderEventSummaryModal(eventId);
|
||||
renderEventSummaryModal(eventId)
|
||||
.then(pendingPromise.resolve())
|
||||
.catch();
|
||||
} else {
|
||||
pendingPromise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -353,12 +360,14 @@ define([
|
||||
* Render the event summary modal.
|
||||
*
|
||||
* @param {Number} eventId The calendar event id.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
var renderEventSummaryModal = function(eventId) {
|
||||
var pendingPromise = new Pending('core_calendar/view_manager:renderEventSummaryModal');
|
||||
var typeClass = '';
|
||||
|
||||
// Calendar repository promise.
|
||||
CalendarRepository.getEventById(eventId).then(function(getEventResponse) {
|
||||
return CalendarRepository.getEventById(eventId).then(function(getEventResponse) {
|
||||
if (!getEventResponse.event) {
|
||||
throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
|
||||
}
|
||||
@@ -384,7 +393,8 @@ define([
|
||||
// Create the modal.
|
||||
return ModalFactory.create(modalParams);
|
||||
|
||||
}).done(function(modal) {
|
||||
})
|
||||
.then(function(modal) {
|
||||
// Handle hidden event.
|
||||
modal.getRoot().on(ModalEvents.hidden, function() {
|
||||
// Destroy when hidden.
|
||||
@@ -394,7 +404,14 @@ define([
|
||||
// Finally, render the modal!
|
||||
modal.show();
|
||||
|
||||
}).fail(Notification.exception);
|
||||
return modal;
|
||||
})
|
||||
.then(function(modal) {
|
||||
pendingPromise.resolve();
|
||||
|
||||
return modal;
|
||||
})
|
||||
.catch(Notification.exception);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
+26
@@ -126,6 +126,10 @@ class day_exporter extends exporter {
|
||||
'type' => PARAM_URL,
|
||||
'optional' => true,
|
||||
],
|
||||
'viewdaylinktitle' => [
|
||||
'type' => PARAM_RAW,
|
||||
'optional' => true,
|
||||
],
|
||||
'events' => [
|
||||
'type' => calendar_event_exporter::read_properties_definition(),
|
||||
'multiple' => true,
|
||||
@@ -183,6 +187,10 @@ class day_exporter extends exporter {
|
||||
'viewdaylink' => $this->url->out(false),
|
||||
];
|
||||
|
||||
if ($viewdaylinktitle = $this->get_view_link_title()) {
|
||||
$return['viewdaylinktitle'] = $viewdaylinktitle;
|
||||
}
|
||||
|
||||
|
||||
$cache = $this->related['cache'];
|
||||
$eventexporters = array_map(function($event) use ($cache, $output) {
|
||||
@@ -267,4 +275,22 @@ class day_exporter extends exporter {
|
||||
'time' => $this->calendar->time,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title for view link.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_view_link_title() {
|
||||
$title = null;
|
||||
|
||||
$userdate = userdate($this->data[0], get_string('strftimedayshort'));
|
||||
if ($this->data['istoday']) {
|
||||
$title = get_string('todayplustitle', 'calendar', $userdate);
|
||||
} else if (count($this->related['events'])) {
|
||||
$title = get_string('eventsfor', 'calendar', $userdate);
|
||||
}
|
||||
|
||||
return $title;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,12 +292,16 @@ class core_calendar_renderer extends plugin_renderer_base {
|
||||
$courseurl = new moodle_url($returnurl);
|
||||
$courseurl->remove_params('course');
|
||||
|
||||
if ($label === null) {
|
||||
if (empty($label)) {
|
||||
$label = get_string('listofcourses');
|
||||
$labelclass = 'sr-only';
|
||||
} else {
|
||||
$labelclass = 'mr-1';
|
||||
}
|
||||
|
||||
$select = html_writer::label($label, 'course', false, ['class' => 'mr-1']);
|
||||
$select .= html_writer::select($courseoptions, 'course', $selected, false, ['class' => 'cal_courses_flt']);
|
||||
$select = html_writer::label($label, 'course', false, ['class' => $labelclass]);
|
||||
$select .= html_writer::select($courseoptions, 'course', $selected, false,
|
||||
['class' => 'cal_courses_flt', 'id' => 'course']);
|
||||
|
||||
return $select;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,11 @@
|
||||
}} data-container="body"{{!
|
||||
}} data-toggle="popover"{{!
|
||||
}} data-html="true"{{!
|
||||
}} data-trigger="hover"{{!
|
||||
}} data-trigger="hover focus"{{!
|
||||
}} data-placement="top"{{!
|
||||
}} data-title="{{$title}}{{title}}{{/title}}"{{!
|
||||
}} data-alternate="{{$nocontent}}{{/nocontent}}"{{!
|
||||
}} aria-label="{{viewdaylinktitle}}"{{!
|
||||
}}>{{$day}}{{day}}{{/day}}</a>
|
||||
<div class="hidden">
|
||||
{{$content}}{{/content}}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
{{# daynames }}
|
||||
<th class="header text-xs-center">
|
||||
<th class="header text-xs-center" aria-label="{{fullname}}">
|
||||
{{shortname}}
|
||||
</th>
|
||||
{{/ daynames }}
|
||||
@@ -73,7 +73,7 @@
|
||||
data-new-event-timestamp="{{neweventtimestamp}}">
|
||||
<div class="d-none d-md-block hidden-phone text-xs-center">
|
||||
{{#hasevents}}
|
||||
<a data-action="view-day-link" href="{{viewdaylink}}" class="day" title="{{viewdaylinktitle}}">{{mday}}</a>
|
||||
<a data-action="view-day-link" href="{{viewdaylink}}" class="day" aria-label="{{viewdaylinktitle}}">{{mday}}</a>
|
||||
{{/hasevents}}
|
||||
{{^hasevents}}
|
||||
{{mday}}
|
||||
@@ -121,12 +121,10 @@
|
||||
</div>
|
||||
<div class="d-md-none hidden-desktop hidden-tablet">
|
||||
{{#hasevents}}
|
||||
<a data-action="view-day-link" href="{{viewdaylink}}" class="day" title="{{viewdaylinktitle}}">{{mday}}</a>
|
||||
<a data-action="view-day-link" href="{{viewdaylink}}" class="day" aria-label="{{viewdaylinktitle}}">{{mday}}</a>
|
||||
{{/hasevents}}
|
||||
{{^hasevents}}
|
||||
<div data-region="day-content">
|
||||
{{mday}}
|
||||
</div>
|
||||
{{/hasevents}}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -77,8 +77,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
{{# daynames }}
|
||||
<th class="header text-xs-center" scope="col">
|
||||
<abbr title="{{fullname}}">{{shortname}}</abbr>
|
||||
<th class="header text-xs-center" scope="col" aria-label="{{fullname}}">
|
||||
{{shortname}}
|
||||
</th>
|
||||
{{/ daynames }}
|
||||
</tr>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template calendar/calendar_threemonth
|
||||
@template calendar/threemonth_month
|
||||
|
||||
Calendar view to show three months as a block.
|
||||
|
||||
|
||||
@@ -52,11 +52,23 @@ require_once($CFG->dirroot.'/calendar/lib.php');
|
||||
$categoryid = optional_param('category', null, PARAM_INT);
|
||||
$courseid = optional_param('course', SITEID, PARAM_INT);
|
||||
$view = optional_param('view', 'upcoming', PARAM_ALPHA);
|
||||
$day = optional_param('cal_d', 0, PARAM_INT);
|
||||
$mon = optional_param('cal_m', 0, PARAM_INT);
|
||||
$year = optional_param('cal_y', 0, PARAM_INT);
|
||||
$time = optional_param('time', 0, PARAM_INT);
|
||||
$lookahead = optional_param('lookahead', null, PARAM_INT);
|
||||
|
||||
$url = new moodle_url('/calendar/view.php');
|
||||
|
||||
// If a day, month and year were passed then convert it to a timestamp. If these were passed
|
||||
// then we can assume the day, month and year are passed as Gregorian, as no where in core
|
||||
// should we be passing these values rather than the time. This is done for BC.
|
||||
if (!empty($day) && !empty($mon) && !empty($year)) {
|
||||
if (checkdate($mon, $day, $year)) {
|
||||
$time = make_timestamp($year, $mon, $day);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($time)) {
|
||||
$time = time();
|
||||
}
|
||||
|
||||
@@ -4777,6 +4777,40 @@ class api {
|
||||
$DB->delete_records(template_cohort::TABLE, array('cohortid' => $cohort->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to perform when a user is deleted.
|
||||
*
|
||||
* @param int $userid The user id.
|
||||
*/
|
||||
public static function hook_user_deleted($userid) {
|
||||
global $DB;
|
||||
|
||||
$usercompetencies = $DB->get_records(user_competency::TABLE, ['userid' => $userid], '', 'id');
|
||||
foreach ($usercompetencies as $usercomp) {
|
||||
$DB->delete_records(evidence::TABLE, ['usercompetencyid' => $usercomp->id]);
|
||||
}
|
||||
|
||||
$DB->delete_records(user_competency::TABLE, ['userid' => $userid]);
|
||||
$DB->delete_records(user_competency_course::TABLE, ['userid' => $userid]);
|
||||
$DB->delete_records(user_competency_plan::TABLE, ['userid' => $userid]);
|
||||
|
||||
// Delete any associated files.
|
||||
$fs = get_file_storage();
|
||||
$context = context_user::instance($userid);
|
||||
$userevidences = $DB->get_records(user_evidence::TABLE, ['userid' => $userid], '', 'id');
|
||||
foreach ($userevidences as $userevidence) {
|
||||
$DB->delete_records(user_evidence_competency::TABLE, ['userevidenceid' => $userevidence->id]);
|
||||
$DB->delete_records(user_evidence::TABLE, ['id' => $userevidence->id]);
|
||||
$fs->delete_area_files($context->id, 'core_competency', 'userevidence', $userevidence->id);
|
||||
}
|
||||
|
||||
$userplans = $DB->get_records(plan::TABLE, ['userid' => $userid], '', 'id');
|
||||
foreach ($userplans as $userplan) {
|
||||
$DB->delete_records(plan_competency::TABLE, ['planid' => $userplan->id]);
|
||||
$DB->delete_records(plan::TABLE, ['id' => $userplan->id]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually grade a user competency.
|
||||
*
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user