Compare commits
636 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd27666c42 | |||
| d1787723db | |||
| 7ba9f14fe0 | |||
| 0d70e9b8e9 | |||
| 432d3cc846 | |||
| 59de100b8b | |||
| f1e0f1c194 | |||
| 444d046f9e | |||
| 64722afd45 | |||
| be092b7309 | |||
| 005b2bc8a0 | |||
| f4bd5f5e1a | |||
| 1a8342d9d4 | |||
| 6a87753e12 | |||
| 1d16496ee6 | |||
| e7dc32ad80 | |||
| fde541a1eb | |||
| 95ee02b253 | |||
| f562290f78 | |||
| 1273a6665b | |||
| 74ea4a91e1 | |||
| d035bebb1e | |||
| 4c51211800 | |||
| 61edefee12 | |||
| 304bd3b26b | |||
| 57c5494741 | |||
| f1019f4616 | |||
| bbdb62fffe | |||
| cce2977770 | |||
| 60357a5551 | |||
| ce5ea7e187 | |||
| 36b7d0471c | |||
| 580a0918b4 | |||
| 6aaf582982 | |||
| d6cbf8b8fb | |||
| e760287046 | |||
| f0b924f980 | |||
| 1260ed9e33 | |||
| 79df6aee8c | |||
| ee9c931987 | |||
| 01389839df | |||
| 88a78e9307 | |||
| 99e51855a6 | |||
| 5263019b3a | |||
| 442dc8270a | |||
| dab8d7a0dc | |||
| 9f256a8380 | |||
| 6f0614ef19 | |||
| 43dfc84482 | |||
| 5a7e921f70 | |||
| 0220cc4bdf | |||
| d470ef4c27 | |||
| efecaa34a4 | |||
| 11694360bd | |||
| 060e39220c | |||
| e016281821 | |||
| 2267c9fe7f | |||
| 8bcca194ce | |||
| 9bb1a66c3c | |||
| 627b0a7a45 | |||
| f873ff9ad2 | |||
| 0dfef1313f | |||
| e647f49fe4 | |||
| 088f6b973a | |||
| 3c613eb0f9 | |||
| 05403f4989 | |||
| a0a10c4d4c | |||
| f92552d372 | |||
| 769987cf28 | |||
| 3bee541afc | |||
| 069bf5a9dd | |||
| b5c2fef399 | |||
| 6c5b701bb5 | |||
| 696e063e65 | |||
| 0807b60603 | |||
| 2e7dbfaabb | |||
| d6e31537bc | |||
| d01d3d8eef | |||
| 7ac4ad9a8b | |||
| 9d349b8170 | |||
| bd20ed1a7c | |||
| f5d6dcfd06 | |||
| 466ae79141 | |||
| 821001ce9c | |||
| ac7cf1cfeb | |||
| f2057a1115 | |||
| 61c36e125e | |||
| 629c7f75b3 | |||
| bdefa64956 | |||
| bfc047cef0 | |||
| 9c9227995c | |||
| 3a69718c58 | |||
| cb8a553af9 | |||
| 83dc898b00 | |||
| 3e6e80f96f | |||
| 1b052996ff | |||
| e516332576 | |||
| 50615d7105 | |||
| 867e35d77f | |||
| 72337efdf7 | |||
| c0d8a1c3e1 | |||
| f6be98b942 | |||
| d694bf2435 | |||
| 2f58da96bb | |||
| 66f63a3bf6 | |||
| 349ac5cbd7 | |||
| 7985a77474 | |||
| 9df6a3972d | |||
| 5e26c462d3 | |||
| bd547eb6dd | |||
| b4db1fd8ad | |||
| 8cdf03c5a9 | |||
| 17567f33c9 | |||
| 8509759768 | |||
| 20eb1297ef | |||
| ddf605c655 | |||
| 6efc731aab | |||
| 6ea7ce8acb | |||
| 74a30640d6 | |||
| 105bfe1d95 | |||
| e87bbfbe94 | |||
| 5450a19dec | |||
| 094730c7d2 | |||
| 76f2ed9c6a | |||
| 0af0d1d7f6 | |||
| 6415201009 | |||
| 99566b5a60 | |||
| 06a92988fa | |||
| 4202a6935e | |||
| 7c730dfe01 | |||
| 6e0f59fe32 | |||
| 6ed9356daf | |||
| 8539f6168e | |||
| 9e6ea43d93 | |||
| 6b65d96d70 | |||
| 98e96df381 | |||
| 127b7446a0 | |||
| a032e58de6 | |||
| 4db39b4571 | |||
| a9b13cfac3 | |||
| c1d4386dd7 | |||
| b3065a69ed | |||
| aba27d5317 | |||
| 7391dfc208 | |||
| 6d2fedaa47 | |||
| 02f31bb65b | |||
| 1e2f780503 | |||
| 57bebf7c4b | |||
| f940b5dcd9 | |||
| 7cccc7e03a | |||
| 76483d4f45 | |||
| d3ef856e79 | |||
| 5c4aef2664 | |||
| 4bd4b46070 | |||
| 2392c10734 | |||
| f86bd7ece0 | |||
| 805417c33d | |||
| 127b54ffbe | |||
| 8450d9948a | |||
| 3eca9bf311 | |||
| 6b3738bc20 | |||
| 1974156194 | |||
| f0a4839154 | |||
| 3ab6d8ba2b | |||
| 1d2b6c84a2 | |||
| 266744a6f9 | |||
| c3a1ccc244 | |||
| 96df578e8e | |||
| 6cf6523cc7 | |||
| 43eadce6fc | |||
| 98fe6eee32 | |||
| 671ad6b497 | |||
| 49bfb1e5f9 | |||
| 000a5d0bec | |||
| 936ed6067f | |||
| 238006f649 | |||
| d11038dd43 | |||
| 6b8b06911b | |||
| fe1ae23939 | |||
| 64b5d44abc | |||
| 526e3e8c97 | |||
| 8668391630 | |||
| f35e7a0eeb | |||
| 949f522c13 | |||
| ac6bef4e38 | |||
| 72bb7ac302 | |||
| 1b4cd6be30 | |||
| 10dae695ec | |||
| fc12d8d459 | |||
| c46c85055b | |||
| f4411ba700 | |||
| 37002deeda | |||
| 06e4325bda | |||
| 8aa12f5bff | |||
| 84bef5413c | |||
| df26945105 | |||
| b9b8d94716 | |||
| ec92a0762d | |||
| 9bd851f39c | |||
| 502b17bebd | |||
| 2857ca0c9f | |||
| 5a1aa729df | |||
| 6020745280 | |||
| 948485dbd6 | |||
| 1da88f2c6a | |||
| ab38db069e | |||
| c1a29fc252 | |||
| a1d8976005 | |||
| c0171e6090 | |||
| bdf7f7613e | |||
| b42457028a | |||
| 783f2a8b8e | |||
| ac496f22af | |||
| a1c3aedf5b | |||
| a2c9e82c42 | |||
| 82d2d27cd3 | |||
| e8d8bccb25 | |||
| d5859dacc8 | |||
| d738ae91e7 | |||
| daff43e9c7 | |||
| 6aedf45bee | |||
| fa9fffdc9a | |||
| 7ad1d28554 | |||
| 3f3385c46a | |||
| 11ebc149ed | |||
| c985ad6263 | |||
| 6a6d683283 | |||
| ab9f7ab790 | |||
| 3248a578e9 | |||
| 3cd9d0a689 | |||
| 8ee6b2a201 | |||
| 692049a9c1 | |||
| 36895ee702 | |||
| 68e2d67939 | |||
| dd6f34e334 | |||
| 310eff99fc | |||
| 5047c83b9c | |||
| 5b531aabee | |||
| 5723130d84 | |||
| f490505b23 | |||
| ed90c88864 | |||
| faabcf6d76 | |||
| 8837262d2c | |||
| e30562a127 | |||
| b43f63c11f | |||
| 78ccc2c951 | |||
| 9835dc02dd | |||
| 5420485076 | |||
| e69a3f20ee | |||
| 5e29146127 | |||
| 421619e6e3 | |||
| 39ffa8c6e3 | |||
| 53e622ba88 | |||
| 3f70323ba7 | |||
| 99d1a03857 | |||
| a2ddeab20e | |||
| bb05597274 | |||
| cb8c503098 | |||
| dfe3389cf5 | |||
| aac5333538 | |||
| c9038ad429 | |||
| 6d0a80dfc1 | |||
| f22286dd83 | |||
| c4030220a4 | |||
| e088a1f198 | |||
| b6f35bf63e | |||
| b5282bfb67 | |||
| 5721a09e82 | |||
| 21e4f90ca8 | |||
| 9cb0f99c37 | |||
| 4c66ff31c8 | |||
| 2542cb94e4 | |||
| 97be43646f | |||
| 4b1adeef8d | |||
| 0ef5bc0b18 | |||
| 8632026272 | |||
| 706ba3f3af | |||
| c2c97dee39 | |||
| c74949b071 | |||
| 6dc3988fa0 | |||
| 1d59ae4286 | |||
| 38d309036c | |||
| 3c2fa60af3 | |||
| a9b2db5880 | |||
| 3482ea90a6 | |||
| c50377f55f | |||
| 2a9787beee | |||
| 73df15dd67 | |||
| 3fe9524aaf | |||
| 0ec785bfa5 | |||
| c6fa9f2ea2 | |||
| c485c0c443 | |||
| 474ec7e94b | |||
| d7b6ef5729 | |||
| fb6ae10135 | |||
| cadff587be | |||
| 74fbce7560 | |||
| 6ede900c49 | |||
| 4f7cf4088d | |||
| 76486ab639 | |||
| a3809c4fc6 | |||
| b80a22f5ba | |||
| ef6bdbae8e | |||
| 85abbf80e2 | |||
| f2757d51a9 | |||
| 24e83b2807 | |||
| 4e37604528 | |||
| 8b2ae94442 | |||
| 4dec82b25b | |||
| e3796a0678 | |||
| 104c9ffae9 | |||
| c5a4536bb8 | |||
| 740db2e5c3 | |||
| 9fc60dfeb1 | |||
| c270b92ddb | |||
| 835d570210 | |||
| 7a81bcdb34 | |||
| 9818499ed8 | |||
| 5e0c2c9815 | |||
| 50e48b78e9 | |||
| f3a11484ad | |||
| af9b4f1f3b | |||
| 25c946f628 | |||
| 110f3a3d68 | |||
| b20a4bc15c | |||
| d4315bf53e | |||
| f31105a9f5 | |||
| 771c8455cb | |||
| 8297de9fa3 | |||
| e336f07cd7 | |||
| 2ff2c49c85 | |||
| 2f7aaf1d53 | |||
| 551f57926a | |||
| b8daa178de | |||
| 8405e6f1cc | |||
| 89076b49fb | |||
| 8fc16d9e6a | |||
| 5136441985 | |||
| 1afa3903ae | |||
| 4c32762ca7 | |||
| 313531b030 | |||
| 2b37ee6049 | |||
| a5f3967e57 | |||
| 5b78584fdb | |||
| ce390c4725 | |||
| 942d4df55b | |||
| 23628a98fc | |||
| 2daceaf000 | |||
| 8675c2c908 | |||
| 70ffd8bb2b | |||
| 6c1aa273f2 | |||
| 8cc2d39207 | |||
| cac9523423 | |||
| cb4bb6efc5 | |||
| 750121698f | |||
| a78f6cd123 | |||
| c2fea8ae43 | |||
| 0a520053cb | |||
| 1da4edbaf0 | |||
| 9eff7d9535 | |||
| e154204728 | |||
| 2d90a7d91a | |||
| 8ac7c623d3 | |||
| caa9881c0d | |||
| b01258d7f0 | |||
| f0188d7b66 | |||
| 4e52235a23 | |||
| ab94fee038 | |||
| 2d64d63373 | |||
| 6cb165d681 | |||
| 55fc303a5f | |||
| c453ea21e1 | |||
| 24e4ec9d60 | |||
| 25e9be8ea9 | |||
| 9ffcaa7db7 | |||
| 936d21d2dd | |||
| d20b4b2f23 | |||
| d587c4bd90 | |||
| 3828033e98 | |||
| 4892662238 | |||
| ccbd142a63 | |||
| 3b52f52351 | |||
| 173968b25d | |||
| 4cdd9e77dc | |||
| 7805012e59 | |||
| 483d8fefa8 | |||
| 654eac4f13 | |||
| 86699bedb0 | |||
| 914e26d916 | |||
| ced6691934 | |||
| 07e1fa272f | |||
| ff1fbf2992 | |||
| 49e435c00a | |||
| b67ae5a0ad | |||
| b2b1bb2d29 | |||
| 5a13884a0f | |||
| d42328e223 | |||
| 3879b520b7 | |||
| 6a3cffd3c6 | |||
| 40790755ce | |||
| 310a1f0895 | |||
| ed3468e40b | |||
| 9bedb84419 | |||
| c843630aee | |||
| 94f5d062bd | |||
| 233847bdb7 | |||
| f29d518ba8 | |||
| 749c7cc5a9 | |||
| bb3ce79c7e | |||
| 4b52a5b2c6 | |||
| af344834e1 | |||
| cdc9e56198 | |||
| 0e0fd72d98 | |||
| 543005a342 | |||
| 5c33665a74 | |||
| 362518ef34 | |||
| ff246f2260 | |||
| fc1b13ede9 | |||
| 458b0fb900 | |||
| 5df53c769e | |||
| 380a437428 | |||
| 5f6539e379 | |||
| b44ddd4a7f | |||
| 833e2398ef | |||
| df9443db71 | |||
| 4d23edfb37 | |||
| 11489b20a5 | |||
| b3483e7ede | |||
| dd05e0d29e | |||
| 1b37fe78f9 | |||
| 82f21031a3 | |||
| 6545e50f62 | |||
| 3d7630a158 | |||
| 2588126d66 | |||
| de71bef7cb | |||
| e310f10818 | |||
| 9b6938f99b | |||
| 9c42327dd1 | |||
| 28631a1455 | |||
| aa7deee9cc | |||
| 009a9ca500 | |||
| 7de4759799 | |||
| 33433bb8a1 | |||
| 9efe4b84ce | |||
| 12d4fe9ee6 | |||
| 8793e120b9 | |||
| 43f742e1e5 | |||
| 3f4db3e89e | |||
| bea7b33840 | |||
| 4900f375d7 | |||
| a94b1fed30 | |||
| 9337d85eb3 | |||
| 2a8f28520d | |||
| e972b9e909 | |||
| 74c35a9133 | |||
| b4b5124c73 | |||
| af963ad2ae | |||
| 2341e8695e | |||
| f55516fd46 | |||
| 36e337b176 | |||
| 23e9fea9a5 | |||
| 1408aa8bc8 | |||
| 2f86cad04e | |||
| fd77e38fc9 | |||
| 0191113d24 | |||
| 016e029ea6 | |||
| 8bc0bbf1ee | |||
| 1dc4c34d07 | |||
| 5339d26259 | |||
| bc7a379947 | |||
| e9553d973a | |||
| 4f7fa18201 | |||
| 697a28aadf | |||
| 26f5750319 | |||
| d4fea9b20f | |||
| af280e5929 | |||
| fa8d508f36 | |||
| e7bf43ba0d | |||
| 793da897d1 | |||
| e1aa99e99d | |||
| 06b8272791 | |||
| 15703fb9f1 | |||
| 20d8c489d8 | |||
| 1d97a7818e | |||
| 7c64406d4d | |||
| f71b39b960 | |||
| 44dd58756b | |||
| 1dbbcaf9ba | |||
| d0e1b2921d | |||
| 72107631d7 | |||
| 888cfdc75f | |||
| 9886790075 | |||
| f3323c22fe | |||
| d8e011f721 | |||
| 3b63cb559e | |||
| f5189e3aa3 | |||
| 2c643bb988 | |||
| 54e6ea018d | |||
| c5fd53af5d | |||
| e275b0c4a6 | |||
| 9482a77a04 | |||
| fab105578b | |||
| b4dcfe9a06 | |||
| a31a20a5c6 | |||
| e6e7a85087 | |||
| 612c718696 | |||
| 180988c999 | |||
| 59380a9a64 | |||
| d2593fadb6 | |||
| 904aec69a0 | |||
| d851bb5c2a | |||
| 6fa61e69a7 | |||
| c0e65fbbf8 | |||
| cc0b367755 | |||
| 1dc3e88cca | |||
| d04e88b78f | |||
| d77209e8c5 | |||
| 3f27598353 | |||
| 334ced71a1 | |||
| f3a817e959 | |||
| 9712975595 | |||
| 4656db12e1 | |||
| a1ec9e389e | |||
| 3714b3d104 | |||
| 251358b364 | |||
| d2b6ce1555 | |||
| 3c22418e0a | |||
| 94269e4505 | |||
| 0f272f06ea | |||
| 08ca997c76 | |||
| 7d52baba45 | |||
| 72f73d595d | |||
| bf67536031 | |||
| 7a78ae6c10 | |||
| 2cdbfd1dca | |||
| d8924ce166 | |||
| 9ccbf4386c | |||
| fe9d8a7274 | |||
| 9ec5fa2208 | |||
| bffc50ae9d | |||
| a634c88ced | |||
| 2eb7c6d6cb | |||
| 8ae9044ef4 | |||
| eef776a0b4 | |||
| ef57748905 | |||
| 9903e25871 | |||
| 557a9b5674 | |||
| 0ff993ffe3 | |||
| a8a9876c1d | |||
| fd308528cd | |||
| b0f0c2af7e | |||
| ba5dde0347 | |||
| 22cab09558 | |||
| b02f319b78 | |||
| e03ef397fc | |||
| cddd2b461b | |||
| ec41f1ce1b | |||
| 7ce29a90af | |||
| 4dda9579b3 | |||
| 9b77f93fa0 | |||
| 4fc3adb35b | |||
| d3e3f0f109 | |||
| 8bc2ee1a4a | |||
| 93e51e74b4 | |||
| 148ef43015 | |||
| 8b5dfd0b06 | |||
| 1ebd063f4e | |||
| 40579dc143 | |||
| f26629e665 | |||
| ab96a452c6 | |||
| 50e1bbb583 | |||
| 09b6fbdaca | |||
| 8f9c8a5826 | |||
| 9c607c0b8c | |||
| 6dc178e448 | |||
| 187d2a26c0 | |||
| c6755d6f34 | |||
| bea2c40f53 | |||
| 363622d03e | |||
| 649c96c259 | |||
| ac32f752d9 | |||
| db79038541 | |||
| c390ae4f8d | |||
| e0a8cfdb6d | |||
| 52f03c6fc6 | |||
| 9f2c4a9277 | |||
| 575e209b93 | |||
| 0b363e6c4b | |||
| 5d8821ae05 | |||
| 512a916207 | |||
| 86a7cf0909 | |||
| f93f81a9f6 | |||
| bf6f0f03cf | |||
| 903182e4a3 | |||
| e7554e4b67 | |||
| 167420bd2e | |||
| f479887c77 | |||
| 5441c8fef4 | |||
| b104cede75 | |||
| c3c8223ca2 | |||
| d4773c32d6 | |||
| c8511e2808 | |||
| b23e6519fe | |||
| dc7c48a9d7 | |||
| a210233bfb | |||
| 9dcf28af7d | |||
| c030a94b68 | |||
| bd8b3a91e0 | |||
| 582eb6e6e2 | |||
| 9b127bcb70 | |||
| 171a764049 | |||
| 0da8ef69a8 | |||
| 815be4e003 | |||
| d897846115 | |||
| 5427855d50 | |||
| 6d7093e370 | |||
| d7c805a9f0 | |||
| 2032603ae9 | |||
| e7e91b8bae | |||
| 30cbfbdd61 | |||
| ab7694d82e | |||
| 61ee9a304e | |||
| f59bc1b152 | |||
| 3aa57f20af | |||
| b875ad18b5 | |||
| 2d69b0fbe7 | |||
| c8663da1a9 | |||
| 1de2735b16 | |||
| 4a5adabe95 | |||
| b566d8002a | |||
| 2e404995a0 | |||
| 654ad71526 | |||
| 2b50975563 | |||
| 81fb99ed66 | |||
| deb358f16c | |||
| 881098f39c |
@@ -2,6 +2,7 @@
|
||||
theme/bootstrapbase/style/
|
||||
theme/clean/style/custom.css
|
||||
theme/more/style/custom.css
|
||||
theme/boost/style/moodle.css
|
||||
node_modules/
|
||||
vendor/
|
||||
admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ matrix:
|
||||
include:
|
||||
# Run grunt/npm install on highest version ('node' is an alias for the latest node.js version.)
|
||||
- php: 7.2
|
||||
env: DB=none TASK=GRUNT NVM_VERSION='8.9'
|
||||
env: DB=none TASK=GRUNT NVM_VERSION='lts/carbon'
|
||||
|
||||
exclude:
|
||||
# MySQL - it's just too slow.
|
||||
|
||||
+14
-2
@@ -137,6 +137,16 @@ module.exports = function(grunt) {
|
||||
}
|
||||
}
|
||||
},
|
||||
sass: {
|
||||
dist: {
|
||||
files: {
|
||||
"theme/boost/style/moodle.css": "theme/boost/scss/preset/default.scss"
|
||||
}
|
||||
},
|
||||
options: {
|
||||
includePaths: ["theme/boost/scss/"]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
options: {
|
||||
nospawn: true // We need not to spawn so config can be changed dynamically.
|
||||
@@ -214,7 +224,8 @@ module.exports = function(grunt) {
|
||||
'# Generated by "grunt ignorefiles"',
|
||||
'theme/bootstrapbase/style/',
|
||||
'theme/clean/style/custom.css',
|
||||
'theme/more/style/custom.css'
|
||||
'theme/more/style/custom.css',
|
||||
'theme/boost/style/moodle.css'
|
||||
].concat(thirdPartyPaths);
|
||||
grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
|
||||
};
|
||||
@@ -362,6 +373,7 @@ module.exports = function(grunt) {
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
grunt.loadNpmTasks('grunt-contrib-less');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-sass');
|
||||
grunt.loadNpmTasks('grunt-eslint');
|
||||
grunt.loadNpmTasks('grunt-stylelint');
|
||||
|
||||
@@ -374,7 +386,7 @@ module.exports = function(grunt) {
|
||||
grunt.registerTask('js', ['amd', 'yui']);
|
||||
|
||||
// Register CSS taks.
|
||||
grunt.registerTask('css', ['stylelint:scss', 'stylelint:less', 'less:bootstrapbase', 'stylelint:css']);
|
||||
grunt.registerTask('css', ['stylelint:scss', 'sass', 'stylelint:less', 'less:bootstrapbase', 'stylelint:css']);
|
||||
|
||||
// Register the startup task.
|
||||
grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup);
|
||||
|
||||
@@ -2435,4 +2435,185 @@
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="3.6" requires="3.1">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="5.5.31" />
|
||||
<VENDOR name="mysql" version="5.6" />
|
||||
<VENDOR name="postgres" version="9.4" />
|
||||
<VENDOR name="mssql" version="10.0" />
|
||||
<VENDOR name="oracle" version="11.2" />
|
||||
</DATABASE>
|
||||
<PHP version="7.0.0" level="required">
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="pcreunicodewarning" />
|
||||
</FEEDBACK>
|
||||
</PCREUNICODE>
|
||||
<PHP_EXTENSIONS>
|
||||
<PHP_EXTENSION name="iconv" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="iconvrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="mbstring" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="mbstringrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="curl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="curlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="openssl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="opensslrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="tokenizer" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlrpc" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="xmlrpcrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="ctype" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ctyperequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zip" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ziprequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zlib" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="gd" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="gdrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="simplexml" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="simplexmlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="spl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="splrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="pcre" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="dom" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xml" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlreader" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="intl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="intlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="json" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="settingmemorylimit" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="file_uploads" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingfileuploads" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="opcache.enable" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="opcacherecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
</PHP_SETTINGS>
|
||||
<CUSTOM_CHECKS>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbstorageengine" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="quizattemptsupgradedmessage" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="slashargumentswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unsupporteddbtablerowformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unoconvwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="libcurlwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfileformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfilepertable" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddblargeprefix" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="ishttpswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="incompleteunicodesupport" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="sixtyfourbitswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
@@ -40,8 +40,9 @@ if ($unregistration && \core\hub\registration::is_registered()) {
|
||||
if ($siteunregistrationform->is_cancelled()) {
|
||||
redirect(new moodle_url('/admin/registration/index.php'));
|
||||
} else if ($data = $siteunregistrationform->get_data()) {
|
||||
if (\core\hub\registration::unregister($data->unpublishalladvertisedcourses,
|
||||
$data->unpublishalluploadedcourses)) {
|
||||
\core\hub\registration::unregister($data->unpublishalladvertisedcourses,
|
||||
$data->unpublishalluploadedcourses);
|
||||
if (!\core\hub\registration::is_registered()) {
|
||||
redirect(new moodle_url('/admin/registration/index.php'));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1990,7 +1990,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
if (empty($CFG->docroot) or $environment_result->plugin) {
|
||||
$report = get_string($stringtouse, 'admin', $rec);
|
||||
} else {
|
||||
$report = $this->doc_link(join($linkparts, '/'), get_string($stringtouse, 'admin', $rec));
|
||||
$report = $this->doc_link(join($linkparts, '/'), get_string($stringtouse, 'admin', $rec), true);
|
||||
}
|
||||
// Enclose report text in div so feedback text will be displayed underneath it.
|
||||
$report = html_writer::div($report);
|
||||
|
||||
+111
-84
@@ -28,113 +28,139 @@ admin_externalpage_setup('searchareas');
|
||||
|
||||
$areaid = optional_param('areaid', null, PARAM_ALPHAEXT);
|
||||
$action = optional_param('action', null, PARAM_ALPHA);
|
||||
$indexingenabled = \core_search\manager::is_indexing_enabled(); // This restricts many of the actions on this page.
|
||||
|
||||
// Get a search manager instance, which we'll need for display and to handle some actions.
|
||||
try {
|
||||
$searchmanager = \core_search\manager::instance();
|
||||
} catch (core_search\engine_exception $searchmanagererror) {
|
||||
// Continue, we return an error later depending on the requested action.
|
||||
// In action cases, we'll throw this exception below. In non-action cases, we produce a lang string error.
|
||||
}
|
||||
|
||||
// Handle all the actions.
|
||||
if ($action) {
|
||||
|
||||
// If dealing with an areaid, we need to check that the area exists.
|
||||
if ($areaid) {
|
||||
// We need to check that the area exists.
|
||||
$area = \core_search\manager::get_search_area($areaid);
|
||||
if ($area === false) {
|
||||
throw new moodle_exception('invalidrequest');
|
||||
}
|
||||
}
|
||||
|
||||
if ($action !== 'enable' && $action !== 'disable') {
|
||||
// All actions but enable/disable need the search engine to be ready.
|
||||
if (!empty($searchmanagererror)) {
|
||||
throw $searchmanagererror;
|
||||
}
|
||||
// All the indexing actions.
|
||||
if (in_array($action, ['delete', 'indexall', 'reindexall', 'deleteall'])) {
|
||||
|
||||
// Show confirm prompt for all these actions as they may be inadvisable, or may cause
|
||||
// an interruption in search functionality, on production systems.
|
||||
if (!optional_param('confirm', 0, PARAM_INT)) {
|
||||
// Display confirmation prompt.
|
||||
$a = null;
|
||||
if ($areaid) {
|
||||
$a = html_writer::tag('strong', $area->get_visible_name());
|
||||
// All of these actions require that indexing is enabled.
|
||||
if ($indexingenabled) {
|
||||
|
||||
// For all of these actions, we strictly need a manager instance.
|
||||
if (isset($searchmanagererror)) {
|
||||
throw $searchmanagererror;
|
||||
}
|
||||
|
||||
$actionparams = ['sesskey' => sesskey(), 'action' => $action, 'confirm' => 1];
|
||||
if ($areaid) {
|
||||
$actionparams['areaid'] = $areaid;
|
||||
}
|
||||
$actionurl = new moodle_url('/admin/searchareas.php', $actionparams);
|
||||
$cancelurl = new moodle_url('/admin/searchareas.php');
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->confirm(get_string('confirm_' . $action, 'search', $a),
|
||||
// Show confirm prompt for all these actions as they may be inadvisable, or may cause
|
||||
// an interruption in search functionality, on production systems.
|
||||
if (!optional_param('confirm', 0, PARAM_INT)) {
|
||||
// Display confirmation prompt.
|
||||
$a = null;
|
||||
if ($areaid) {
|
||||
$a = html_writer::tag('strong', $area->get_visible_name());
|
||||
}
|
||||
|
||||
$actionparams = ['sesskey' => sesskey(), 'action' => $action, 'confirm' => 1];
|
||||
if ($areaid) {
|
||||
$actionparams['areaid'] = $areaid;
|
||||
}
|
||||
$actionurl = new moodle_url('/admin/searchareas.php', $actionparams);
|
||||
$cancelurl = new moodle_url('/admin/searchareas.php');
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->confirm(get_string('confirm_' . $action, 'search', $a),
|
||||
new single_button($actionurl, get_string('continue'), 'post', true),
|
||||
new single_button($cancelurl, get_string('cancel'), 'get'));
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
} else {
|
||||
// Confirmed, so run the required action.
|
||||
require_sesskey();
|
||||
|
||||
switch ($action) {
|
||||
case 'delete':
|
||||
$searchmanager->delete_index($areaid);
|
||||
\core\notification::success(get_string('searchindexdeleted', 'admin'));
|
||||
break;
|
||||
case 'indexall':
|
||||
$searchmanager->index();
|
||||
\core\notification::success(get_string('searchindexupdated', 'admin'));
|
||||
break;
|
||||
case 'reindexall':
|
||||
$searchmanager->index(true);
|
||||
\core\notification::success(get_string('searchreindexed', 'admin'));
|
||||
break;
|
||||
case 'deleteall':
|
||||
$searchmanager->delete_index();
|
||||
\core\notification::success(get_string('searchalldeleted', 'admin'));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Redirect back to the main page after taking action.
|
||||
redirect(new moodle_url('/admin/searchareas.php'));
|
||||
}
|
||||
}
|
||||
} else if (in_array($action, ['enable', 'disable'])) {
|
||||
// Toggling search areas requires no confirmation.
|
||||
require_sesskey();
|
||||
|
||||
switch ($action) {
|
||||
case 'enable':
|
||||
$area->set_enabled(true);
|
||||
\core\notification::success(get_string('searchareaenabled', 'admin'));
|
||||
break;
|
||||
case 'disable':
|
||||
$area->set_enabled(false);
|
||||
core\notification::success(get_string('searchareadisabled', 'admin'));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
redirect(new moodle_url('/admin/searchareas.php'));
|
||||
} else {
|
||||
// Invalid action.
|
||||
throw new moodle_exception('invalidaction');
|
||||
}
|
||||
}
|
||||
|
||||
// We are now taking an actual action, so require sesskey.
|
||||
require_sesskey();
|
||||
|
||||
switch ($action) {
|
||||
case 'enable':
|
||||
$area->set_enabled(true);
|
||||
\core\notification::add(get_string('searchareaenabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
|
||||
break;
|
||||
case 'disable':
|
||||
$area->set_enabled(false);
|
||||
\core\notification::add(get_string('searchareadisabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
|
||||
break;
|
||||
case 'delete':
|
||||
$search = \core_search\manager::instance();
|
||||
$search->delete_index($areaid);
|
||||
\core\notification::add(get_string('searchindexdeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
|
||||
break;
|
||||
case 'indexall':
|
||||
$searchmanager->index();
|
||||
\core\notification::add(get_string('searchindexupdated', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
|
||||
break;
|
||||
case 'reindexall':
|
||||
$searchmanager->index(true);
|
||||
\core\notification::add(get_string('searchreindexed', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
|
||||
break;
|
||||
case 'deleteall':
|
||||
$searchmanager->delete_index();
|
||||
\core\notification::add(get_string('searchalldeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
|
||||
break;
|
||||
default:
|
||||
throw new moodle_exception('invalidaction');
|
||||
break;
|
||||
}
|
||||
|
||||
// Redirect back to the main page after taking action.
|
||||
redirect(new moodle_url('/admin/searchareas.php'));
|
||||
// Display.
|
||||
if (isset($searchmanager) && $indexingenabled) {
|
||||
\core\notification::info(get_string('indexinginfo', 'admin'));
|
||||
} else if (isset($searchmanager)) {
|
||||
$params = (object) [
|
||||
'url' => (new moodle_url("/admin/settings.php?section=manageglobalsearch#admin-searchindexwhendisabled"))->out(false)
|
||||
];
|
||||
\core\notification::error(get_string('indexwhendisabledfullnotice', 'search', $params));
|
||||
} else {
|
||||
// In non-action cases, init errors are translated and displayed to the user as error notifications.
|
||||
$errorstr = get_string($searchmanagererror->errorcode, $searchmanagererror->module, $searchmanagererror->a);
|
||||
\core\notification::error($errorstr);
|
||||
}
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
$searchareas = \core_search\manager::get_search_areas_list();
|
||||
if (empty($searchmanagererror)) {
|
||||
$areasconfig = $searchmanager->get_areas_config($searchareas);
|
||||
} else {
|
||||
$areasconfig = false;
|
||||
}
|
||||
|
||||
if (!empty($searchmanagererror)) {
|
||||
$errorstr = get_string($searchmanagererror->errorcode, $searchmanagererror->module, $searchmanagererror->a);
|
||||
echo $OUTPUT->notification($errorstr, \core\output\notification::NOTIFY_ERROR);
|
||||
} else {
|
||||
echo $OUTPUT->notification(get_string('indexinginfo', 'admin'), \core\output\notification::NOTIFY_INFO);
|
||||
}
|
||||
|
||||
$table = new html_table();
|
||||
$table->id = 'core-search-areas';
|
||||
$table->head = [
|
||||
get_string('searcharea', 'search'),
|
||||
get_string('enable'),
|
||||
get_string('newestdocindexed', 'admin'),
|
||||
get_string('searchlastrun', 'admin'),
|
||||
get_string('searchindexactions', 'admin')
|
||||
];
|
||||
|
||||
$table->head = array(get_string('searcharea', 'search'), get_string('enable'), get_string('newestdocindexed', 'admin'),
|
||||
get_string('searchlastrun', 'admin'), get_string('searchindexactions', 'admin'));
|
||||
|
||||
$searchareas = \core_search\manager::get_search_areas_list();
|
||||
$areasconfig = isset($searchmanager) ? $searchmanager->get_areas_config($searchareas) : false;
|
||||
foreach ($searchareas as $area) {
|
||||
$areaid = $area->get_area_id();
|
||||
$columns = array(new html_table_cell($area->get_visible_name()));
|
||||
@@ -144,7 +170,7 @@ foreach ($searchareas as $area) {
|
||||
new pix_icon('t/hide', get_string('disable'), 'moodle', array('title' => '', 'class' => 'iconsmall')),
|
||||
null, array('title' => get_string('disable')));
|
||||
|
||||
if ($areasconfig) {
|
||||
if ($areasconfig && $indexingenabled) {
|
||||
$columns[] = $areasconfig[$areaid]->lastindexrun;
|
||||
|
||||
if ($areasconfig[$areaid]->indexingstart) {
|
||||
@@ -173,7 +199,11 @@ foreach ($searchareas as $area) {
|
||||
$columns[] = html_writer::alist($actions, ['class' => 'unstyled list-unstyled']);
|
||||
|
||||
} else {
|
||||
$blankrow = new html_table_cell(get_string('searchnotavailable', 'admin'));
|
||||
if (!$areasconfig) {
|
||||
$blankrow = new html_table_cell(get_string('searchnotavailable', 'admin'));
|
||||
} else {
|
||||
$blankrow = new html_table_cell(get_string('indexwhendisabledshortnotice', 'search'));
|
||||
}
|
||||
$blankrow->colspan = 3;
|
||||
$columns[] = $blankrow;
|
||||
}
|
||||
@@ -192,10 +222,7 @@ foreach ($searchareas as $area) {
|
||||
}
|
||||
|
||||
// Cross-search area tasks.
|
||||
$options = array();
|
||||
if (!empty($searchmanagererror)) {
|
||||
$options['disabled'] = true;
|
||||
}
|
||||
$options = (isset($searchmanager) && $indexingenabled) ? [] : ['disabled' => true];
|
||||
echo $OUTPUT->box_start('search-areas-actions');
|
||||
echo $OUTPUT->single_button(admin_searcharea_action_url('indexall'), get_string('searchupdateindex', 'admin'), 'get', $options);
|
||||
echo $OUTPUT->single_button(admin_searcharea_action_url('reindexall'), get_string('searchreindexindex', 'admin'), 'get', $options);
|
||||
@@ -204,7 +231,7 @@ echo $OUTPUT->box_end();
|
||||
|
||||
echo html_writer::table($table);
|
||||
|
||||
if (empty($searchmanagererror)) {
|
||||
if (isset($searchmanager)) {
|
||||
// Show information about queued index requests for specific contexts.
|
||||
$searchrenderer = $PAGE->get_renderer('core_search');
|
||||
echo $searchrenderer->render_index_requests_info($searchmanager->get_index_requests_info());
|
||||
|
||||
+2
-1
@@ -38,7 +38,8 @@ if (!($settingspage->check_access())) {
|
||||
$statusmsg = '';
|
||||
$errormsg = '';
|
||||
|
||||
if ($data = data_submitted() and confirm_sesskey()) {
|
||||
// Form is submitted with changed settings. Do not want to execute when modifying a block.
|
||||
if ($data = data_submitted() and confirm_sesskey() and isset($data->action) and $data->action == 'save-settings') {
|
||||
|
||||
$count = admin_write_settings($data);
|
||||
// Regardless of whether any setting change was written (a positive count), check validation errors for those that didn't.
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<div class="settingsform clearfix">
|
||||
{{#params}}
|
||||
<input type="hidden" name="{{name}}" value="{{value}}">
|
||||
<input type="hidden" name="action" value="save-settings">
|
||||
{{/params}}
|
||||
<input type="hidden" name="sesskey" value="{{sesskey}}">
|
||||
<input type="hidden" name="return" value="{{return}}">
|
||||
|
||||
@@ -43,10 +43,10 @@ $string['selectusers'] = 'Select users to assign role';
|
||||
$string['taskname'] = 'Sync cohort role assignments';
|
||||
$string['thisuserroles'] = 'Roles assigned relative to this user';
|
||||
$string['privacy:metadata:tool_cohortroles'] = 'The Cohort roles management plugin stores user cohort role mappings.';
|
||||
$string['privacy:metadata:tool_cohortroles:id'] = 'The ID of the cohort role mapping record.';
|
||||
$string['privacy:metadata:tool_cohortroles:cohortid'] = 'The ID of the cohort.';
|
||||
$string['privacy:metadata:tool_cohortroles:roleid'] = 'The ID of the role.';
|
||||
$string['privacy:metadata:tool_cohortroles:userid'] = 'The ID of the user.';
|
||||
$string['privacy:metadata:tool_cohortroles:timecreated'] = 'The date/time of when the cohort role mapping was created.';
|
||||
$string['privacy:metadata:tool_cohortroles:timemodified'] = 'The date/time of when the cohort role mapping was modified.';
|
||||
$string['privacy:metadata:tool_cohortroles:usermodified'] = 'The ID of the user who last modified the cohort role mapping.';
|
||||
$string['privacy:metadata:tool_cohortroles:id'] = 'The ID of the cohort role mapping record';
|
||||
$string['privacy:metadata:tool_cohortroles:cohortid'] = 'The ID of the cohort';
|
||||
$string['privacy:metadata:tool_cohortroles:roleid'] = 'The ID of the role';
|
||||
$string['privacy:metadata:tool_cohortroles:userid'] = 'The ID of the user';
|
||||
$string['privacy:metadata:tool_cohortroles:timecreated'] = 'The time when the cohort role mapping was created';
|
||||
$string['privacy:metadata:tool_cohortroles:timemodified'] = 'The time when the cohort role mapping was modified';
|
||||
$string['privacy:metadata:tool_cohortroles:usermodified'] = 'The ID of the user who last modified the cohort role mapping';
|
||||
|
||||
@@ -38,5 +38,8 @@ function xmldb_tool_customlang_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v3.4.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
// Automatically generated Moodle v3.5.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ $string['checkin'] = 'Save strings to language pack';
|
||||
$string['checkout'] = 'Open language pack for editing';
|
||||
$string['checkoutdone'] = 'Language pack loaded';
|
||||
$string['checkoutinprogress'] = 'Loading language pack';
|
||||
$string['confirmcheckin'] = 'You are about to save modifications to your local language pack. This will export the customised strings from the translator into you Moodle data directory and Moodle will start using the modified strings. Press \'Continue\' to proceed with saving.';
|
||||
$string['confirmcheckin'] = 'You are about to save modifications to your local language pack. This will export the customised strings from the translator into your site data directory and your site will start using the modified strings. Press \'Continue\' to proceed with saving.';
|
||||
$string['customlang:edit'] = 'Edit local translation';
|
||||
$string['customlang:view'] = 'View local translation';
|
||||
$string['filter'] = 'Filter strings';
|
||||
|
||||
@@ -134,11 +134,13 @@ class tool_customlang_renderer extends plugin_renderer_base {
|
||||
$output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'translatorsubmitted', 'value'=>1));
|
||||
$output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey()));
|
||||
$output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'p', 'value'=>$translator->currentpage));
|
||||
$save1 = html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'savecontinue', 'value'=>get_string('savecontinue', 'tool_customlang')));
|
||||
$save2 = html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'savecheckin', 'value'=>get_string('savecheckin', 'tool_customlang')));
|
||||
$output .= html_writer::tag('fieldset', $save1.$save2, array('class'=>'buttonsbar'));
|
||||
$save1 = html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'savecontinue',
|
||||
'value' => get_string('savecontinue', 'tool_customlang'), 'class' => 'btn btn-secondary'));
|
||||
$save2 = html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'savecheckin',
|
||||
'value' => get_string('savecheckin', 'tool_customlang'), 'class' => 'btn btn-secondary'));
|
||||
$output .= html_writer::tag('fieldset', $save1 . ' ' . $save2, array('class' => 'buttonsbar'));
|
||||
$output .= html_writer::table($table);
|
||||
$output .= html_writer::tag('fieldset', $save1.$save2, array('class'=>'buttonsbar'));
|
||||
$output .= html_writer::tag('fieldset', $save1 . ' ' . $save2, array('class' => 'buttonsbar'));
|
||||
$output .= html_writer::end_tag('div');
|
||||
$output .= html_writer::end_tag('form');
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f){var g={DELETE:'[data-action="deletecategory"]'},h=function(){this.registerEvents()};return h.prototype.registerEvents=function(){a(g.DELETE).click(function(g){g.preventDefault();var h=a(this).data("id"),i=a(this).data("name"),j=[{key:"deletecategory",component:"tool_dataprivacy",param:i},{key:"deletecategorytext",component:"tool_dataprivacy",param:i}];d.get_strings(j).then(function(d){var g=d[0],i=d[1];return e.create({title:g,body:i,type:e.types.SAVE_CANCEL}).then(function(d){return d.setSaveButtonText(g),d.getRoot().on(f.save,function(){var d={methodname:"tool_dataprivacy_delete_category",args:{id:h}};b.call([d])[0].done(function(b){b.result?a('tr[data-categoryid="'+h+'"]').remove():c.addNotification({message:b.warnings[0].message,type:"error"})}).fail(c.exception)}),d.getRoot().on(f.hidden,function(){d.destroy()}),d})}).done(function(a){a.show()}).fail(c.exception)})},{init:function(){return new h}}});
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f){var g={DELETE:'[data-action="deletecategory"]'},h=function(){this.registerEvents()};return h.prototype.registerEvents=function(){a(g.DELETE).click(function(g){g.preventDefault();var h=a(this).data("id"),i=a(this).data("name"),j=[{key:"deletecategory",component:"tool_dataprivacy"},{key:"deletecategorytext",component:"tool_dataprivacy",param:i},{key:"delete"}];d.get_strings(j).then(function(d){var g=d[0],i=d[1],j=d[2];return e.create({title:g,body:i,type:e.types.SAVE_CANCEL}).then(function(d){return d.setSaveButtonText(j),d.getRoot().on(f.save,function(){var d={methodname:"tool_dataprivacy_delete_category",args:{id:h}};b.call([d])[0].done(function(b){b.result?a('tr[data-categoryid="'+h+'"]').remove():c.addNotification({message:b.warnings[0].message,type:"error"})}).fail(c.exception)}),d.getRoot().on(f.hidden,function(){d.destroy()}),d})}).done(function(a){a.show()}).fail(c.exception)})},{init:function(){return new h}}});
|
||||
@@ -1 +1 @@
|
||||
define(["jquery","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","tool_dataprivacy/events"],function(a,b,c,d,e,f){var g=!1,h={APPROVE_BUTTON:'[data-action="approve"]',DENY_BUTTON:'[data-action="deny"]'},i=function(a){d.call(this,a),this.getFooter().find(h.APPROVE_BUTTON).length||b.exception({message:"No approve button found"}),this.getFooter().find(h.DENY_BUTTON).length||b.exception({message:"No deny button found"})};return i.TYPE="tool_dataprivacy-data_request",i.prototype=Object.create(d.prototype),i.prototype.constructor=i,i.prototype.registerEventListeners=function(){d.prototype.registerEventListeners.call(this),this.getModal().on(c.events.activate,h.APPROVE_BUTTON,function(b,c){var d=a.Event(f.approve);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this)),this.getModal().on(c.events.activate,h.DENY_BUTTON,function(b,c){var d=a.Event(f.deny);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this))},g||(e.register(i.TYPE,i,"tool_dataprivacy/data_request_modal"),g=!0),i});
|
||||
define(["jquery","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","tool_dataprivacy/events"],function(a,b,c,d,e,f){var g=!1,h={APPROVE_BUTTON:'[data-action="approve"]',DENY_BUTTON:'[data-action="deny"]',COMPLETE_BUTTON:'[data-action="complete"]'},i=function(a){d.call(this,a)};return i.TYPE="tool_dataprivacy-data_request",i.prototype=Object.create(d.prototype),i.prototype.constructor=i,i.prototype.registerEventListeners=function(){d.prototype.registerEventListeners.call(this),this.getModal().on(c.events.activate,h.APPROVE_BUTTON,function(b,c){var d=a.Event(f.approve);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this)),this.getModal().on(c.events.activate,h.DENY_BUTTON,function(b,c){var d=a.Event(f.deny);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this)),this.getModal().on(c.events.activate,h.COMPLETE_BUTTON,function(b,c){var d=a.Event(f.complete);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this))},g||(e.register(i.TYPE,i,"tool_dataprivacy/data_request_modal"),g=!0),i});
|
||||
+1
-1
@@ -1 +1 @@
|
||||
define([],function(){return{approve:"tool_dataprivacy-data_request:approve",deny:"tool_dataprivacy-data_request:deny"}});
|
||||
define([],function(){return{approve:"tool_dataprivacy-data_request:approve",deny:"tool_dataprivacy-data_request:deny",complete:"tool_dataprivacy-data_request:complete"}});
|
||||
+1
-1
@@ -1 +1 @@
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f){var g={DELETE:'[data-action="deletepurpose"]'},h=function(){this.registerEvents()};return h.prototype.registerEvents=function(){a(g.DELETE).click(function(g){g.preventDefault();var h=a(this).data("id"),i=a(this).data("name"),j=[{key:"deletepurpose",component:"tool_dataprivacy",param:i},{key:"deletepurposetext",component:"tool_dataprivacy",param:i}];d.get_strings(j).then(function(d){var g=d[0],i=d[1];return e.create({title:g,body:i,type:e.types.SAVE_CANCEL}).then(function(d){return d.setSaveButtonText(g),d.getRoot().on(f.save,function(){var d={methodname:"tool_dataprivacy_delete_purpose",args:{id:h}};b.call([d])[0].done(function(b){b.result?a('tr[data-purposeid="'+h+'"]').remove():c.addNotification({message:b.warnings[0].message,type:"error"})}).fail(c.exception)}),d.getRoot().on(f.hidden,function(){d.destroy()}),d})}).done(function(a){a.show()}).fail(c.exception)})},{init:function(){return new h}}});
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f){var g={DELETE:'[data-action="deletepurpose"]'},h=function(){this.registerEvents()};return h.prototype.registerEvents=function(){a(g.DELETE).click(function(g){g.preventDefault();var h=a(this).data("id"),i=a(this).data("name"),j=[{key:"deletepurpose",component:"tool_dataprivacy"},{key:"deletepurposetext",component:"tool_dataprivacy",param:i},{key:"delete"}];d.get_strings(j).then(function(d){var g=d[0],i=d[1],j=d[2];return e.create({title:g,body:i,type:e.types.SAVE_CANCEL}).then(function(d){return d.setSaveButtonText(j),d.getRoot().on(f.save,function(){var d={methodname:"tool_dataprivacy_delete_purpose",args:{id:h}};b.call([d])[0].done(function(b){b.result?a('tr[data-purposeid="'+h+'"]').remove():c.addNotification({message:b.warnings[0].message,type:"error"})}).fail(c.exception)}),d.getRoot().on(f.hidden,function(){d.destroy()}),d})}).done(function(a){a.show()}).fail(c.exception)})},{init:function(){return new h}}});
|
||||
@@ -0,0 +1 @@
|
||||
define(["jquery","core/form-autocomplete","core/str","core/notification"],function(a,b,c,d){var e={REQUEST_FILTERS:"#request-filters"},f=function(){var f=[{key:"filter",component:"moodle"},{key:"nofiltersapplied",component:"moodle"}];c.get_strings(f).then(function(a){var c=a[0],d=a[1];return b.enhance(e.REQUEST_FILTERS,!1,"",c,!1,!0,d,!0)}).fail(d.exception);var g=a(e.REQUEST_FILTERS).val();a(e.REQUEST_FILTERS).on("change",function(){var b=a(this).val();g.join(",")!==b.join(",")&&(0===b.length&&a("#filters-cleared").val(1),a(this.form).submit())})};return{init:function(){f()}}});
|
||||
+1
-1
@@ -1 +1 @@
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates","tool_dataprivacy/data_request_modal","tool_dataprivacy/events"],function(a,b,c,d,e,f,g,h,i){function j(a,g){var h=[],j="";switch(a){case i.approve:h=[{key:"approverequest",component:"tool_dataprivacy"},{key:"confirmapproval",component:"tool_dataprivacy"}],j="tool_dataprivacy_approve_data_request";break;case i.deny:h=[{key:"denyrequest",component:"tool_dataprivacy"},{key:"confirmdenial",component:"tool_dataprivacy"}],j="tool_dataprivacy_deny_data_request"}var k="";d.get_strings(h).then(function(a){k=a[0];var b=a[1];return e.create({title:k,body:b,type:e.types.SAVE_CANCEL})}).then(function(a){return a.setSaveButtonText(k),a.getRoot().on(f.save,function(){var a={requestid:g},d={methodname:j,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)}var k={APPROVE_REQUEST:'[data-action="approve"]',DENY_REQUEST:'[data-action="deny"]',VIEW_REQUEST:'[data-action="view"]'},l=function(){this.registerEvents()};return l.prototype.registerEvents=function(){a(k.VIEW_REQUEST).click(function(d){d.preventDefault();var k=a(this).data("requestid"),l={requestid:k},m={methodname:"tool_dataprivacy_get_data_request",args:l},n=b.call([m]),o="",p=e.types.DEFAULT;a.when(n[0]).then(function(a){return a.result?(2==a.result.status&&(p=h.TYPE),o=a.result.typename,g.render("tool_dataprivacy/request_details",a.result)):(c.addNotification({message:a.warnings[0].message,type:"error"}),!1)}).then(function(a){return e.create({title:o,body:a,type:p,large:!0}).then(function(a){return a.getRoot().on(i.approve,function(){j(i.approve,k)}),a.getRoot().on(i.deny,function(){j(i.deny,k)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a})}).done(function(a){a.show()}).fail(c.exception)}),a(k.APPROVE_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.approve,c)}),a(k.DENY_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.deny,c)})},l});
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates","tool_dataprivacy/data_request_modal","tool_dataprivacy/events"],function(a,b,c,d,e,f,g,h,i){function j(a,b){var g=[],h="",j={requestid:b};switch(a){case i.approve:g=[{key:"approverequest",component:"tool_dataprivacy"},{key:"confirmapproval",component:"tool_dataprivacy"}],h="tool_dataprivacy_approve_data_request";break;case i.deny:g=[{key:"denyrequest",component:"tool_dataprivacy"},{key:"confirmdenial",component:"tool_dataprivacy"}],h="tool_dataprivacy_deny_data_request";break;case i.complete:g=[{key:"markcomplete",component:"tool_dataprivacy"},{key:"confirmcompletion",component:"tool_dataprivacy"}],h="tool_dataprivacy_mark_complete"}var l="";d.get_strings(g).then(function(a){l=a[0];var b=a[1];return e.create({title:l,body:b,type:e.types.SAVE_CANCEL})}).then(function(a){a.setSaveButtonText(l),a.getRoot().on(f.save,function(){k(h,j)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a.show()})["catch"](c.exception)}function k(a,d){var e={methodname:a,args:d};b.call([e])[0].done(function(a){a.result?window.location.reload():c.addNotification({message:a.warnings[0].message,type:"error"})}).fail(c.exception)}var l={APPROVE_REQUEST:'[data-action="approve"]',DENY_REQUEST:'[data-action="deny"]',VIEW_REQUEST:'[data-action="view"]',MARK_COMPLETE:'[data-action="complete"]'},m=function(){this.registerEvents()};return m.prototype.registerEvents=function(){a(l.VIEW_REQUEST).click(function(d){d.preventDefault();var l=a(this).data("requestid"),m={requestid:l},n={methodname:"tool_dataprivacy_get_data_request",args:m},o=b.call([n]);a.when(o[0]).then(function(a){return a.result?a.result:(c.addNotification({message:a.warnings[0].message,type:"error"}),!1)}).then(function(a){var b=g.render("tool_dataprivacy/request_details",a),c={approvedeny:a.approvedeny,canmarkcomplete:a.canmarkcomplete};return e.create({title:a.typename,body:b,type:h.TYPE,large:!0,templateContext:c})}).then(function(a){a.getRoot().on(i.approve,function(){j(i.approve,l)}),a.getRoot().on(i.deny,function(){j(i.deny,l)}),a.getRoot().on(i.complete,function(){var a={requestid:l};k("tool_dataprivacy_mark_complete",a)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a.show()})["catch"](c.exception)}),a(l.APPROVE_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.approve,c)}),a(l.DENY_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.deny,c)}),a(l.MARK_COMPLETE).click(function(b){b.preventDefault(),j(i.complete,a(this).data("requestid"))})},m});
|
||||
@@ -58,25 +58,28 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents) {
|
||||
var stringkeys = [
|
||||
{
|
||||
key: 'deletecategory',
|
||||
component: 'tool_dataprivacy',
|
||||
param: categoryname
|
||||
component: 'tool_dataprivacy'
|
||||
},
|
||||
{
|
||||
key: 'deletecategorytext',
|
||||
component: 'tool_dataprivacy',
|
||||
param: categoryname
|
||||
},
|
||||
{
|
||||
key: 'delete'
|
||||
}
|
||||
];
|
||||
|
||||
Str.get_strings(stringkeys).then(function(langStrings) {
|
||||
var title = langStrings[0];
|
||||
var confirmMessage = langStrings[1];
|
||||
var buttonText = langStrings[2];
|
||||
return ModalFactory.create({
|
||||
title: title,
|
||||
body: confirmMessage,
|
||||
type: ModalFactory.types.SAVE_CANCEL
|
||||
}).then(function(modal) {
|
||||
modal.setSaveButtonText(title);
|
||||
modal.setSaveButtonText(buttonText);
|
||||
|
||||
// Handle save event.
|
||||
modal.getRoot().on(ModalEvents.save, function() {
|
||||
|
||||
@@ -234,6 +234,7 @@ define(['jquery', 'core/str', 'core/ajax', 'core/notification', 'core/templates'
|
||||
},
|
||||
fail: Notification.exception
|
||||
}]);
|
||||
return;
|
||||
}).catch(Notification.exception);
|
||||
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/m
|
||||
var SELECTORS = {
|
||||
APPROVE_BUTTON: '[data-action="approve"]',
|
||||
DENY_BUTTON: '[data-action="deny"]',
|
||||
COMPLETE_BUTTON: '[data-action="complete"]'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -38,14 +39,6 @@ define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/m
|
||||
*/
|
||||
var ModalDataRequest = function(root) {
|
||||
Modal.call(this, root);
|
||||
|
||||
if (!this.getFooter().find(SELECTORS.APPROVE_BUTTON).length) {
|
||||
Notification.exception({message: 'No approve button found'});
|
||||
}
|
||||
|
||||
if (!this.getFooter().find(SELECTORS.DENY_BUTTON).length) {
|
||||
Notification.exception({message: 'No deny button found'});
|
||||
}
|
||||
};
|
||||
|
||||
ModalDataRequest.TYPE = 'tool_dataprivacy-data_request';
|
||||
@@ -80,6 +73,16 @@ define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/m
|
||||
data.originalEvent.preventDefault();
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.getModal().on(CustomEvents.events.activate, SELECTORS.COMPLETE_BUTTON, function(e, data) {
|
||||
var completeEvent = $.Event(DataPrivacyEvents.complete);
|
||||
this.getRoot().trigger(completeEvent, this);
|
||||
|
||||
if (!completeEvent.isDefaultPrevented()) {
|
||||
this.hide();
|
||||
data.originalEvent.preventDefault();
|
||||
}
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
// Automatically register with the modal registry the first time this module is imported so that you can create modals
|
||||
|
||||
@@ -26,5 +26,6 @@ define([], function() {
|
||||
return {
|
||||
approve: 'tool_dataprivacy-data_request:approve',
|
||||
deny: 'tool_dataprivacy-data_request:deny',
|
||||
complete: 'tool_dataprivacy-data_request:complete'
|
||||
};
|
||||
});
|
||||
|
||||
@@ -34,7 +34,6 @@ define(['jquery', 'core/url', 'core/str'], function($, url, str) {
|
||||
*
|
||||
* @param {object} targetnode The node that we want to expand / collapse
|
||||
* @param {object} thisnode The node that was clicked.
|
||||
* @return {null}
|
||||
*/
|
||||
expandCollapse: function(targetnode, thisnode) {
|
||||
if (targetnode.hasClass('hide')) {
|
||||
@@ -58,7 +57,6 @@ define(['jquery', 'core/url', 'core/str'], function($, url, str) {
|
||||
* Expand or collapse all nodes on this page.
|
||||
*
|
||||
* @param {string} nextstate The next state to change to.
|
||||
* @return {null}
|
||||
*/
|
||||
expandCollapseAll: function(nextstate) {
|
||||
var currentstate = (nextstate == 'visible') ? 'hide' : 'visible';
|
||||
@@ -75,6 +73,7 @@ define(['jquery', 'core/url', 'core/str'], function($, url, str) {
|
||||
|
||||
str.get_string(currentstate, 'tool_dataprivacy').then(function(langString) {
|
||||
$('.tool_dataprivacy-expand-all').html(langString);
|
||||
return;
|
||||
}).catch(Notification.exception);
|
||||
|
||||
$(':header i.fa').each(function() {
|
||||
|
||||
@@ -58,25 +58,28 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents) {
|
||||
var stringkeys = [
|
||||
{
|
||||
key: 'deletepurpose',
|
||||
component: 'tool_dataprivacy',
|
||||
param: purposename
|
||||
component: 'tool_dataprivacy'
|
||||
},
|
||||
{
|
||||
key: 'deletepurposetext',
|
||||
component: 'tool_dataprivacy',
|
||||
param: purposename
|
||||
},
|
||||
{
|
||||
key: 'delete'
|
||||
}
|
||||
];
|
||||
|
||||
Str.get_strings(stringkeys).then(function(langStrings) {
|
||||
var title = langStrings[0];
|
||||
var confirmMessage = langStrings[1];
|
||||
var buttonText = langStrings[2];
|
||||
return ModalFactory.create({
|
||||
title: title,
|
||||
body: confirmMessage,
|
||||
type: ModalFactory.types.SAVE_CANCEL
|
||||
}).then(function(modal) {
|
||||
modal.setSaveButtonText(title);
|
||||
modal.setSaveButtonText(buttonText);
|
||||
|
||||
// Handle save event.
|
||||
modal.getRoot().on(ModalEvents.save, function() {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* JS module for the data requests filter.
|
||||
*
|
||||
* @module tool_dataprivacy/request_filter
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'], function($, Autocomplete, Str, Notification) {
|
||||
|
||||
/**
|
||||
* Selectors.
|
||||
*
|
||||
* @access private
|
||||
* @type {{REQUEST_FILTERS: string}}
|
||||
*/
|
||||
var SELECTORS = {
|
||||
REQUEST_FILTERS: '#request-filters'
|
||||
};
|
||||
|
||||
/**
|
||||
* Init function.
|
||||
*
|
||||
* @method init
|
||||
* @private
|
||||
*/
|
||||
var init = function() {
|
||||
var stringkeys = [
|
||||
{
|
||||
key: 'filter',
|
||||
component: 'moodle'
|
||||
},
|
||||
{
|
||||
key: 'nofiltersapplied',
|
||||
component: 'moodle'
|
||||
}
|
||||
];
|
||||
|
||||
Str.get_strings(stringkeys).then(function(langstrings) {
|
||||
var placeholder = langstrings[0];
|
||||
var noSelectionString = langstrings[1];
|
||||
return Autocomplete.enhance(SELECTORS.REQUEST_FILTERS, false, '', placeholder, false, true, noSelectionString, true);
|
||||
}).fail(Notification.exception);
|
||||
|
||||
var last = $(SELECTORS.REQUEST_FILTERS).val();
|
||||
$(SELECTORS.REQUEST_FILTERS).on('change', function() {
|
||||
var current = $(this).val();
|
||||
// Prevent form from submitting unnecessarily, eg. on blur when no filter is selected.
|
||||
if (last.join(',') !== current.join(',')) {
|
||||
// If we're submitting without filters, set the hidden input 'filters-cleared' to 1.
|
||||
if (current.length === 0) {
|
||||
$('#filters-cleared').val(1);
|
||||
}
|
||||
$(this.form).submit();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return /** @alias module:core/form-autocomplete */ {
|
||||
/**
|
||||
* Initialise the unified user filter.
|
||||
*
|
||||
* @method init
|
||||
*/
|
||||
init: function() {
|
||||
init();
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -39,11 +39,13 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
* @type {{APPROVE_REQUEST: string}}
|
||||
* @type {{DENY_REQUEST: string}}
|
||||
* @type {{VIEW_REQUEST: string}}
|
||||
* @type {{MARK_COMPLETE: string}}
|
||||
*/
|
||||
var ACTIONS = {
|
||||
APPROVE_REQUEST: '[data-action="approve"]',
|
||||
DENY_REQUEST: '[data-action="deny"]',
|
||||
VIEW_REQUEST: '[data-action="view"]'
|
||||
VIEW_REQUEST: '[data-action="view"]',
|
||||
MARK_COMPLETE: '[data-action="complete"]'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -73,16 +75,9 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
};
|
||||
|
||||
var promises = Ajax.call([request]);
|
||||
var modalTitle = '';
|
||||
var modalType = ModalFactory.types.DEFAULT;
|
||||
$.when(promises[0]).then(function(data) {
|
||||
if (data.result) {
|
||||
// Check if the status is awaiting approval.
|
||||
if (data.result.status == 2) {
|
||||
modalType = ModalDataRequest.TYPE;
|
||||
}
|
||||
modalTitle = data.result.typename;
|
||||
return Templates.render('tool_dataprivacy/request_details', data.result);
|
||||
return data.result;
|
||||
}
|
||||
// Fail.
|
||||
Notification.addNotification({
|
||||
@@ -91,35 +86,51 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
});
|
||||
return false;
|
||||
|
||||
}).then(function(html) {
|
||||
}).then(function(data) {
|
||||
var body = Templates.render('tool_dataprivacy/request_details', data);
|
||||
var templateContext = {
|
||||
approvedeny: data.approvedeny,
|
||||
canmarkcomplete: data.canmarkcomplete
|
||||
};
|
||||
return ModalFactory.create({
|
||||
title: modalTitle,
|
||||
body: html,
|
||||
type: modalType,
|
||||
large: true
|
||||
}).then(function(modal) {
|
||||
// Handle approve event.
|
||||
modal.getRoot().on(DataPrivacyEvents.approve, function() {
|
||||
showConfirmation(DataPrivacyEvents.approve, requestId);
|
||||
});
|
||||
|
||||
// Handle deny event.
|
||||
modal.getRoot().on(DataPrivacyEvents.deny, function() {
|
||||
showConfirmation(DataPrivacyEvents.deny, requestId);
|
||||
});
|
||||
|
||||
// Handle hidden event.
|
||||
modal.getRoot().on(ModalEvents.hidden, function() {
|
||||
// Destroy when hidden.
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
return modal;
|
||||
title: data.typename,
|
||||
body: body,
|
||||
type: ModalDataRequest.TYPE,
|
||||
large: true,
|
||||
templateContext: templateContext
|
||||
});
|
||||
}).done(function(modal) {
|
||||
|
||||
}).then(function(modal) {
|
||||
// Handle approve event.
|
||||
modal.getRoot().on(DataPrivacyEvents.approve, function() {
|
||||
showConfirmation(DataPrivacyEvents.approve, requestId);
|
||||
});
|
||||
|
||||
// Handle deny event.
|
||||
modal.getRoot().on(DataPrivacyEvents.deny, function() {
|
||||
showConfirmation(DataPrivacyEvents.deny, requestId);
|
||||
});
|
||||
|
||||
// Handle send event.
|
||||
modal.getRoot().on(DataPrivacyEvents.complete, function() {
|
||||
var params = {
|
||||
'requestid': requestId
|
||||
};
|
||||
handleSave('tool_dataprivacy_mark_complete', params);
|
||||
});
|
||||
|
||||
// Handle hidden event.
|
||||
modal.getRoot().on(ModalEvents.hidden, function() {
|
||||
// Destroy when hidden.
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
// Show the modal!
|
||||
modal.show();
|
||||
}).fail(Notification.exception);
|
||||
|
||||
return;
|
||||
|
||||
}).catch(Notification.exception);
|
||||
});
|
||||
|
||||
$(ACTIONS.APPROVE_REQUEST).click(function(e) {
|
||||
@@ -135,6 +146,11 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
var requestId = $(this).data('requestid');
|
||||
showConfirmation(DataPrivacyEvents.deny, requestId);
|
||||
});
|
||||
|
||||
$(ACTIONS.MARK_COMPLETE).click(function(e) {
|
||||
e.preventDefault();
|
||||
showConfirmation(DataPrivacyEvents.complete, $(this).data('requestid'));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -146,6 +162,9 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
function showConfirmation(action, requestId) {
|
||||
var keys = [];
|
||||
var wsfunction = '';
|
||||
var params = {
|
||||
'requestid': requestId
|
||||
};
|
||||
switch (action) {
|
||||
case DataPrivacyEvents.approve:
|
||||
keys = [
|
||||
@@ -173,6 +192,19 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
];
|
||||
wsfunction = 'tool_dataprivacy_deny_data_request';
|
||||
break;
|
||||
case DataPrivacyEvents.complete:
|
||||
keys = [
|
||||
{
|
||||
key: 'markcomplete',
|
||||
component: 'tool_dataprivacy'
|
||||
},
|
||||
{
|
||||
key: 'confirmcompletion',
|
||||
component: 'tool_dataprivacy'
|
||||
}
|
||||
];
|
||||
wsfunction = 'tool_dataprivacy_mark_complete';
|
||||
break;
|
||||
}
|
||||
|
||||
var modalTitle = '';
|
||||
@@ -189,26 +221,7 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
|
||||
// Handle save event.
|
||||
modal.getRoot().on(ModalEvents.save, function() {
|
||||
// Confirm the request.
|
||||
var params = {
|
||||
'requestid': requestId
|
||||
};
|
||||
|
||||
var request = {
|
||||
methodname: wsfunction,
|
||||
args: params
|
||||
};
|
||||
|
||||
Ajax.call([request])[0].done(function(data) {
|
||||
if (data.result) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
Notification.addNotification({
|
||||
message: data.warnings[0].message,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}).fail(Notification.exception);
|
||||
handleSave(wsfunction, params);
|
||||
});
|
||||
|
||||
// Handle hidden event.
|
||||
@@ -217,9 +230,39 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
return modal;
|
||||
}).done(function(modal) {
|
||||
modal.show();
|
||||
|
||||
return;
|
||||
|
||||
}).catch(Notification.exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a web service function and reloads the page on success and shows a notification.
|
||||
* Displays an error notification, otherwise.
|
||||
*
|
||||
* @param {String} wsfunction The web service function to call.
|
||||
* @param {Object} params The parameters for the web service functoon.
|
||||
*/
|
||||
function handleSave(wsfunction, params) {
|
||||
// Confirm the request.
|
||||
var request = {
|
||||
methodname: wsfunction,
|
||||
args: params
|
||||
};
|
||||
|
||||
Ajax.call([request])[0].done(function(data) {
|
||||
if (data.result) {
|
||||
// On success, reload the page so that the data request table will be updated.
|
||||
// TODO: Probably in the future, better to reload the table or the target data request via AJAX.
|
||||
window.location.reload();
|
||||
} else {
|
||||
// Add the notification.
|
||||
Notification.addNotification({
|
||||
message: data.warnings[0].message,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}).fail(Notification.exception);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$url = new moodle_url("/admin/tool/dataprivacy/categories.php");
|
||||
$title = get_string('editcategories', 'tool_dataprivacy');
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
namespace tool_dataprivacy;
|
||||
|
||||
use coding_exception;
|
||||
use context_course;
|
||||
use context_system;
|
||||
use core\invalid_persistent_exception;
|
||||
use core\message\message;
|
||||
@@ -75,7 +76,7 @@ class api {
|
||||
/** The request is now being processed. */
|
||||
const DATAREQUEST_STATUS_PROCESSING = 4;
|
||||
|
||||
/** Data request completed. */
|
||||
/** Information/other request completed. */
|
||||
const DATAREQUEST_STATUS_COMPLETE = 5;
|
||||
|
||||
/** Data request cancelled by the user. */
|
||||
@@ -84,6 +85,15 @@ class api {
|
||||
/** Data request rejected by the DPO. */
|
||||
const DATAREQUEST_STATUS_REJECTED = 7;
|
||||
|
||||
/** Data request download ready. */
|
||||
const DATAREQUEST_STATUS_DOWNLOAD_READY = 8;
|
||||
|
||||
/** Data request expired. */
|
||||
const DATAREQUEST_STATUS_EXPIRED = 9;
|
||||
|
||||
/** Data delete request completed, account is removed. */
|
||||
const DATAREQUEST_STATUS_DELETED = 10;
|
||||
|
||||
/**
|
||||
* Determines whether the user can contact the site's Data Protection Officer via Moodle.
|
||||
*
|
||||
@@ -126,6 +136,25 @@ class api {
|
||||
require_capability('tool/dataprivacy:managedataregistry', $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the role shortnames of Data Protection Officer roles.
|
||||
*
|
||||
* @return array An array of the DPO role shortnames
|
||||
*/
|
||||
public static function get_dpo_role_names() : array {
|
||||
global $DB;
|
||||
|
||||
$dporoleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
|
||||
$dponames = array();
|
||||
|
||||
if (!empty($dporoleids)) {
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($dporoleids);
|
||||
$dponames = $DB->get_fieldset_select('role', 'shortname', "id {$insql}", $inparams);
|
||||
}
|
||||
|
||||
return $dponames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of users with the Data Protection Officer role.
|
||||
*
|
||||
@@ -197,7 +226,7 @@ class api {
|
||||
} else {
|
||||
// If not a DPO, only users with the capability to make data requests for the user should be allowed.
|
||||
// (e.g. users with the Parent role, etc).
|
||||
if (!api::can_create_data_request_for_user($foruser)) {
|
||||
if (!self::can_create_data_request_for_user($foruser)) {
|
||||
$forusercontext = \context_user::instance($foruser);
|
||||
throw new required_capability_exception($forusercontext,
|
||||
'tool/dataprivacy:makedatarequestsforchildren', 'nopermissions', '');
|
||||
@@ -232,16 +261,42 @@ class api {
|
||||
* (e.g. Users with the Data Protection Officer roles)
|
||||
*
|
||||
* @param int $userid The User ID.
|
||||
* @param int[] $statuses The status filters.
|
||||
* @param int[] $types The request type filters.
|
||||
* @param string $sort The order by clause.
|
||||
* @param int $offset Amount of records to skip.
|
||||
* @param int $limit Amount of records to fetch.
|
||||
* @return data_request[]
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public static function get_data_requests($userid = 0) {
|
||||
public static function get_data_requests($userid = 0, $statuses = [], $types = [], $sort = '', $offset = 0, $limit = 0) {
|
||||
global $DB, $USER;
|
||||
$results = [];
|
||||
$sort = 'status ASC, timemodified ASC';
|
||||
$sqlparams = [];
|
||||
$sqlconditions = [];
|
||||
|
||||
// Set default sort.
|
||||
if (empty($sort)) {
|
||||
$sort = 'status ASC, timemodified ASC';
|
||||
}
|
||||
|
||||
// Set status filters.
|
||||
if (!empty($statuses)) {
|
||||
list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
|
||||
$sqlconditions[] = "status $statusinsql";
|
||||
}
|
||||
|
||||
// Set request type filter.
|
||||
if (!empty($types)) {
|
||||
list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
|
||||
$sqlconditions[] = "type $typeinsql";
|
||||
$sqlparams = array_merge($sqlparams, $typeparams);
|
||||
}
|
||||
|
||||
if ($userid) {
|
||||
// Get the data requests for the user or data requests made by the user.
|
||||
$select = "(userid = :userid OR requestedby = :requestedby)";
|
||||
$sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
|
||||
$params = [
|
||||
'userid' => $userid,
|
||||
'requestedby' => $userid
|
||||
@@ -256,20 +311,99 @@ class api {
|
||||
$alloweduserids = array_merge($alloweduserids, array_keys($children));
|
||||
}
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
|
||||
$select .= " AND userid $insql";
|
||||
$params = array_merge($params, $inparams);
|
||||
$sqlconditions[] .= "userid $insql";
|
||||
$select = implode(' AND ', $sqlconditions);
|
||||
$params = array_merge($params, $inparams, $sqlparams);
|
||||
|
||||
$results = data_request::get_records_select($select, $params, $sort);
|
||||
$results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit);
|
||||
} else {
|
||||
// If the current user is one of the site's Data Protection Officers, then fetch all data requests.
|
||||
if (self::is_site_dpo($USER->id)) {
|
||||
$results = data_request::get_records(null, $sort, '');
|
||||
if (!empty($sqlconditions)) {
|
||||
$select = implode(' AND ', $sqlconditions);
|
||||
$results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit);
|
||||
} else {
|
||||
$results = data_request::get_records(null, $sort, '', $offset, $limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If any are due to expire, expire them and re-fetch updated data.
|
||||
if (empty($statuses)
|
||||
|| in_array(self::DATAREQUEST_STATUS_DOWNLOAD_READY, $statuses)
|
||||
|| in_array(self::DATAREQUEST_STATUS_EXPIRED, $statuses)) {
|
||||
$expiredrequests = data_request::get_expired_requests($userid);
|
||||
|
||||
if (!empty($expiredrequests)) {
|
||||
data_request::expire($expiredrequests);
|
||||
$results = self::get_data_requests($userid, $statuses, $types, $sort, $offset, $limit);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the count of data request records based on the given parameters.
|
||||
*
|
||||
* @param int $userid The User ID.
|
||||
* @param int[] $statuses The status filters.
|
||||
* @param int[] $types The request type filters.
|
||||
* @return int
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public static function get_data_requests_count($userid = 0, $statuses = [], $types = []) {
|
||||
global $DB, $USER;
|
||||
$count = 0;
|
||||
$sqlparams = [];
|
||||
$sqlconditions = [];
|
||||
if (!empty($statuses)) {
|
||||
list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
|
||||
$sqlconditions[] = "status $statusinsql";
|
||||
}
|
||||
if (!empty($types)) {
|
||||
list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
|
||||
$sqlconditions[] = "type $typeinsql";
|
||||
$sqlparams = array_merge($sqlparams, $typeparams);
|
||||
}
|
||||
if ($userid) {
|
||||
// Get the data requests for the user or data requests made by the user.
|
||||
$sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
|
||||
$params = [
|
||||
'userid' => $userid,
|
||||
'requestedby' => $userid
|
||||
];
|
||||
|
||||
// Build a list of user IDs that the user is allowed to make data requests for.
|
||||
// Of course, the user should be included in this list.
|
||||
$alloweduserids = [$userid];
|
||||
// Get any users that the user can make data requests for.
|
||||
if ($children = helper::get_children_of_user($userid)) {
|
||||
// Get the list of user IDs of the children and merge to the allowed user IDs.
|
||||
$alloweduserids = array_merge($alloweduserids, array_keys($children));
|
||||
}
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
|
||||
$sqlconditions[] .= "userid $insql";
|
||||
$select = implode(' AND ', $sqlconditions);
|
||||
$params = array_merge($params, $inparams, $sqlparams);
|
||||
|
||||
$count = data_request::count_records_select($select, $params);
|
||||
} else {
|
||||
// If the current user is one of the site's Data Protection Officers, then fetch all data requests.
|
||||
if (self::is_site_dpo($USER->id)) {
|
||||
if (!empty($sqlconditions)) {
|
||||
$select = implode(' AND ', $sqlconditions);
|
||||
$count = data_request::count_records_select($select, $sqlparams);
|
||||
} else {
|
||||
$count = data_request::count_records();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
|
||||
*
|
||||
@@ -287,6 +421,9 @@ class api {
|
||||
self::DATAREQUEST_STATUS_COMPLETE,
|
||||
self::DATAREQUEST_STATUS_CANCELLED,
|
||||
self::DATAREQUEST_STATUS_REJECTED,
|
||||
self::DATAREQUEST_STATUS_DOWNLOAD_READY,
|
||||
self::DATAREQUEST_STATUS_EXPIRED,
|
||||
self::DATAREQUEST_STATUS_DELETED,
|
||||
];
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED);
|
||||
$select = 'type = :type AND userid = :userid AND status NOT ' . $insql;
|
||||
@@ -310,6 +447,9 @@ class api {
|
||||
self::DATAREQUEST_STATUS_COMPLETE,
|
||||
self::DATAREQUEST_STATUS_CANCELLED,
|
||||
self::DATAREQUEST_STATUS_REJECTED,
|
||||
self::DATAREQUEST_STATUS_DOWNLOAD_READY,
|
||||
self::DATAREQUEST_STATUS_EXPIRED,
|
||||
self::DATAREQUEST_STATUS_DELETED,
|
||||
];
|
||||
|
||||
return !in_array($status, $finalstatuses);
|
||||
@@ -333,7 +473,22 @@ class api {
|
||||
if ($dpoid) {
|
||||
$datarequest->set('dpo', $dpoid);
|
||||
}
|
||||
$datarequest->set('dpocomment', $comment);
|
||||
// Update the comment if necessary.
|
||||
if (!empty(trim($comment))) {
|
||||
$params = [
|
||||
'date' => userdate(time()),
|
||||
'comment' => $comment
|
||||
];
|
||||
$commenttosave = get_string('datecomment', 'tool_dataprivacy', $params);
|
||||
// Check if there's an existing DPO comment.
|
||||
$currentcomment = trim($datarequest->get('dpocomment'));
|
||||
if ($currentcomment) {
|
||||
// Append the new comment to the current comment and give them 1 line space in between.
|
||||
$commenttosave = $currentcomment . PHP_EOL . PHP_EOL . $commenttosave;
|
||||
}
|
||||
$datarequest->set('dpocomment', $commenttosave);
|
||||
}
|
||||
|
||||
return $datarequest->update();
|
||||
}
|
||||
|
||||
@@ -428,7 +583,6 @@ class api {
|
||||
* @param data_request $request The data request
|
||||
* @return int|false
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function notify_dpo($dpo, data_request $request) {
|
||||
@@ -500,6 +654,54 @@ class api {
|
||||
return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user can download a data request.
|
||||
*
|
||||
* @param int $userid Target user id (subject of data request)
|
||||
* @param int $requesterid Requester user id (person who requsted it)
|
||||
* @param int|null $downloaderid Person who wants to download user id (default current)
|
||||
* @return bool
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) {
|
||||
global $USER;
|
||||
|
||||
if (!$downloaderid) {
|
||||
$downloaderid = $USER->id;
|
||||
}
|
||||
|
||||
$usercontext = \context_user::instance($userid);
|
||||
// If it's your own and you have the right capability, you can download it.
|
||||
if ($userid == $downloaderid && has_capability('tool/dataprivacy:downloadownrequest', $usercontext, $downloaderid)) {
|
||||
return true;
|
||||
}
|
||||
// If you can download anyone's in that context, you can download it.
|
||||
if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) {
|
||||
return true;
|
||||
}
|
||||
// If you can have the 'child access' ability to request in that context, and you are the one
|
||||
// who requested it, then you can download it.
|
||||
if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an action menu link to download a data request.
|
||||
*
|
||||
* @param \context_user $usercontext User context (of user who the data is for)
|
||||
* @param int $requestid Request id
|
||||
* @return \action_menu_link_secondary Action menu link
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public static function get_download_link(\context_user $usercontext, $requestid) {
|
||||
$downloadurl = moodle_url::make_pluginfile_url($usercontext->id,
|
||||
'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true);
|
||||
$downloadtext = get_string('download', 'tool_dataprivacy');
|
||||
return new \action_menu_link_secondary($downloadurl, null, $downloadtext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new data purpose.
|
||||
*
|
||||
|
||||
@@ -289,7 +289,8 @@ class data_registry {
|
||||
* @param int $forcedcategoryvalue Use this value as if this was this context level category.
|
||||
* @return int[]
|
||||
*/
|
||||
public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false, $forcedcategoryvalue = false) {
|
||||
public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false,
|
||||
$forcedcategoryvalue = false) {
|
||||
|
||||
list($purposeid, $categoryid) = self::get_defaults($contextlevel);
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@ class data_request extends persistent {
|
||||
api::DATAREQUEST_STATUS_COMPLETE,
|
||||
api::DATAREQUEST_STATUS_CANCELLED,
|
||||
api::DATAREQUEST_STATUS_REJECTED,
|
||||
api::DATAREQUEST_STATUS_DOWNLOAD_READY,
|
||||
api::DATAREQUEST_STATUS_EXPIRED,
|
||||
api::DATAREQUEST_STATUS_DELETED,
|
||||
],
|
||||
'type' => PARAM_INT
|
||||
],
|
||||
@@ -110,4 +113,101 @@ class data_request extends persistent {
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a completed data export request has expired.
|
||||
* The response will be valid regardless of the expiry scheduled task having run.
|
||||
*
|
||||
* @param data_request $request the data request object whose expiry will be checked.
|
||||
* @return bool true if the request has expired.
|
||||
*/
|
||||
public static function is_expired(data_request $request) {
|
||||
$result = false;
|
||||
|
||||
// Only export requests expire.
|
||||
if ($request->get('type') == api::DATAREQUEST_TYPE_EXPORT) {
|
||||
switch ($request->get('status')) {
|
||||
// Expired requests are obviously expired.
|
||||
case api::DATAREQUEST_STATUS_EXPIRED:
|
||||
$result = true;
|
||||
break;
|
||||
// Complete requests are expired if the expiry time has elapsed.
|
||||
case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
|
||||
$expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry');
|
||||
if ($expiryseconds > 0 && time() >= ($request->get('timemodified') + $expiryseconds)) {
|
||||
$result = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Fetch completed data requests which are due to expire.
|
||||
*
|
||||
* @param int $userid Optional user ID to filter by.
|
||||
*
|
||||
* @return array Details of completed requests which are due to expire.
|
||||
*/
|
||||
public static function get_expired_requests($userid = 0) {
|
||||
global $DB;
|
||||
|
||||
$expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry');
|
||||
$expirytime = strtotime("-{$expiryseconds} second");
|
||||
$table = self::TABLE;
|
||||
$sqlwhere = 'type = :export_type AND status = :completestatus AND timemodified <= :expirytime';
|
||||
$params = array(
|
||||
'export_type' => api::DATAREQUEST_TYPE_EXPORT,
|
||||
'completestatus' => api::DATAREQUEST_STATUS_DOWNLOAD_READY,
|
||||
'expirytime' => $expirytime,
|
||||
);
|
||||
$sort = 'id';
|
||||
$fields = 'id, userid';
|
||||
|
||||
// Filter by user ID if specified.
|
||||
if ($userid > 0) {
|
||||
$sqlwhere .= ' AND (userid = :userid OR requestedby = :requestedby)';
|
||||
$params['userid'] = $userid;
|
||||
$params['requestedby'] = $userid;
|
||||
}
|
||||
|
||||
return $DB->get_records_select_menu($table, $sqlwhere, $params, $sort, $fields, 0, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expire a given set of data requests.
|
||||
* Update request status and delete the files.
|
||||
*
|
||||
* @param array $expiredrequests [requestid => userid]
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function expire($expiredrequests) {
|
||||
global $DB;
|
||||
|
||||
$ids = array_keys($expiredrequests);
|
||||
|
||||
if (count($ids) > 0) {
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($ids);
|
||||
$initialparams = array(api::DATAREQUEST_STATUS_EXPIRED, time());
|
||||
$params = array_merge($initialparams, $inparams);
|
||||
|
||||
$update = "UPDATE {" . self::TABLE . "}
|
||||
SET status = ?, timemodified = ?
|
||||
WHERE id $insql";
|
||||
|
||||
if ($DB->execute($update, $params)) {
|
||||
$fs = get_file_storage();
|
||||
|
||||
foreach ($expiredrequests as $id => $userid) {
|
||||
$usercontext = \context_user::instance($userid);
|
||||
$fs->delete_area_files($usercontext->id, 'tool_dataprivacy', 'export', $id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,7 @@
|
||||
*/
|
||||
namespace tool_dataprivacy;
|
||||
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\purpose;
|
||||
use tool_dataprivacy\context_instance;
|
||||
use tool_dataprivacy\data_registry;
|
||||
use core_privacy\manager;
|
||||
use tool_dataprivacy\expired_context;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
@@ -90,7 +87,7 @@ abstract class expired_contexts_manager {
|
||||
return $numprocessed;
|
||||
}
|
||||
|
||||
$privacymanager = new \core_privacy\manager();
|
||||
$privacymanager = new manager();
|
||||
$privacymanager->set_observer(new \tool_dataprivacy\manager_observer());
|
||||
|
||||
foreach ($this->get_context_levels() as $level) {
|
||||
@@ -118,11 +115,11 @@ abstract class expired_contexts_manager {
|
||||
/**
|
||||
* Deletes user data from the provided context.
|
||||
*
|
||||
* @param \core_privacy\manager $privacymanager
|
||||
* @param \tool_dataprivacy\expired_context $expiredctx
|
||||
* @param manager $privacymanager
|
||||
* @param expired_context $expiredctx
|
||||
* @return \context|false
|
||||
*/
|
||||
protected function delete_expired_context(\core_privacy\manager $privacymanager, \tool_dataprivacy\expired_context $expiredctx) {
|
||||
protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
|
||||
|
||||
$context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
|
||||
if (!$context) {
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
*/
|
||||
namespace tool_dataprivacy;
|
||||
|
||||
use tool_dataprivacy\purpose;
|
||||
use tool_dataprivacy\context_instance;
|
||||
use core_privacy\manager;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -110,11 +109,11 @@ class expired_user_contexts extends \tool_dataprivacy\expired_contexts_manager {
|
||||
*
|
||||
* Overwritten to delete the user.
|
||||
*
|
||||
* @param \core_privacy\manager $privacymanager
|
||||
* @param \tool_dataprivacy\expired_context $expiredctx
|
||||
* @param manager $privacymanager
|
||||
* @param expired_context $expiredctx
|
||||
* @return \context|false
|
||||
*/
|
||||
protected function delete_expired_context(\core_privacy\manager $privacymanager, \tool_dataprivacy\expired_context $expiredctx) {
|
||||
protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
|
||||
$context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
|
||||
if (!$context) {
|
||||
api::delete_expired_context($expiredctx->get('contextid'));
|
||||
|
||||
@@ -31,6 +31,7 @@ use context_helper;
|
||||
use context_system;
|
||||
use context_user;
|
||||
use core\invalid_persistent_exception;
|
||||
use core\notification;
|
||||
use core_user;
|
||||
use dml_exception;
|
||||
use external_api;
|
||||
@@ -144,7 +145,7 @@ class external extends external_api {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny a data request.
|
||||
* Make a general enquiry to a DPO.
|
||||
*
|
||||
* @since Moodle 3.5
|
||||
* @param string $message The message to be sent to the DPO.
|
||||
@@ -210,7 +211,7 @@ class external extends external_api {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter description for deny_data_request().
|
||||
* Parameter description for contact_dpo().
|
||||
*
|
||||
* @since Moodle 3.5
|
||||
* @return external_description
|
||||
@@ -222,6 +223,70 @@ class external extends external_api {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter description for mark_complete().
|
||||
*
|
||||
* @since Moodle 3.5.2
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function mark_complete_parameters() {
|
||||
return new external_function_parameters([
|
||||
'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a user's general enquiry's status as complete.
|
||||
*
|
||||
* @since Moodle 3.5.2
|
||||
* @param int $requestid The request ID of the general enquiry.
|
||||
* @return array
|
||||
* @throws coding_exception
|
||||
* @throws invalid_parameter_exception
|
||||
* @throws invalid_persistent_exception
|
||||
* @throws restricted_context_exception
|
||||
* @throws dml_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function mark_complete($requestid) {
|
||||
global $USER;
|
||||
|
||||
$warnings = [];
|
||||
$params = external_api::validate_parameters(self::mark_complete_parameters(), [
|
||||
'requestid' => $requestid,
|
||||
]);
|
||||
$requestid = $params['requestid'];
|
||||
|
||||
// Validate context.
|
||||
$context = context_system::instance();
|
||||
self::validate_context($context);
|
||||
|
||||
$message = get_string('markedcomplete', 'tool_dataprivacy');
|
||||
// Update the data request record.
|
||||
if ($result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE, $USER->id, $message)) {
|
||||
// Add notification in the session to be shown when the page is reloaded on the JS side.
|
||||
notification::success(get_string('requestmarkedcomplete', 'tool_dataprivacy'));
|
||||
}
|
||||
|
||||
return [
|
||||
'result' => $result,
|
||||
'warnings' => $warnings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter description for mark_complete().
|
||||
*
|
||||
* @since Moodle 3.5.2
|
||||
* @return external_description
|
||||
*/
|
||||
public static function mark_complete_returns() {
|
||||
return new external_single_structure([
|
||||
'result' => new external_value(PARAM_BOOL, 'The processing result'),
|
||||
'warnings' => new external_warnings()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter description for get_data_request().
|
||||
*
|
||||
@@ -258,9 +323,9 @@ class external extends external_api {
|
||||
// Validate context.
|
||||
$context = context_system::instance();
|
||||
self::validate_context($context);
|
||||
$requestpersistent = new data_request($requestid);
|
||||
require_capability('tool/dataprivacy:managedatarequests', $context);
|
||||
|
||||
$requestpersistent = new data_request($requestid);
|
||||
$exporter = new data_request_exporter($requestpersistent, ['context' => $context]);
|
||||
$renderer = $PAGE->get_renderer('tool_dataprivacy');
|
||||
$result = $exporter->export($renderer);
|
||||
@@ -326,6 +391,9 @@ class external extends external_api {
|
||||
$result = false;
|
||||
if ($requestexists) {
|
||||
$result = api::approve_data_request($requestid);
|
||||
|
||||
// Add notification in the session to be shown when the page is reloaded on the JS side.
|
||||
notification::success(get_string('requestapproved', 'tool_dataprivacy'));
|
||||
} else {
|
||||
$warnings[] = [
|
||||
'item' => $requestid,
|
||||
@@ -395,6 +463,9 @@ class external extends external_api {
|
||||
$result = false;
|
||||
if ($requestexists) {
|
||||
$result = api::deny_data_request($requestid);
|
||||
|
||||
// Add notification in the session to be shown when the page is reloaded on the JS side.
|
||||
notification::success(get_string('requestdenied', 'tool_dataprivacy'));
|
||||
} else {
|
||||
$warnings[] = [
|
||||
'item' => $requestid,
|
||||
|
||||
@@ -102,6 +102,16 @@ class data_request_exporter extends persistent_exporter {
|
||||
'optional' => true,
|
||||
'default' => false
|
||||
],
|
||||
'approvedeny' => [
|
||||
'type' => PARAM_BOOL,
|
||||
'optional' => true,
|
||||
'default' => false
|
||||
],
|
||||
'canmarkcomplete' => [
|
||||
'type' => PARAM_BOOL,
|
||||
'optional' => true,
|
||||
'default' => false
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -140,14 +150,19 @@ class data_request_exporter extends persistent_exporter {
|
||||
|
||||
$values['messagehtml'] = text_to_html($this->persistent->get('comments'));
|
||||
|
||||
$values['typename'] = helper::get_request_type_string($this->persistent->get('type'));
|
||||
$values['typenameshort'] = helper::get_shortened_request_type_string($this->persistent->get('type'));
|
||||
$requesttype = $this->persistent->get('type');
|
||||
$values['typename'] = helper::get_request_type_string($requesttype);
|
||||
$values['typenameshort'] = helper::get_shortened_request_type_string($requesttype);
|
||||
|
||||
$values['canreview'] = false;
|
||||
$values['approvedeny'] = false;
|
||||
$values['statuslabel'] = helper::get_request_status_string($this->persistent->get('status'));
|
||||
|
||||
switch ($this->persistent->get('status')) {
|
||||
case api::DATAREQUEST_STATUS_PENDING:
|
||||
$values['statuslabelclass'] = 'label-default';
|
||||
$values['statuslabelclass'] = 'label-info';
|
||||
// Request can be manually completed for general enquiry requests.
|
||||
$values['canmarkcomplete'] = $requesttype == api::DATAREQUEST_TYPE_OTHERS;
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_PREPROCESSING:
|
||||
$values['statuslabelclass'] = 'label-default';
|
||||
@@ -156,6 +171,8 @@ class data_request_exporter extends persistent_exporter {
|
||||
$values['statuslabelclass'] = 'label-info';
|
||||
// DPO can review the request once it's ready.
|
||||
$values['canreview'] = true;
|
||||
// Whether the DPO can approve or deny the request.
|
||||
$values['approvedeny'] = in_array($requesttype, [api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_TYPE_DELETE]);
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_APPROVED:
|
||||
$values['statuslabelclass'] = 'label-info';
|
||||
@@ -164,6 +181,8 @@ class data_request_exporter extends persistent_exporter {
|
||||
$values['statuslabelclass'] = 'label-info';
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_COMPLETE:
|
||||
case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
|
||||
case api::DATAREQUEST_STATUS_DELETED:
|
||||
$values['statuslabelclass'] = 'label-success';
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_CANCELLED:
|
||||
@@ -172,6 +191,9 @@ class data_request_exporter extends persistent_exporter {
|
||||
case api::DATAREQUEST_STATUS_REJECTED:
|
||||
$values['statuslabelclass'] = 'label-important';
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_EXPIRED:
|
||||
$values['statuslabelclass'] = 'label-default';
|
||||
break;
|
||||
}
|
||||
|
||||
return $values;
|
||||
|
||||
@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\data_registry;
|
||||
use tool_dataprivacy\purpose;
|
||||
|
||||
/**
|
||||
* Context instance data form.
|
||||
@@ -186,12 +187,12 @@ class context_instance extends \core\form\persistent {
|
||||
/**
|
||||
* Returns the purpose display text.
|
||||
*
|
||||
* @param \tool_dataprivacy\purpose $effectivepurpose
|
||||
* @param purpose $effectivepurpose
|
||||
* @param int $retentioncontextlevel
|
||||
* @param \context $context The context, just for displaying (filters) purposes.
|
||||
* @return string
|
||||
*/
|
||||
protected static function get_retention_display_text(\tool_dataprivacy\purpose $effectivepurpose, $retentioncontextlevel, \context $context) {
|
||||
protected static function get_retention_display_text(purpose $effectivepurpose, $retentioncontextlevel, \context $context) {
|
||||
global $PAGE;
|
||||
|
||||
$renderer = $PAGE->get_renderer('tool_dataprivacy');
|
||||
|
||||
@@ -35,6 +35,17 @@ use tool_dataprivacy\api;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class helper {
|
||||
/** The default number of results to be shown per page. */
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
/** Filter constant associated with the request type filter. */
|
||||
const FILTER_TYPE = 1;
|
||||
|
||||
/** Filter constant associated with the request status filter. */
|
||||
const FILTER_STATUS = 2;
|
||||
|
||||
/** The request filters preference key. */
|
||||
const PREF_REQUEST_FILTERS = 'tool_dataprivacy_request-filters';
|
||||
|
||||
/**
|
||||
* Retrieves the human-readable text value of a data request type.
|
||||
@@ -45,16 +56,11 @@ class helper {
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function get_request_type_string($requesttype) {
|
||||
switch ($requesttype) {
|
||||
case api::DATAREQUEST_TYPE_EXPORT:
|
||||
return get_string('requesttypeexport', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_TYPE_DELETE:
|
||||
return get_string('requesttypedelete', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_TYPE_OTHERS:
|
||||
return get_string('requesttypeothers', 'tool_dataprivacy');
|
||||
default:
|
||||
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
|
||||
$types = self::get_request_types();
|
||||
if (!isset($types[$requesttype])) {
|
||||
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
|
||||
}
|
||||
return $types[$requesttype];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,16 +72,37 @@ class helper {
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function get_shortened_request_type_string($requesttype) {
|
||||
switch ($requesttype) {
|
||||
case api::DATAREQUEST_TYPE_EXPORT:
|
||||
return get_string('requesttypeexportshort', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_TYPE_DELETE:
|
||||
return get_string('requesttypedeleteshort', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_TYPE_OTHERS:
|
||||
return get_string('requesttypeothersshort', 'tool_dataprivacy');
|
||||
default:
|
||||
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
|
||||
$types = self::get_request_types_short();
|
||||
if (!isset($types[$requesttype])) {
|
||||
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
|
||||
}
|
||||
return $types[$requesttype];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key value-pairs of request type code and their string value.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_request_types() {
|
||||
return [
|
||||
api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexport', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedelete', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothers', 'tool_dataprivacy'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key value-pairs of request type code and their shortened string value.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_request_types_short() {
|
||||
return [
|
||||
api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexportshort', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedeleteshort', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothersshort', 'tool_dataprivacy'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,30 +110,36 @@ class helper {
|
||||
*
|
||||
* @param int $status The request status.
|
||||
* @return string
|
||||
* @throws coding_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function get_request_status_string($status) {
|
||||
switch ($status) {
|
||||
case api::DATAREQUEST_STATUS_PENDING:
|
||||
return get_string('statuspending', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_PREPROCESSING:
|
||||
return get_string('statuspreprocessing', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_AWAITING_APPROVAL:
|
||||
return get_string('statusawaitingapproval', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_APPROVED:
|
||||
return get_string('statusapproved', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_PROCESSING:
|
||||
return get_string('statusprocessing', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_COMPLETE:
|
||||
return get_string('statuscomplete', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_CANCELLED:
|
||||
return get_string('statuscancelled', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_REJECTED:
|
||||
return get_string('statusrejected', 'tool_dataprivacy');
|
||||
default:
|
||||
throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
|
||||
$statuses = self::get_request_statuses();
|
||||
if (!isset($statuses[$status])) {
|
||||
throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
return $statuses[$status];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key value-pairs of request status code and string value.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_request_statuses() {
|
||||
return [
|
||||
api::DATAREQUEST_STATUS_PENDING => get_string('statuspending', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_PREPROCESSING => get_string('statuspreprocessing', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_AWAITING_APPROVAL => get_string('statusawaitingapproval', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_APPROVED => get_string('statusapproved', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_PROCESSING => get_string('statusprocessing', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_COMPLETE => get_string('statuscomplete', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_DOWNLOAD_READY => get_string('statusready', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_EXPIRED => get_string('statusexpired', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_CANCELLED => get_string('statuscancelled', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_REJECTED => get_string('statusrejected', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_DELETED => get_string('statusdeleted', 'tool_dataprivacy'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +165,7 @@ class helper {
|
||||
'contextlevel' => CONTEXT_USER
|
||||
];
|
||||
|
||||
// The final list of users that we will return;
|
||||
// The final list of users that we will return.
|
||||
$finalresults = [];
|
||||
|
||||
// Our prospective list of users.
|
||||
@@ -146,4 +179,34 @@ class helper {
|
||||
}
|
||||
return $finalresults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get options for the data requests filter.
|
||||
*
|
||||
* @return array
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public static function get_request_filter_options() {
|
||||
$filters = [
|
||||
self::FILTER_TYPE => (object)[
|
||||
'name' => get_string('requesttype', 'tool_dataprivacy'),
|
||||
'options' => self::get_request_types_short()
|
||||
],
|
||||
self::FILTER_STATUS => (object)[
|
||||
'name' => get_string('requeststatus', 'tool_dataprivacy'),
|
||||
'options' => self::get_request_statuses()
|
||||
],
|
||||
];
|
||||
$options = [];
|
||||
foreach ($filters as $category => $filtercategory) {
|
||||
foreach ($filtercategory->options as $key => $name) {
|
||||
$option = (object)[
|
||||
'category' => $filtercategory->name,
|
||||
'name' => $name
|
||||
];
|
||||
$options["{$category}:{$key}"] = get_string('filteroption', 'tool_dataprivacy', $option);
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,8 @@ class metadata_registry {
|
||||
$internaldata['compliant'] = false;
|
||||
}
|
||||
// Check to see if we are an external plugin.
|
||||
$componentshortname = explode('_', $component);
|
||||
// Plugin names can contain _ characters, limit to 2 to just remove initial plugintype.
|
||||
$componentshortname = explode('_', $component, 2);
|
||||
$shortname = array_pop($componentshortname);
|
||||
if (isset($contributedplugins[$plugintype][$shortname])) {
|
||||
$internaldata['external'] = true;
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace tool_dataprivacy\output;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use coding_exception;
|
||||
use dml_exception;
|
||||
use moodle_exception;
|
||||
use moodle_url;
|
||||
use renderable;
|
||||
@@ -34,7 +33,7 @@ use single_select;
|
||||
use stdClass;
|
||||
use templatable;
|
||||
use tool_dataprivacy\data_request;
|
||||
use tool_dataprivacy\output\expired_contexts_table;
|
||||
use tool_dataprivacy\local\helper;
|
||||
|
||||
/**
|
||||
* Class containing data for a user's data requests.
|
||||
@@ -44,9 +43,6 @@ use tool_dataprivacy\output\expired_contexts_table;
|
||||
*/
|
||||
class data_deletion_page implements renderable, templatable {
|
||||
|
||||
/** The default number of results to be shown per page. */
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
/** @var data_request[] $requests List of data requests. */
|
||||
protected $filter = null;
|
||||
|
||||
@@ -57,7 +53,7 @@ class data_deletion_page implements renderable, templatable {
|
||||
* Construct this renderable.
|
||||
*
|
||||
* @param \tool_dataprivacy\data_request[] $filter
|
||||
* @param \tool_dataprivacy\expired_contexts_table $expiredcontextstable
|
||||
* @param expired_contexts_table $expiredcontextstable
|
||||
*/
|
||||
public function __construct($filter, expired_contexts_table $expiredcontextstable) {
|
||||
$this->filter = $filter;
|
||||
@@ -70,7 +66,6 @@ class data_deletion_page implements renderable, templatable {
|
||||
* @param renderer_base $output
|
||||
* @return stdClass
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public function export_for_template(renderer_base $output) {
|
||||
@@ -87,7 +82,7 @@ class data_deletion_page implements renderable, templatable {
|
||||
$data->filter = $filterselector->export_for_template($output);
|
||||
|
||||
ob_start();
|
||||
$this->expiredcontextstable->out(self::DEFAULT_PAGE_SIZE, true);
|
||||
$this->expiredcontextstable->out(helper::DEFAULT_PAGE_SIZE, true);
|
||||
$expiredcontexts = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$data->expiredcontexts = $expiredcontexts;
|
||||
|
||||
@@ -24,20 +24,17 @@
|
||||
namespace tool_dataprivacy\output;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use action_menu;
|
||||
use action_menu_link_secondary;
|
||||
use coding_exception;
|
||||
use context_system;
|
||||
use dml_exception;
|
||||
use moodle_exception;
|
||||
use moodle_url;
|
||||
use renderable;
|
||||
use renderer_base;
|
||||
use single_select;
|
||||
use stdClass;
|
||||
use templatable;
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\data_request;
|
||||
use tool_dataprivacy\external\data_request_exporter;
|
||||
use tool_dataprivacy\local\helper;
|
||||
|
||||
/**
|
||||
* Class containing data for a user's data requests.
|
||||
@@ -47,16 +44,21 @@ use tool_dataprivacy\external\data_request_exporter;
|
||||
*/
|
||||
class data_requests_page implements renderable, templatable {
|
||||
|
||||
/** @var data_request[] $requests List of data requests. */
|
||||
protected $requests = [];
|
||||
/** @var data_requests_table $table The data requests table. */
|
||||
protected $table;
|
||||
|
||||
/** @var int[] $filters The applied filters. */
|
||||
protected $filters = [];
|
||||
|
||||
/**
|
||||
* Construct this renderable.
|
||||
*
|
||||
* @param data_request[] $requests
|
||||
* @param data_requests_table $table The data requests table.
|
||||
* @param int[] $filters The applied filters.
|
||||
*/
|
||||
public function __construct($requests) {
|
||||
$this->requests = $requests;
|
||||
public function __construct($table, $filters) {
|
||||
$this->table = $table;
|
||||
$this->filters = $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,43 +80,17 @@ class data_requests_page implements renderable, templatable {
|
||||
$data->httpsite = array('message' => $httpwarningmessage, 'announce' => 1);
|
||||
}
|
||||
|
||||
$requests = [];
|
||||
foreach ($this->requests as $request) {
|
||||
$requestid = $request->get('id');
|
||||
$status = $request->get('status');
|
||||
$requestexporter = new data_request_exporter($request, ['context' => context_system::instance()]);
|
||||
$item = $requestexporter->export($output);
|
||||
$url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
|
||||
$filteroptions = helper::get_request_filter_options();
|
||||
$filter = new request_filter($filteroptions, $this->filters, $url);
|
||||
$data->filter = $filter->export_for_template($output);
|
||||
|
||||
// Prepare actions.
|
||||
$actions = [];
|
||||
ob_start();
|
||||
$this->table->out(helper::DEFAULT_PAGE_SIZE, true);
|
||||
$requests = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
// View action.
|
||||
$actionurl = new moodle_url('#');
|
||||
$actiondata = ['data-action' => 'view', 'data-requestid' => $requestid];
|
||||
$actiontext = get_string('viewrequest', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
|
||||
if ($status == api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
|
||||
// Approve.
|
||||
$actiondata['data-action'] = 'approve';
|
||||
$actiontext = get_string('approverequest', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
|
||||
// Deny.
|
||||
$actiondata['data-action'] = 'deny';
|
||||
$actiontext = get_string('denyrequest', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
}
|
||||
|
||||
$actionsmenu = new action_menu($actions);
|
||||
$actionsmenu->set_menu_trigger(get_string('actions'));
|
||||
$actionsmenu->set_owner_selector('request-actions-' . $requestid);
|
||||
$actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
|
||||
$item->actions = $actionsmenu->export_for_template($output);
|
||||
|
||||
$requests[] = $item;
|
||||
}
|
||||
$data->requests = $requests;
|
||||
$data->datarequests = $requests;
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Contains the class used for the displaying the data requests table.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Jun Pataleta <jun@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace tool_dataprivacy\output;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->libdir . '/tablelib.php');
|
||||
|
||||
use action_menu;
|
||||
use action_menu_link_secondary;
|
||||
use coding_exception;
|
||||
use dml_exception;
|
||||
use html_writer;
|
||||
use moodle_url;
|
||||
use stdClass;
|
||||
use table_sql;
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\external\data_request_exporter;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
/**
|
||||
* The class for displaying the data requests table.
|
||||
*
|
||||
* @copyright 2018 Jun Pataleta <jun@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class data_requests_table extends table_sql {
|
||||
|
||||
/** @var int The user ID. */
|
||||
protected $userid = 0;
|
||||
|
||||
/** @var int[] The status filters. */
|
||||
protected $statuses = [];
|
||||
|
||||
/** @var int[] The request type filters. */
|
||||
protected $types = [];
|
||||
|
||||
/** @var bool Whether this table is being rendered for managing data requests. */
|
||||
protected $manage = false;
|
||||
|
||||
/** @var \tool_dataprivacy\data_request[] Array of data request persistents. */
|
||||
protected $datarequests = [];
|
||||
|
||||
/**
|
||||
* data_requests_table constructor.
|
||||
*
|
||||
* @param int $userid The user ID
|
||||
* @param int[] $statuses
|
||||
* @param int[] $types
|
||||
* @param bool $manage
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function __construct($userid = 0, $statuses = [], $types = [], $manage = false) {
|
||||
parent::__construct('data-requests-table');
|
||||
|
||||
$this->userid = $userid;
|
||||
$this->statuses = $statuses;
|
||||
$this->types = $types;
|
||||
$this->manage = $manage;
|
||||
|
||||
$columnheaders = [
|
||||
'type' => get_string('requesttype', 'tool_dataprivacy'),
|
||||
'userid' => get_string('user', 'tool_dataprivacy'),
|
||||
'timecreated' => get_string('daterequested', 'tool_dataprivacy'),
|
||||
'requestedby' => get_string('requestby', 'tool_dataprivacy'),
|
||||
'status' => get_string('requeststatus', 'tool_dataprivacy'),
|
||||
'comments' => get_string('message', 'tool_dataprivacy'),
|
||||
'actions' => '',
|
||||
];
|
||||
|
||||
$this->define_columns(array_keys($columnheaders));
|
||||
$this->define_headers(array_values($columnheaders));
|
||||
$this->no_sorting('actions');
|
||||
}
|
||||
|
||||
/**
|
||||
* The type column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_type($data) {
|
||||
if ($this->manage) {
|
||||
return $data->typenameshort;
|
||||
}
|
||||
return $data->typename;
|
||||
}
|
||||
|
||||
/**
|
||||
* The user column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return mixed
|
||||
*/
|
||||
public function col_userid($data) {
|
||||
$user = $data->foruser;
|
||||
return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The context information column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_timecreated($data) {
|
||||
return userdate($data->timecreated);
|
||||
}
|
||||
|
||||
/**
|
||||
* The requesting user's column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return mixed
|
||||
*/
|
||||
public function col_requestedby($data) {
|
||||
$user = $data->requestedbyuser;
|
||||
return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The status column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return mixed
|
||||
*/
|
||||
public function col_status($data) {
|
||||
return html_writer::span($data->statuslabel, 'label ' . $data->statuslabelclass);
|
||||
}
|
||||
|
||||
/**
|
||||
* The comments column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_comments($data) {
|
||||
return shorten_text($data->comments, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* The actions column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_actions($data) {
|
||||
global $OUTPUT;
|
||||
|
||||
$requestid = $data->id;
|
||||
$status = $data->status;
|
||||
|
||||
// Prepare actions.
|
||||
$actions = [];
|
||||
|
||||
// View action.
|
||||
$actionurl = new moodle_url('#');
|
||||
$actiondata = ['data-action' => 'view', 'data-requestid' => $requestid];
|
||||
$actiontext = get_string('viewrequest', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
|
||||
switch ($status) {
|
||||
case api::DATAREQUEST_STATUS_PENDING:
|
||||
// Add action to mark a general enquiry request as complete.
|
||||
if ($data->type == api::DATAREQUEST_TYPE_OTHERS) {
|
||||
$actiondata['data-action'] = 'complete';
|
||||
$nameemail = (object)[
|
||||
'name' => $data->foruser->fullname,
|
||||
'email' => $data->foruser->email
|
||||
];
|
||||
$actiondata['data-requestid'] = $data->id;
|
||||
$actiondata['data-replytoemail'] = get_string('nameemail', 'tool_dataprivacy', $nameemail);
|
||||
$actiontext = get_string('markcomplete', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
}
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_AWAITING_APPROVAL:
|
||||
// Approve.
|
||||
$actiondata['data-action'] = 'approve';
|
||||
$actiontext = get_string('approverequest', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
|
||||
// Deny.
|
||||
$actiondata['data-action'] = 'deny';
|
||||
$actiontext = get_string('denyrequest', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
|
||||
$userid = $data->foruser->id;
|
||||
$usercontext = \context_user::instance($userid, IGNORE_MISSING);
|
||||
// If user has permission to view download link, show relevant action item.
|
||||
if ($usercontext && api::can_download_data_request_for_user($userid, $data->requestedbyuser->id)) {
|
||||
$actions[] = api::get_download_link($usercontext, $requestid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$actionsmenu = new action_menu($actions);
|
||||
$actionsmenu->set_menu_trigger(get_string('actions'));
|
||||
$actionsmenu->set_owner_selector('request-actions-' . $requestid);
|
||||
$actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
|
||||
$actionsmenu->set_constraint('[data-region=data-requests-table] > .no-overflow');
|
||||
|
||||
return $OUTPUT->render($actionsmenu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the database for results to display in the table.
|
||||
*
|
||||
* @param int $pagesize size of page for paginated displayed table.
|
||||
* @param bool $useinitialsbar do you want to use the initials bar.
|
||||
* @throws dml_exception
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function query_db($pagesize, $useinitialsbar = true) {
|
||||
global $PAGE;
|
||||
|
||||
// Set dummy page total until we fetch full result set.
|
||||
$this->pagesize($pagesize, $pagesize + 1);
|
||||
|
||||
$sort = $this->get_sql_sort();
|
||||
|
||||
// Get data requests from the given conditions.
|
||||
$datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types, $sort,
|
||||
$this->get_page_start(), $this->get_page_size());
|
||||
|
||||
// Count data requests from the given conditions.
|
||||
$total = api::get_data_requests_count($this->userid, $this->statuses, $this->types);
|
||||
$this->pagesize($pagesize, $total);
|
||||
|
||||
$this->rawdata = [];
|
||||
$context = \context_system::instance();
|
||||
$renderer = $PAGE->get_renderer('tool_dataprivacy');
|
||||
|
||||
foreach ($datarequests as $persistent) {
|
||||
$this->datarequests[$persistent->get('id')] = $persistent;
|
||||
$exporter = new data_request_exporter($persistent, ['context' => $context]);
|
||||
$this->rawdata[] = $exporter->export($renderer);
|
||||
}
|
||||
|
||||
// Set initial bars.
|
||||
if ($useinitialsbar) {
|
||||
$this->initialbars($total > $pagesize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override default implementation to display a more meaningful information to the user.
|
||||
*/
|
||||
public function print_nothing_to_display() {
|
||||
global $OUTPUT;
|
||||
echo $this->render_reset_button();
|
||||
$this->print_initials_bar();
|
||||
if (!empty($this->statuses) || !empty($this->types)) {
|
||||
$message = get_string('nodatarequestsmatchingfilter', 'tool_dataprivacy');
|
||||
} else {
|
||||
$message = get_string('nodatarequests', 'tool_dataprivacy');
|
||||
}
|
||||
echo $OUTPUT->notification($message, 'warning');
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the table's show_hide_link method to prevent the show/hide links from rendering.
|
||||
*
|
||||
* @param string $column the column name, index into various names.
|
||||
* @param int $index numerical index of the column.
|
||||
* @return string HTML fragment.
|
||||
*/
|
||||
protected function show_hide_link($column, $index) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,7 @@ class my_data_requests_page implements renderable, templatable {
|
||||
$requestid = $request->get('id');
|
||||
$status = $request->get('status');
|
||||
$userid = $request->get('userid');
|
||||
$type = $request->get('type');
|
||||
|
||||
$usercontext = context_user::instance($userid, IGNORE_MISSING);
|
||||
if (!$usercontext) {
|
||||
@@ -94,7 +95,8 @@ class my_data_requests_page implements renderable, templatable {
|
||||
$requestexporter = new data_request_exporter($request, ['context' => $outputcontext]);
|
||||
$item = $requestexporter->export($output);
|
||||
|
||||
if ($request->get('userid') != $USER->id) {
|
||||
$self = $request->get('userid') == $USER->id;
|
||||
if (!$self) {
|
||||
// Append user name if it differs from $USER.
|
||||
$a = (object)['typename' => $item->typename, 'user' => $item->foruser->fullname];
|
||||
$item->typename = get_string('requesttypeuser', 'tool_dataprivacy', $a);
|
||||
@@ -107,7 +109,28 @@ class my_data_requests_page implements renderable, templatable {
|
||||
$item->statuslabelclass = 'label-success';
|
||||
$item->statuslabel = get_string('statuscomplete', 'tool_dataprivacy');
|
||||
$cancancel = false;
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
|
||||
$item->statuslabelclass = 'label-success';
|
||||
$item->statuslabel = get_string('statusready', 'tool_dataprivacy');
|
||||
$cancancel = false;
|
||||
$candownload = true;
|
||||
|
||||
if ($usercontext) {
|
||||
$candownload = api::can_download_data_request_for_user(
|
||||
$request->get('userid'), $request->get('requestedby'));
|
||||
}
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_DELETED:
|
||||
$item->statuslabelclass = 'label-success';
|
||||
$item->statuslabel = get_string('statusdeleted', 'tool_dataprivacy');
|
||||
$cancancel = false;
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_EXPIRED:
|
||||
$item->statuslabelclass = 'label-default';
|
||||
$item->statuslabel = get_string('statusexpired', 'tool_dataprivacy');
|
||||
$item->statuslabeltitle = get_string('downloadexpireduser', 'tool_dataprivacy');
|
||||
$cancancel = false;
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_CANCELLED:
|
||||
case api::DATAREQUEST_STATUS_REJECTED:
|
||||
@@ -124,10 +147,7 @@ class my_data_requests_page implements renderable, templatable {
|
||||
$actions[] = new action_menu_link_secondary($cancelurl, null, $canceltext, $canceldata);
|
||||
}
|
||||
if ($candownload && $usercontext) {
|
||||
$downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $requestid, '/',
|
||||
'export.zip', true);
|
||||
$downloadtext = get_string('download', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($downloadurl, null, $downloadtext);
|
||||
$actions[] = api::get_download_link($usercontext, $requestid);
|
||||
}
|
||||
if (!empty($actions)) {
|
||||
$actionsmenu = new action_menu($actions);
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Class containing the filter options data for rendering the autocomplete element for the data requests page.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace tool_dataprivacy\output;
|
||||
|
||||
use moodle_url;
|
||||
use renderable;
|
||||
use renderer_base;
|
||||
use stdClass;
|
||||
use templatable;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Class containing the filter options data for rendering the autocomplete element for the data requests page.
|
||||
*
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class request_filter implements renderable, templatable {
|
||||
|
||||
/** @var array $filteroptions The filter options. */
|
||||
protected $filteroptions;
|
||||
|
||||
/** @var array $selectedoptions The list of selected filter option values. */
|
||||
protected $selectedoptions;
|
||||
|
||||
/** @var moodle_url|string $baseurl The url with params needed to call up this page. */
|
||||
protected $baseurl;
|
||||
|
||||
/**
|
||||
* request_filter constructor.
|
||||
*
|
||||
* @param array $filteroptions The filter options.
|
||||
* @param array $selectedoptions The list of selected filter option values.
|
||||
* @param string|moodle_url $baseurl The url with params needed to call up this page.
|
||||
*/
|
||||
public function __construct($filteroptions, $selectedoptions, $baseurl = null) {
|
||||
$this->filteroptions = $filteroptions;
|
||||
$this->selectedoptions = $selectedoptions;
|
||||
if (!empty($baseurl)) {
|
||||
$this->baseurl = new moodle_url($baseurl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to export the renderer data in a format that is suitable for a mustache template.
|
||||
*
|
||||
* @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
|
||||
* @return stdClass|array
|
||||
*/
|
||||
public function export_for_template(renderer_base $output) {
|
||||
global $PAGE;
|
||||
$data = new stdClass();
|
||||
if (empty($this->baseurl)) {
|
||||
$this->baseurl = $PAGE->url;
|
||||
}
|
||||
$data->action = $this->baseurl->out(false);
|
||||
|
||||
foreach ($this->selectedoptions as $option) {
|
||||
if (!isset($this->filteroptions[$option])) {
|
||||
$this->filteroptions[$option] = $option;
|
||||
}
|
||||
}
|
||||
|
||||
$data->filteroptions = [];
|
||||
foreach ($this->filteroptions as $value => $label) {
|
||||
$selected = in_array($value, $this->selectedoptions);
|
||||
$filteroption = (object)[
|
||||
'value' => $value,
|
||||
'label' => $label
|
||||
];
|
||||
$filteroption->selected = $selected;
|
||||
$data->filteroptions[] = $filteroption;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,10 @@ class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
|
||||
// This tool may provide access to and deletion of user data.
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
|
||||
// This plugin has some sitewide user preferences to export.
|
||||
\core_privacy\local\request\user_preference_provider {
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
*
|
||||
@@ -70,6 +73,10 @@ class provider implements
|
||||
],
|
||||
'privacy:metadata:request'
|
||||
);
|
||||
|
||||
$collection->add_user_preference(tool_helper::PREF_REQUEST_FILTERS,
|
||||
'privacy:metadata:preference:tool_dataprivacy_request-filters');
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
@@ -162,4 +169,36 @@ class provider implements
|
||||
*/
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user preferences for the plugin.
|
||||
*
|
||||
* @param int $userid The userid of the user whose data is to be exported.
|
||||
*/
|
||||
public static function export_user_preferences(int $userid) {
|
||||
$preffilter = get_user_preferences(tool_helper::PREF_REQUEST_FILTERS, null, $userid);
|
||||
if ($preffilter !== null) {
|
||||
$filters = json_decode($preffilter);
|
||||
$descriptions = [];
|
||||
foreach ($filters as $filter) {
|
||||
list($category, $value) = explode(':', $filter);
|
||||
$option = new stdClass();
|
||||
switch($category) {
|
||||
case tool_helper::FILTER_TYPE:
|
||||
$option->category = get_string('requesttype', 'tool_dataprivacy');
|
||||
$option->name = tool_helper::get_shortened_request_type_string($value);
|
||||
break;
|
||||
case tool_helper::FILTER_STATUS:
|
||||
$option->category = get_string('requeststatus', 'tool_dataprivacy');
|
||||
$option->name = tool_helper::get_request_status_string($value);
|
||||
break;
|
||||
}
|
||||
$descriptions[] = get_string('filteroption', 'tool_dataprivacy', $option);
|
||||
}
|
||||
// Export the filter preference as comma-separated values and text descriptions.
|
||||
$values = implode(', ', $filters);
|
||||
$descriptionstext = implode(', ', $descriptions);
|
||||
writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_FILTERS, $values, $descriptionstext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class purpose extends \core\persistent {
|
||||
// Replicate self::read.
|
||||
$this->from_record($data);
|
||||
|
||||
// Using validate() as self::$validated is private.
|
||||
// Validate the purpose record.
|
||||
$this->validate();
|
||||
|
||||
// Now replicate the parent constructor.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Scheduled task to delete files and update statuses of expired data requests.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Michael Hawkins
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_dataprivacy\task;
|
||||
|
||||
use coding_exception;
|
||||
use core\task\scheduled_task;
|
||||
use tool_dataprivacy\api;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
/**
|
||||
* Scheduled task to delete files and update request statuses once they expire.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Michael Hawkins
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class delete_expired_requests extends scheduled_task {
|
||||
|
||||
/**
|
||||
* Returns the task name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name() {
|
||||
return get_string('deleteexpireddatarequeststask', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the task to delete expired data request files and update request statuses.
|
||||
*
|
||||
*/
|
||||
public function execute() {
|
||||
$expiredrequests = \tool_dataprivacy\data_request::get_expired_requests();
|
||||
$deletecount = count($expiredrequests);
|
||||
|
||||
if ($deletecount > 0) {
|
||||
\tool_dataprivacy\data_request::expire($expiredrequests);
|
||||
|
||||
mtrace($deletecount . ' expired completed data requests have been deleted');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class initiate_data_request_task extends adhoc_task {
|
||||
public function execute() {
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/admin/tool/dataprivacy/lib.php');
|
||||
require_once($CFG->dirroot . "/{$CFG->admin}/tool/dataprivacy/lib.php");
|
||||
|
||||
if (!isset($this->get_custom_data()->requestid)) {
|
||||
throw new coding_exception('The custom data \'requestid\' is required.');
|
||||
|
||||
@@ -57,7 +57,7 @@ class process_data_request_task extends adhoc_task {
|
||||
public function execute() {
|
||||
global $CFG, $PAGE, $SITE;
|
||||
|
||||
require_once($CFG->dirroot . '/admin/tool/dataprivacy/lib.php');
|
||||
require_once($CFG->dirroot . "/{$CFG->admin}/tool/dataprivacy/lib.php");
|
||||
|
||||
if (!isset($this->get_custom_data()->requestid)) {
|
||||
throw new coding_exception('The custom data \'requestid\' is required.');
|
||||
@@ -81,6 +81,7 @@ class process_data_request_task extends adhoc_task {
|
||||
// Update the status of this request as pre-processing.
|
||||
mtrace('Processing request...');
|
||||
api::update_request_status($requestid, api::DATAREQUEST_STATUS_PROCESSING);
|
||||
$completestatus = api::DATAREQUEST_STATUS_COMPLETE;
|
||||
|
||||
if ($request->type == api::DATAREQUEST_TYPE_EXPORT) {
|
||||
// Get the collection of approved_contextlist objects needed for core_privacy data export.
|
||||
@@ -105,7 +106,7 @@ class process_data_request_task extends adhoc_task {
|
||||
$filerecord->author = fullname($foruser);
|
||||
// Save somewhere.
|
||||
$thing = $fs->create_file_from_pathname($filerecord, $exportedcontent);
|
||||
|
||||
$completestatus = api::DATAREQUEST_STATUS_DOWNLOAD_READY;
|
||||
} else if ($request->type == api::DATAREQUEST_TYPE_DELETE) {
|
||||
// Get the collection of approved_contextlist objects needed for core_privacy data deletion.
|
||||
$approvedclcollection = api::get_approved_contextlist_collection_for_request($requestpersistent);
|
||||
@@ -115,10 +116,11 @@ class process_data_request_task extends adhoc_task {
|
||||
$manager->set_observer(new \tool_dataprivacy\manager_observer());
|
||||
|
||||
$manager->delete_data_for_user($approvedclcollection);
|
||||
$completestatus = api::DATAREQUEST_STATUS_DELETED;
|
||||
}
|
||||
|
||||
// When the preparation of the metadata finishes, update the request status to awaiting approval.
|
||||
api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE);
|
||||
api::update_request_status($requestid, $completestatus);
|
||||
mtrace('The processing of the user data request has been completed...');
|
||||
|
||||
// Create message to notify the user regarding the processing results.
|
||||
@@ -139,8 +141,17 @@ class process_data_request_task extends adhoc_task {
|
||||
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
$emailonly = false;
|
||||
$notifyuser = true;
|
||||
switch ($request->type) {
|
||||
case api::DATAREQUEST_TYPE_EXPORT:
|
||||
// Check if the user is allowed to download their own export. (This is for
|
||||
// institutions which centrally co-ordinate subject access request across many
|
||||
// systems, not just one Moodle instance, so we don't want every instance emailing
|
||||
// the user.)
|
||||
if (!api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->userid)) {
|
||||
$notifyuser = false;
|
||||
}
|
||||
|
||||
$typetext = get_string('requesttypeexport', 'tool_dataprivacy');
|
||||
// We want to notify the user in Moodle about the processing results.
|
||||
$message->notification = 1;
|
||||
@@ -179,18 +190,40 @@ class process_data_request_task extends adhoc_task {
|
||||
$message->fullmessagehtml = $messagehtml;
|
||||
|
||||
// Send message to the user involved.
|
||||
if ($emailonly) {
|
||||
email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
|
||||
} else {
|
||||
message_send($message);
|
||||
if ($notifyuser) {
|
||||
if ($emailonly) {
|
||||
email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
|
||||
} else {
|
||||
message_send($message);
|
||||
}
|
||||
mtrace('Message sent to user: ' . $messagetextdata['username']);
|
||||
}
|
||||
mtrace('Message sent to user: ' . $messagetextdata['username']);
|
||||
|
||||
// Send to requester as well if this request was made on behalf of another user who's not a DPO,
|
||||
// and has the capability to make data requests for the user (e.g. Parent).
|
||||
if (!api::is_site_dpo($request->requestedby) && $foruser->id != $request->requestedby) {
|
||||
// Send to requester as well in some circumstances.
|
||||
if ($foruser->id != $request->requestedby) {
|
||||
$sendtorequester = false;
|
||||
switch ($request->type) {
|
||||
case api::DATAREQUEST_TYPE_EXPORT:
|
||||
// Send to the requester as well if they can download it, unless they are the
|
||||
// DPO. If we didn't notify the user themselves (because they can't download)
|
||||
// then send to requester even if it is the DPO, as in that case the requester
|
||||
// needs to take some action.
|
||||
if (api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->requestedby)) {
|
||||
$sendtorequester = !$notifyuser || !api::is_site_dpo($request->requestedby);
|
||||
}
|
||||
break;
|
||||
case api::DATAREQUEST_TYPE_DELETE:
|
||||
// Send to the requester if they are not the DPO and if they are allowed to
|
||||
// create data requests for the user (e.g. Parent).
|
||||
$sendtorequester = !api::is_site_dpo($request->requestedby) &&
|
||||
api::can_create_data_request_for_user($request->userid, $request->requestedby);
|
||||
break;
|
||||
default:
|
||||
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
// Ensure the requester has the capability to make data requests for this user.
|
||||
if (api::can_create_data_request_for_user($request->userid, $request->requestedby)) {
|
||||
if ($sendtorequester) {
|
||||
$requestedby = core_user::get_user($request->requestedby);
|
||||
$message->userto = $requestedby;
|
||||
$messagetextdata['username'] = fullname($requestedby);
|
||||
|
||||
@@ -53,8 +53,9 @@ if ($manage) {
|
||||
$PAGE->set_context($context);
|
||||
|
||||
// If contactdataprotectionofficer is disabled, send the user back to the profile page, or the privacy policy page.
|
||||
if (!\tool_dataprivacy\api::can_contact_dpo()) {
|
||||
redirect($returnurl, get_string('contactdpoviaprivacypolicy', 'tool_dataprivacy'), \core\output\notification::NOTIFY_ERROR);
|
||||
// That is, unless you have sufficient capabilities to perform this on behalf of a user.
|
||||
if (!$manage && !\tool_dataprivacy\api::can_contact_dpo()) {
|
||||
redirect($returnurl, get_string('contactdpoviaprivacypolicy', 'tool_dataprivacy'), 0, \core\output\notification::NOTIFY_ERROR);
|
||||
}
|
||||
|
||||
$mform = new tool_dataprivacy_data_request_form($url->out(false), ['manage' => !empty($manage)]);
|
||||
|
||||
@@ -44,19 +44,28 @@ class tool_dataprivacy_data_request_form extends moodleform {
|
||||
/**
|
||||
* Form definition.
|
||||
*
|
||||
* @throws HTML_QuickForm_Error
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public function definition() {
|
||||
global $DB, $USER;
|
||||
global $USER;
|
||||
$mform =& $this->_form;
|
||||
|
||||
$this->manage = $this->_customdata['manage'];
|
||||
if ($this->manage) {
|
||||
$options = [
|
||||
'ajax' => 'tool_dataprivacy/form-user-selector',
|
||||
'multiple' => false
|
||||
'valuehtmlcallback' => function($value) {
|
||||
global $OUTPUT;
|
||||
|
||||
$allusernames = get_all_user_name_fields(true);
|
||||
$fields = 'id, email, ' . $allusernames;
|
||||
$user = \core_user::get_user($value, $fields);
|
||||
$useroptiondata = [
|
||||
'fullname' => fullname($user),
|
||||
'email' => $user->email
|
||||
];
|
||||
return $OUTPUT->render_from_template('tool_dataprivacy/form-user-selector-suggestion', $useroptiondata);
|
||||
}
|
||||
];
|
||||
$mform->addElement('autocomplete', 'userid', get_string('requestfor', 'tool_dataprivacy'), [], $options);
|
||||
$mform->addRule('userid', null, 'required', null, 'client');
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$filter = optional_param('filter', CONTEXT_COURSE, PARAM_INT);
|
||||
|
||||
$url = new moodle_url('/admin/tool/dataprivacy/datadeletion.php');
|
||||
@@ -35,13 +37,19 @@ $title = get_string('datadeletion', 'tool_dataprivacy');
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
$table = new \tool_dataprivacy\output\expired_contexts_table($filter);
|
||||
$table->baseurl = $url;
|
||||
$table->baseurl->param('filter', $filter);
|
||||
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
|
||||
$table = new \tool_dataprivacy\output\expired_contexts_table($filter);
|
||||
$table->baseurl = $url;
|
||||
$table->baseurl->param('filter', $filter);
|
||||
|
||||
$datadeletionpage = new \tool_dataprivacy\output\data_deletion_page($filter, $table);
|
||||
$datadeletionpage = new \tool_dataprivacy\output\data_deletion_page($filter, $table);
|
||||
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->render($datadeletionpage);
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->render($datadeletionpage);
|
||||
} else {
|
||||
$dponamestring = implode (',', tool_dataprivacy\api::get_dpo_role_names());
|
||||
$message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring);
|
||||
echo $OUTPUT->notification($message, 'error');
|
||||
}
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$contextlevel = optional_param('contextlevel', CONTEXT_SYSTEM, PARAM_INT);
|
||||
$contextid = optional_param('contextid', 0, PARAM_INT);
|
||||
|
||||
@@ -36,7 +38,12 @@ $title = get_string('dataregistry', 'tool_dataprivacy');
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->header();
|
||||
|
||||
$dataregistry = new tool_dataprivacy\output\data_registry_page($contextlevel, $contextid);
|
||||
|
||||
echo $output->render($dataregistry);
|
||||
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
|
||||
$dataregistry = new tool_dataprivacy\output\data_registry_page($contextlevel, $contextid);
|
||||
echo $output->render($dataregistry);
|
||||
} else {
|
||||
$dponamestring = implode (', ', tool_dataprivacy\api::get_dpo_role_names());
|
||||
$message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring);
|
||||
echo $OUTPUT->notification($message, 'error');
|
||||
}
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
require_once("../../../config.php");
|
||||
require_once('lib.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
|
||||
|
||||
$title = get_string('datarequests', 'tool_dataprivacy');
|
||||
@@ -34,9 +36,45 @@ $title = get_string('datarequests', 'tool_dataprivacy');
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading($title);
|
||||
|
||||
$requests = tool_dataprivacy\api::get_data_requests();
|
||||
$requestlist = new tool_dataprivacy\output\data_requests_page($requests);
|
||||
$requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $requestlistoutput->render($requestlist);
|
||||
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
|
||||
$filtersapplied = optional_param_array('request-filters', [-1], PARAM_NOTAGS);
|
||||
$filterscleared = optional_param('filters-cleared', 0, PARAM_INT);
|
||||
if ($filtersapplied === [-1]) {
|
||||
// If there are no filters submitted, check if there is a saved filters from the user preferences.
|
||||
$filterprefs = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, null);
|
||||
if ($filterprefs && empty($filterscleared)) {
|
||||
$filtersapplied = json_decode($filterprefs);
|
||||
} else {
|
||||
$filtersapplied = [];
|
||||
}
|
||||
}
|
||||
// Save the current applied filters to the user preferences.
|
||||
set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, json_encode($filtersapplied));
|
||||
|
||||
$types = [];
|
||||
$statuses = [];
|
||||
foreach ($filtersapplied as $filter) {
|
||||
list($category, $value) = explode(':', $filter);
|
||||
switch($category) {
|
||||
case \tool_dataprivacy\local\helper::FILTER_TYPE:
|
||||
$types[] = $value;
|
||||
break;
|
||||
case \tool_dataprivacy\local\helper::FILTER_STATUS:
|
||||
$statuses[] = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, true);
|
||||
$table->baseurl = $url;
|
||||
|
||||
$requestlist = new tool_dataprivacy\output\data_requests_page($table, $filtersapplied);
|
||||
$requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $requestlistoutput->render($requestlist);
|
||||
} else {
|
||||
$dponamestring = implode (', ', tool_dataprivacy\api::get_dpo_role_names());
|
||||
$message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring);
|
||||
echo $OUTPUT->notification($message, 'error');
|
||||
}
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
@@ -49,4 +49,22 @@ $capabilities = [
|
||||
'contextlevel' => CONTEXT_USER,
|
||||
'archetypes' => []
|
||||
],
|
||||
|
||||
// Capability for users to download the results of their own data request.
|
||||
'tool/dataprivacy:downloadownrequest' => [
|
||||
'riskbitmask' => 0,
|
||||
'captype' => 'read',
|
||||
'contextlevel' => CONTEXT_USER,
|
||||
'archetypes' => [
|
||||
'user' => CAP_ALLOW
|
||||
]
|
||||
],
|
||||
|
||||
// Capability for administrators to download other people's data requests.
|
||||
'tool/dataprivacy:downloadallrequests' => [
|
||||
'riskbitmask' => RISK_PERSONAL,
|
||||
'captype' => 'read',
|
||||
'contextlevel' => CONTEXT_USER,
|
||||
'archetypes' => []
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180313" COMMENT="XMLDB file for Moodle tool/dataprivacy"
|
||||
<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180821" COMMENT="XMLDB file for Moodle tool/dataprivacy"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
|
||||
>
|
||||
@@ -12,7 +12,7 @@
|
||||
<FIELD NAME="commentsformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID the request is being made for"/>
|
||||
<FIELD NAME="requestedby" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID of the one making the request"/>
|
||||
<FIELD NAME="status" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The current status of the data request"/>
|
||||
<FIELD NAME="status" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The current status of the data request"/>
|
||||
<FIELD NAME="dpo" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID of the Data Protection Officer who is reviewing th request"/>
|
||||
<FIELD NAME="dpocomment" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="DPO's comments (e.g. reason for rejecting the request, etc.)"/>
|
||||
<FIELD NAME="dpocommentformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
@@ -98,7 +98,7 @@
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="status" TYPE="int" LENGTH="2" DEFAULT="0" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="status" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
@@ -147,4 +147,4 @@
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
</XMLDB>
|
||||
@@ -43,6 +43,16 @@ $functions = [
|
||||
'ajax' => true,
|
||||
'loginrequired' => true,
|
||||
],
|
||||
'tool_dataprivacy_mark_complete' => [
|
||||
'classname' => 'tool_dataprivacy\external',
|
||||
'methodname' => 'mark_complete',
|
||||
'classpath' => '',
|
||||
'description' => 'Mark a user\'s general enquiry as complete',
|
||||
'type' => 'write',
|
||||
'capabilities' => 'tool/dataprivacy:managedatarequests',
|
||||
'ajax' => true,
|
||||
'loginrequired' => true,
|
||||
],
|
||||
'tool_dataprivacy_get_data_request' => [
|
||||
'classname' => 'tool_dataprivacy\external',
|
||||
'methodname' => 'get_data_request',
|
||||
|
||||
@@ -42,5 +42,13 @@ $tasks = array(
|
||||
'day' => '*',
|
||||
'dayofweek' => '*',
|
||||
'month' => '*'
|
||||
), array(
|
||||
'classname' => 'tool_dataprivacy\task\delete_expired_requests',
|
||||
'blocking' => 0,
|
||||
'minute' => 'R',
|
||||
'hour' => 'R',
|
||||
'day' => '*',
|
||||
'dayofweek' => '*',
|
||||
'month' => '*'
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* tool_dataprivacy plugin upgrade code
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Function to upgrade tool_dataprivacy.
|
||||
*
|
||||
* @param int $oldversion the version we are upgrading from
|
||||
* @return bool result
|
||||
*/
|
||||
function xmldb_tool_dataprivacy_upgrade($oldversion) {
|
||||
global $CFG, $DB;
|
||||
|
||||
$dbman = $DB->get_manager();
|
||||
|
||||
if ($oldversion < 2018051405) {
|
||||
// Define table tool_dataprivacy_ctxexpired to be created.
|
||||
$table = new xmldb_table('tool_dataprivacy_ctxexpired');
|
||||
|
||||
// Adding fields to table tool_dataprivacy_ctxexpired.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('status', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
|
||||
$table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
|
||||
// Adding keys to table tool_dataprivacy_ctxexpired.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
|
||||
$table->add_key('contextid', XMLDB_KEY_FOREIGN_UNIQUE, array('contextid'), 'context', array('id'));
|
||||
|
||||
// Conditionally launch create table for tool_dataprivacy_ctxexpired.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Define table tool_dataprivacy_contextlist to be created.
|
||||
$table = new xmldb_table('tool_dataprivacy_contextlist');
|
||||
|
||||
// Adding fields to table tool_dataprivacy_contextlist.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('component', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
|
||||
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
|
||||
|
||||
// Adding keys to table tool_dataprivacy_contextlist.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
|
||||
|
||||
// Conditionally launch create table for tool_dataprivacy_contextlist.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Define table tool_dataprivacy_ctxlst_ctx to be created.
|
||||
$table = new xmldb_table('tool_dataprivacy_ctxlst_ctx');
|
||||
|
||||
// Adding fields to table tool_dataprivacy_ctxlst_ctx.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('contextlistid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('status', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
|
||||
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
|
||||
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
|
||||
|
||||
// Adding keys to table tool_dataprivacy_ctxlst_ctx.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
|
||||
$table->add_key('contextlistid', XMLDB_KEY_FOREIGN, array('contextlistid'), 'tool_dataprivacy_contextlist', array('id'));
|
||||
|
||||
// Conditionally launch create table for tool_dataprivacy_ctxlst_ctx.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Define table tool_dataprivacy_rqst_ctxlst to be created.
|
||||
$table = new xmldb_table('tool_dataprivacy_rqst_ctxlst');
|
||||
|
||||
// Adding fields to table tool_dataprivacy_rqst_ctxlst.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('requestid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('contextlistid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
|
||||
// Adding keys to table tool_dataprivacy_rqst_ctxlst.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
|
||||
$table->add_key('requestid', XMLDB_KEY_FOREIGN, array('requestid'), 'tool_dataprivacy_request', array('id'));
|
||||
$table->add_key('contextlistid', XMLDB_KEY_FOREIGN, array('contextlistid'), 'tool_dataprivacy_contextlist', array('id'));
|
||||
$table->add_key('request_contextlist', XMLDB_KEY_UNIQUE, array('requestid', 'contextlistid'));
|
||||
|
||||
// Conditionally launch create table for tool_dataprivacy_rqst_ctxlst.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Define field lawfulbases to be added to tool_dataprivacy_purpose.
|
||||
$table = new xmldb_table('tool_dataprivacy_purpose');
|
||||
|
||||
// It is a required field. We initially define and add it as null and later update it to XMLDB_NOTNULL.
|
||||
$field = new xmldb_field('lawfulbases', XMLDB_TYPE_TEXT, null, null, null, null, null, 'descriptionformat');
|
||||
|
||||
// Conditionally launch add field lawfulbases.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
|
||||
// Set a kind-of-random value to lawfulbasis field.
|
||||
$DB->set_field('tool_dataprivacy_purpose', 'lawfulbases', 'gdpr_art_6_1_a');
|
||||
|
||||
// We redefine it now as not null.
|
||||
$field = new xmldb_field('lawfulbases', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null, 'descriptionformat');
|
||||
|
||||
// Launch change of nullability for field lawfulbases.
|
||||
$dbman->change_field_notnull($table, $field);
|
||||
}
|
||||
|
||||
// Define field sensitivedatareasons to be added to tool_dataprivacy_purpose.
|
||||
$table = new xmldb_table('tool_dataprivacy_purpose');
|
||||
$field = new xmldb_field('sensitivedatareasons', XMLDB_TYPE_TEXT, null, null, null, null, null, 'lawfulbases');
|
||||
|
||||
// Conditionally launch add field sensitivedatareasons.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Dataprivacy savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2018051405, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
if ($oldversion < 2018051406) {
|
||||
// Update completed delete requests to new delete status.
|
||||
$query = "UPDATE {tool_dataprivacy_request}
|
||||
SET status = :setstatus
|
||||
WHERE type = :type
|
||||
AND status = :wherestatus";
|
||||
$params = array(
|
||||
'setstatus' => 10, // Request deleted.
|
||||
'type' => 2, // Delete type.
|
||||
'wherestatus' => 5, // Request completed.
|
||||
);
|
||||
|
||||
$DB->execute($query, $params);
|
||||
|
||||
// Update completed data export requests to new download ready status.
|
||||
$params = array(
|
||||
'setstatus' => 8, // Request download ready.
|
||||
'type' => 1, // export type.
|
||||
'wherestatus' => 5, // Request completed.
|
||||
);
|
||||
|
||||
$DB->execute($query, $params);
|
||||
|
||||
upgrade_plugin_savepoint(true, 2018051406, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
if ($oldversion < 2018051407) {
|
||||
|
||||
// Changing precision of field status on table tool_dataprivacy_request to (2).
|
||||
$table = new xmldb_table('tool_dataprivacy_request');
|
||||
$field = new xmldb_field('status', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'requestedby');
|
||||
|
||||
// Launch change of precision for field status.
|
||||
$dbman->change_field_precision($table, $field);
|
||||
|
||||
// Dataprivacy savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2018051407, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -25,6 +25,8 @@
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$url = new \moodle_url('/admin/tool/dataprivacy/defaults.php');
|
||||
$title = get_string('setdefaults', 'tool_dataprivacy');
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$id = optional_param('id', 0, PARAM_INT);
|
||||
|
||||
$url = new \moodle_url('/admin/tool/dataprivacy/editcategory.php', array('id' => $id));
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$id = optional_param('id', 0, PARAM_INT);
|
||||
|
||||
$url = new \moodle_url('/admin/tool/dataprivacy/editpurpose.php', array('id' => $id));
|
||||
|
||||
@@ -46,10 +46,11 @@ $string['categoryupdated'] = 'Category updated';
|
||||
$string['close'] = 'Close';
|
||||
$string['compliant'] = 'Compliant';
|
||||
$string['confirmapproval'] = 'Do you really want to approve this data request?';
|
||||
$string['confirmcompletion'] = 'Do you really want to mark this user enquiry as complete?';
|
||||
$string['confirmcontextdeletion'] = 'Do you really want to confirm the deletion of the selected contexts? This will also delete all of the user data for their respective sub-contexts.';
|
||||
$string['confirmdenial'] = 'Do you really want deny this data request?';
|
||||
$string['contactdataprotectionofficer'] = 'Contact Data Protection Officer';
|
||||
$string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the Data Protection Officer and make a data request via a link on their profile page.';
|
||||
$string['contactdataprotectionofficer'] = 'Contact the privacy officer';
|
||||
$string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the privacy officer and make a data request via a link on their profile page.';
|
||||
$string['contextlevelname10'] = 'Site';
|
||||
$string['contextlevelname30'] = 'Users';
|
||||
$string['contextlevelname40'] = 'Course categories';
|
||||
@@ -57,7 +58,7 @@ $string['contextlevelname50'] = 'Courses';
|
||||
$string['contextlevelname70'] = 'Activity modules';
|
||||
$string['contextlevelname80'] = 'Blocks';
|
||||
$string['contextpurposecategorysaved'] = 'Purpose and category saved.';
|
||||
$string['contactdpoviaprivacypolicy'] = 'Please contact the Data Protection Officer as described in the privacy policy.';
|
||||
$string['contactdpoviaprivacypolicy'] = 'Please contact the privacy officer as described in the privacy policy.';
|
||||
$string['createcategory'] = 'Create data category';
|
||||
$string['createpurpose'] = 'Create data purpose';
|
||||
$string['datadeletion'] = 'Data deletion';
|
||||
@@ -65,25 +66,30 @@ $string['datadeletionpagehelp'] = 'Data for which the retention period has expir
|
||||
$string['dataprivacy:makedatarequestsforchildren'] = 'Make data requests for minors';
|
||||
$string['dataprivacy:managedatarequests'] = 'Manage data requests';
|
||||
$string['dataprivacy:managedataregistry'] = 'Manage data registry';
|
||||
$string['dataprivacy:downloadownrequest'] = 'Download your own exported data';
|
||||
$string['dataprivacy:downloadallrequests'] = 'Download exported data for everyone';
|
||||
$string['dataregistry'] = 'Data registry';
|
||||
$string['dataregistryinfo'] = 'The data registry enables categories (types of data) and purposes (the reasons for processing data) to be set for all content on the site - from users and courses down to activities and blocks. For each purpose, a retention period may be set. When a retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
|
||||
$string['datarequestcreatedforuser'] = 'Data request created for {$a}';
|
||||
$string['datarequestemailsubject'] = 'Data request: {$a}';
|
||||
$string['datarequests'] = 'Data requests';
|
||||
$string['datecomment'] = '[{$a->date}]: ' . PHP_EOL . ' {$a->comment}';
|
||||
$string['daterequested'] = 'Date requested';
|
||||
$string['daterequesteddetail'] = 'Date requested:';
|
||||
$string['defaultsinfo'] = 'Default categories and purposes are applied to all newly created instances.';
|
||||
$string['deletecategory'] = 'Delete "{$a}" category';
|
||||
$string['deletecategorytext'] = 'Are you sure you want to delete "{$a}" category?';
|
||||
$string['deletecategory'] = 'Delete category';
|
||||
$string['deletecategorytext'] = 'Are you sure you want to delete the category \'{$a}\'?';
|
||||
$string['deleteexpiredcontextstask'] = 'Delete expired contexts';
|
||||
$string['deletepurpose'] = 'Delete "{$a}" purpose';
|
||||
$string['deletepurposetext'] = 'Are you sure you want to delete "{$a}" purpose?';
|
||||
$string['deleteexpireddatarequeststask'] = 'Delete files from completed data requests that have expired';
|
||||
$string['deletepurpose'] = 'Delete purpose';
|
||||
$string['deletepurposetext'] = 'Are you sure you want to delete the purpose \'{$a}\'?';
|
||||
$string['defaultssaved'] = 'Defaults saved';
|
||||
$string['deny'] = 'Deny';
|
||||
$string['denyrequest'] = 'Deny request';
|
||||
$string['download'] = 'Download';
|
||||
$string['dporolemapping'] = 'Data Protection Officer role mapping';
|
||||
$string['dporolemapping_desc'] = 'The Data Protection Officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a Data Protection Officer role mapping option.';
|
||||
$string['downloadexpireduser'] = 'Download has expired. Submit a new request if you wish to export your personal data.';
|
||||
$string['dporolemapping'] = 'Privacy officer role mapping';
|
||||
$string['dporolemapping_desc'] = 'The privacy officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a privacy officer role mapping option.';
|
||||
$string['editcategories'] = 'Edit categories';
|
||||
$string['editcategory'] = 'Edit category';
|
||||
$string['editcategories'] = 'Edit categories';
|
||||
@@ -101,8 +107,10 @@ $string['errorrequestalreadyexists'] = 'You already have an ongoing request.';
|
||||
$string['errorrequestnotfound'] = 'Request not found';
|
||||
$string['errorrequestnotwaitingforapproval'] = 'The request is not awaiting approval. Either it is not yet ready or it has already been processed.';
|
||||
$string['errorsendingmessagetodpo'] = 'An error was encountered while trying to send a message to {$a}.';
|
||||
$string['exceptionnotificationsubject'] = "Exception occured while processing privacy data";
|
||||
$string['exceptionnotificationbody'] = "<p>Exception occured while calling <b>{\$a->fullmethodname}</b>.<br>This means that plugin <b>{\$a->component}</b> did not complete processing data. Below you can find exception information that can be passed to the plugin developer.</p><pre>{\$a->message}<br>\n\n{\$a->backtrace}</pre>";
|
||||
$string['exceptionnotificationsubject'] = 'Exception occurred while processing privacy data';
|
||||
$string['exceptionnotificationbody'] = '<p>Exception occurred while calling <b>{$a->fullmethodname}</b>.<br>This means that plugin <b>{$a->component}</b> did not complete the processing of data. The following exception information may be passed on to the plugin developer:</p><pre>{$a->message}<br>
|
||||
|
||||
{$a->backtrace}</pre>';
|
||||
$string['expiredretentionperiodtask'] = 'Expired retention period';
|
||||
$string['expiry'] = 'Expiry';
|
||||
$string['expandplugin'] = 'Expand and collapse plugin.';
|
||||
@@ -110,6 +118,7 @@ $string['expandplugintype'] = 'Expand and collapse plugin type.';
|
||||
$string['explanationtitle'] = 'Icons used on this page and what they mean.';
|
||||
$string['external'] = 'Additional';
|
||||
$string['externalexplanation'] = 'An additional plugin installed on this site.';
|
||||
$string['filteroption'] = '{$a->category}: {$a->name}';
|
||||
$string['frontpagecourse'] = 'Front page course';
|
||||
$string['gdpr_art_6_1_a_description'] = 'The data subject has given consent to the processing of his or her personal data for one or more specific purposes';
|
||||
$string['gdpr_art_6_1_a_name'] = 'Consent (GDPR Art. 6.1(a))';
|
||||
@@ -148,6 +157,8 @@ $string['httpwarning'] = 'Any data downloaded from this site may not be encrypte
|
||||
$string['inherit'] = 'Inherit';
|
||||
$string['lawfulbases'] = 'Lawful bases';
|
||||
$string['lawfulbases_help'] = 'Select at least one option that will serve as the lawful basis for processing personal data. For details on these lawful bases, please see <a href="https://gdpr-info.eu/art-6-gdpr/" target="_blank">GDPR Art. 6.1</a>';
|
||||
$string['markcomplete'] = 'Mark as complete';
|
||||
$string['markedcomplete'] = 'Your enquiry has been marked as complete by the privacy officer.';
|
||||
$string['messageprovider:contactdataprotectionofficer'] = 'Data requests';
|
||||
$string['messageprovider:datarequestprocessingresults'] = 'Data request processing results';
|
||||
$string['messageprovider:notifyexceptions'] = 'Data requests exceptions notifications';
|
||||
@@ -160,6 +171,7 @@ $string['nameemail'] = '{$a->name} ({$a->email})';
|
||||
$string['nchildren'] = '{$a} children';
|
||||
$string['newrequest'] = 'New request';
|
||||
$string['nodatarequests'] = 'There are no data requests';
|
||||
$string['nodatarequestsmatchingfilter'] = 'There are no data requests matching the given filter';
|
||||
$string['noactivitiestoload'] = 'No activities';
|
||||
$string['noassignedroles'] = 'No assigned roles in this context';
|
||||
$string['noblockstoload'] = 'No blocks';
|
||||
@@ -174,12 +186,16 @@ $string['notset'] = 'Not set (use the default value)';
|
||||
$string['pluginregistry'] = 'Plugin privacy registry';
|
||||
$string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
|
||||
$string['privacy'] = 'Privacy';
|
||||
$string['privacyofficeronly'] = 'Only users who are assigned a privacy officer role ({$a}) have access to this content';
|
||||
$string['privacy:metadata:preference:tool_dataprivacy_request-filters'] = 'The filters currently applied to the data requests page.';
|
||||
$string['privacy:metadata:request'] = 'Information from personal data requests (subject access and deletion requests) made for this site.';
|
||||
$string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.';
|
||||
$string['privacy:metadata:request:userid'] = 'The ID of the user to whom the request belongs';
|
||||
$string['privacy:metadata:request:requestedby'] = 'The ID of the user making the request, if made on behalf of another user.';
|
||||
$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s Data Protection Officer regarding the request.';
|
||||
$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s privacy officer regarding the request.';
|
||||
$string['privacy:metadata:request:timecreated'] = 'The timestamp indicating when the request was made by the user.';
|
||||
$string['privacyrequestexpiry'] = 'Data request expiry';
|
||||
$string['privacyrequestexpiry_desc'] = 'The time that approved data requests will be available for download before expiring. If set to zero, then there is no time limit.';
|
||||
$string['protected'] = 'Protected';
|
||||
$string['protectedlabel'] = 'The retention of this data has a higher legal precedent over a user\'s request to be forgotten. This data will only be deleted after the retention period has expired.';
|
||||
$string['purpose'] = 'Purpose';
|
||||
@@ -192,22 +208,26 @@ $string['purposeslist'] = 'List of data purposes';
|
||||
$string['purposeupdated'] = 'Purpose updated';
|
||||
$string['replyto'] = 'Reply to';
|
||||
$string['requestactions'] = 'Actions';
|
||||
$string['requestapproved'] = 'The request has been approved';
|
||||
$string['requestby'] = 'Requested by';
|
||||
$string['requestbydetail'] = 'Requested by:';
|
||||
$string['requestcomments'] = 'Comments';
|
||||
$string['requestcomments_help'] = 'This box enables you to enter any further details about your data request.';
|
||||
$string['requestdenied'] = 'The request has been denied';
|
||||
$string['requestemailintro'] = 'You have received a data request:';
|
||||
$string['requestfor'] = 'Requesting for';
|
||||
$string['requestmarkedcomplete'] = 'The request has been marked as complete';
|
||||
$string['requeststatus'] = 'Status';
|
||||
$string['requestsubmitted'] = 'Your request has been submitted to the site\'s Data Protection Officer';
|
||||
$string['requestsubmitted'] = 'Your request has been submitted to the privacy officer';
|
||||
$string['requesttype'] = 'Type';
|
||||
$string['requesttypeuser'] = '{$a->typename} ({$a->user})';
|
||||
$string['requesttype_help'] = 'Select the reason why you would like to contact the site\'s Data Protection Officer';
|
||||
$string['requesttype_help'] = 'Select the reason for contacting the privacy officer. Be aware that deletion of all personal data will result in you no longer being able to log in to the site.';
|
||||
$string['requesttypedelete'] = 'Delete all of my personal data';
|
||||
$string['requesttypedeleteshort'] = 'Delete';
|
||||
$string['requesttypeexport'] = 'Export all of my personal data';
|
||||
$string['requesttypeexportshort'] = 'Export';
|
||||
$string['requesttypeothers'] = 'General inquiry';
|
||||
$string['requesttypeothersshort'] = 'Others';
|
||||
$string['requesttypeothersshort'] = 'Message';
|
||||
$string['requiresattention'] = 'Requires attention.';
|
||||
$string['requiresattentionexplanation'] = 'This plugin does not implement the Moodle privacy API. If this plugin stores any personal data it will not be able to be exported or deleted through Moodle\'s privacy system.';
|
||||
$string['resultdeleted'] = 'You recently requested to have your account and personal data in {$a} to be deleted. This process has been completed and you will no longer be able to log in.';
|
||||
@@ -225,7 +245,10 @@ $string['statusapproved'] = 'Approved';
|
||||
$string['statusawaitingapproval'] = 'Awaiting approval';
|
||||
$string['statuscancelled'] = 'Cancelled';
|
||||
$string['statuscomplete'] = 'Complete';
|
||||
$string['statusready'] = 'Download ready';
|
||||
$string['statusdeleted'] = 'Deleted';
|
||||
$string['statusdetail'] = 'Status:';
|
||||
$string['statusexpired'] = 'Expired';
|
||||
$string['statuspreprocessing'] = 'Pre-processing';
|
||||
$string['statusprocessing'] = 'Processing';
|
||||
$string['statuspending'] = 'Pending';
|
||||
|
||||
@@ -185,26 +185,23 @@ function tool_dataprivacy_output_fragment_contextlevel_form($args) {
|
||||
* @return bool Returns false if we don't find a file.
|
||||
*/
|
||||
function tool_dataprivacy_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) {
|
||||
global $USER;
|
||||
|
||||
if ($context->contextlevel == CONTEXT_USER) {
|
||||
// Make sure the user is logged in.
|
||||
require_login(null, false);
|
||||
|
||||
// Validate the user downloading this archive.
|
||||
$usercontext = context_user::instance($USER->id);
|
||||
// The user downloading this is not the user the archive has been prepared for. Check if it's the requester (e.g. parent).
|
||||
if ($usercontext->instanceid !== $context->instanceid) {
|
||||
// Get the data request ID. This should be the first element of the $args array.
|
||||
$itemid = $args[0];
|
||||
// Fetch the data request object. An invalid ID will throw an exception.
|
||||
$datarequest = new \tool_dataprivacy\data_request($itemid);
|
||||
// Get the data request ID. This should be the first element of the $args array.
|
||||
$itemid = $args[0];
|
||||
// Fetch the data request object. An invalid ID will throw an exception.
|
||||
$datarequest = new \tool_dataprivacy\data_request($itemid);
|
||||
|
||||
// Check if the user is the requester and has the capability to make data requests for the target user.
|
||||
$candownloadforuser = has_capability('tool/dataprivacy:makedatarequestsforchildren', $context);
|
||||
if ($USER->id != $datarequest->get('requestedby') || !$candownloadforuser) {
|
||||
return false;
|
||||
}
|
||||
// Check if user is allowed to download it.
|
||||
if (!\tool_dataprivacy\api::can_download_data_request_for_user($context->instanceid, $datarequest->get('requestedby'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make the file unavailable if it has expired.
|
||||
if (\tool_dataprivacy\data_request::is_expired($datarequest)) {
|
||||
send_file_not_found();
|
||||
}
|
||||
|
||||
// All good. Serve the exported data.
|
||||
|
||||
@@ -55,7 +55,7 @@ $PAGE->set_title($title);
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading($title);
|
||||
|
||||
$requests = tool_dataprivacy\api::get_data_requests($USER->id);
|
||||
$requests = tool_dataprivacy\api::get_data_requests($USER->id, [], [], 'timecreated DESC');
|
||||
$requestlist = new tool_dataprivacy\output\my_data_requests_page($requests);
|
||||
$requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $requestlistoutput->render($requestlist);
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$contextlevel = optional_param('contextlevel', CONTEXT_SYSTEM, PARAM_INT);
|
||||
$contextid = optional_param('contextid', 0, PARAM_INT);
|
||||
|
||||
@@ -36,11 +38,18 @@ $title = get_string('pluginregistry', 'tool_dataprivacy');
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->header();
|
||||
|
||||
// Get data!
|
||||
$metadatatool = new \tool_dataprivacy\metadata_registry();
|
||||
$metadata = $metadatatool->get_registry_metadata();
|
||||
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
|
||||
// Get data!
|
||||
$metadatatool = new \tool_dataprivacy\metadata_registry();
|
||||
$metadata = $metadatatool->get_registry_metadata();
|
||||
|
||||
$dataregistry = new tool_dataprivacy\output\data_registry_compliance_page($metadata);
|
||||
$dataregistry = new tool_dataprivacy\output\data_registry_compliance_page($metadata);
|
||||
|
||||
echo $output->render($dataregistry);
|
||||
} else {
|
||||
$dponamestring = implode (', ', tool_dataprivacy\api::get_dpo_role_names());
|
||||
$message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring);
|
||||
echo $OUTPUT->notification($message, 'error');
|
||||
}
|
||||
|
||||
echo $output->render($dataregistry);
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$url = new moodle_url("/admin/tool/dataprivacy/purposes.php");
|
||||
$title = get_string('editpurposes', 'tool_dataprivacy');
|
||||
|
||||
|
||||
@@ -34,6 +34,12 @@ if ($hassiteconfig) {
|
||||
new lang_string('contactdataprotectionofficer_desc', 'tool_dataprivacy'), 0)
|
||||
);
|
||||
|
||||
// Set days approved data requests will be accessible. 1 week default.
|
||||
$privacysettings->add(new admin_setting_configduration('tool_dataprivacy/privacyrequestexpiry',
|
||||
new lang_string('privacyrequestexpiry', 'tool_dataprivacy'),
|
||||
new lang_string('privacyrequestexpiry_desc', 'tool_dataprivacy'),
|
||||
WEEKSECS, 1));
|
||||
|
||||
// Fetch roles that are assignable.
|
||||
$assignableroles = get_assignable_roles(context_system::instance());
|
||||
|
||||
@@ -57,22 +63,25 @@ if ($hassiteconfig) {
|
||||
}
|
||||
}
|
||||
|
||||
// Link that leads to the data requests management page.
|
||||
$ADMIN->add('privacy', new admin_externalpage('datarequests', get_string('datarequests', 'tool_dataprivacy'),
|
||||
new moodle_url('/admin/tool/dataprivacy/datarequests.php'), 'tool/dataprivacy:managedatarequests')
|
||||
);
|
||||
// Restrict config links to the DPO.
|
||||
if (tool_dataprivacy\api::is_site_dpo($USER->id)) {
|
||||
// Link that leads to the data requests management page.
|
||||
$ADMIN->add('privacy', new admin_externalpage('datarequests', get_string('datarequests', 'tool_dataprivacy'),
|
||||
new moodle_url('/admin/tool/dataprivacy/datarequests.php'), 'tool/dataprivacy:managedatarequests')
|
||||
);
|
||||
|
||||
// Link that leads to the data registry management page.
|
||||
$ADMIN->add('privacy', new admin_externalpage('dataregistry', get_string('dataregistry', 'tool_dataprivacy'),
|
||||
new moodle_url('/admin/tool/dataprivacy/dataregistry.php'), 'tool/dataprivacy:managedataregistry')
|
||||
);
|
||||
// Link that leads to the data registry management page.
|
||||
$ADMIN->add('privacy', new admin_externalpage('dataregistry', get_string('dataregistry', 'tool_dataprivacy'),
|
||||
new moodle_url('/admin/tool/dataprivacy/dataregistry.php'), 'tool/dataprivacy:managedataregistry')
|
||||
);
|
||||
|
||||
// Link that leads to the review page of expired contexts that are up for deletion.
|
||||
$ADMIN->add('privacy', new admin_externalpage('datadeletion', get_string('datadeletion', 'tool_dataprivacy'),
|
||||
new moodle_url('/admin/tool/dataprivacy/datadeletion.php'), 'tool/dataprivacy:managedataregistry')
|
||||
);
|
||||
// Link that leads to the review page of expired contexts that are up for deletion.
|
||||
$ADMIN->add('privacy', new admin_externalpage('datadeletion', get_string('datadeletion', 'tool_dataprivacy'),
|
||||
new moodle_url('/admin/tool/dataprivacy/datadeletion.php'), 'tool/dataprivacy:managedataregistry')
|
||||
);
|
||||
|
||||
// Link that leads to the other data registry management page.
|
||||
$ADMIN->add('privacy', new admin_externalpage('pluginregistry', get_string('pluginregistry', 'tool_dataprivacy'),
|
||||
new moodle_url('/admin/tool/dataprivacy/pluginregistry.php'), 'tool/dataprivacy:managedataregistry')
|
||||
);
|
||||
// Link that leads to the other data registry management page.
|
||||
$ADMIN->add('privacy', new admin_externalpage('pluginregistry', get_string('pluginregistry', 'tool_dataprivacy'),
|
||||
new moodle_url('/admin/tool/dataprivacy/pluginregistry.php'), 'tool/dataprivacy:managedataregistry')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,3 +24,7 @@ dd a.contactdpo {
|
||||
/* Reverting dd's left margin */
|
||||
margin-left: inherit;
|
||||
}
|
||||
|
||||
[data-region="data-requests-table"] .moodle-actionmenu {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<div data-region="categories" class="m-t-3 m-b-1">
|
||||
<h3>{{#str}}categories, tool_dataprivacy{{/str}}</h3>
|
||||
<div class="m-y-1">
|
||||
<button class="btn btn-secondary" data-add-element="category">
|
||||
<button class="btn btn-secondary" data-add-element="category" title="{{#str}}addcategory, tool_dataprivacy{{/str}}">
|
||||
{{#pix}}t/add, moodle, {{#str}}addcategory, tool_dataprivacy{{/str}}{{/pix}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
}
|
||||
}}
|
||||
|
||||
<div classs="container-fluid">
|
||||
<div class="container-fluid">
|
||||
<hr />
|
||||
<div class="row">
|
||||
{{#compliant}}
|
||||
@@ -64,17 +64,17 @@
|
||||
</div>
|
||||
|
||||
{{#compliant}}
|
||||
<div class="hide" data-section="{{raw_component}}" aria-expanded="false">
|
||||
<div class="hide" data-section="{{raw_component}}" aria-expanded="false" role="contentinfo">
|
||||
{{#metadata}}
|
||||
<hr />
|
||||
<div class="p-l-3">
|
||||
<dl class="row">
|
||||
<dt class="span3 col-xs-3">
|
||||
{{#link}}
|
||||
<a href="#{{name}}"><h5 style="word-wrap:break-word">{{name}}</h5></a>
|
||||
<a href="#{{name}}"><strong style="word-wrap:break-word">{{name}}</strong></a>
|
||||
{{/link}}
|
||||
{{^link}}
|
||||
<h5 style="word-wrap:break-word">{{name}}</h5>
|
||||
<strong style="word-wrap:break-word">{{name}}</strong>
|
||||
{{/link}}
|
||||
<div class="small text-muted" style="word-wrap:break-word">{{type}}</div>
|
||||
</dt>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -55,7 +55,7 @@
|
||||
<h3 id="{{plugin_type_raw}}">{{#pix}}t/collapsed, moodle, {{#str}}expandplugintype, tool_dataprivacy{{/str}}{{/pix}}{{plugin_type}}</h3>
|
||||
</a>
|
||||
</div>
|
||||
<div class="hide p-b-1" data-plugintarget="{{plugin_type_raw}}" aria-expanded="false">
|
||||
<div class="hide p-b-1" data-plugintarget="{{plugin_type_raw}}" aria-expanded="false" role="contentinfo">
|
||||
{{#plugins}}
|
||||
{{> tool_dataprivacy/component_status}}
|
||||
{{/plugins}}
|
||||
|
||||
@@ -47,59 +47,68 @@
|
||||
"datarequestsurl": "#"
|
||||
}
|
||||
}}
|
||||
<style>
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<p>{{#str}}emailsalutation, tool_dataprivacy, {{dponame}}{{/str}}</p>
|
||||
<p>{{#str}}requestemailintro, tool_dataprivacy{{/str}}</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{#str}}requesttype, tool_dataprivacy{{/str}}
|
||||
</th>
|
||||
<td>
|
||||
{{requesttype}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{#str}}requestfor, tool_dataprivacy{{/str}}
|
||||
</th>
|
||||
<td>
|
||||
{{requestfor}}
|
||||
</td>
|
||||
</tr>
|
||||
{{^forself}}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{#str}}requestby, tool_dataprivacy{{/str}}
|
||||
</th>
|
||||
<td>
|
||||
{{requestedby}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/forself}}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{#str}}requestcomments, tool_dataprivacy{{/str}}
|
||||
</th>
|
||||
<td>
|
||||
{{{requestcomments}}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{#str}}daterequested, tool_dataprivacy{{/str}}
|
||||
</th>
|
||||
<td>
|
||||
{{requestdate}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr>
|
||||
<a href="{{datarequestsurl}}">{{#str}}viewrequest, tool_dataprivacy{{/str}}</a>
|
||||
</div>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
||||
<title>{{#str}}datarequestemailsubject, tool_dataprivacy, {{requesttype}}{{/str}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p>{{#str}}emailsalutation, tool_dataprivacy, {{dponame}}{{/str}}</p>
|
||||
<p>{{#str}}requestemailintro, tool_dataprivacy{{/str}}</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{#str}}requesttype, tool_dataprivacy{{/str}}
|
||||
</th>
|
||||
<td>
|
||||
{{requesttype}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{#str}}requestfor, tool_dataprivacy{{/str}}
|
||||
</th>
|
||||
<td>
|
||||
{{requestfor}}
|
||||
</td>
|
||||
</tr>
|
||||
{{^forself}}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{#str}}requestby, tool_dataprivacy{{/str}}
|
||||
</th>
|
||||
<td>
|
||||
{{requestedby}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/forself}}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{#str}}requestcomments, tool_dataprivacy{{/str}}
|
||||
</th>
|
||||
<td>
|
||||
{{{requestcomments}}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{#str}}daterequested, tool_dataprivacy{{/str}}
|
||||
</th>
|
||||
<td>
|
||||
{{requestdate}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr>
|
||||
<a href="{{datarequestsurl}}">{{#str}}viewrequest, tool_dataprivacy{{/str}}</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -30,11 +30,17 @@
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"title": "Data request modal title"
|
||||
}
|
||||
}}
|
||||
{{< core/modal }}
|
||||
{{$footer}}
|
||||
<button type="button" class="btn btn-primary" data-action="approve">{{#str}} approve, tool_dataprivacy {{/str}}</button>
|
||||
<button type="button" class="btn btn-secondary" data-action="deny">{{#str}} deny, tool_dataprivacy {{/str}}</button>
|
||||
{{#approvedeny}}
|
||||
<button type="button" class="btn btn-primary" data-action="approve">{{#str}} approve, tool_dataprivacy {{/str}}</button>
|
||||
<button type="button" class="btn btn-secondary" data-action="deny">{{#str}} deny, tool_dataprivacy {{/str}}</button>
|
||||
{{/approvedeny}}
|
||||
{{#canmarkcomplete}}
|
||||
<button type="button" class="btn btn-primary" data-action="complete">{{#str}} markcomplete, tool_dataprivacy {{/str}}</button>
|
||||
{{/canmarkcomplete}}
|
||||
{{/footer}}
|
||||
{{/ core/modal }}
|
||||
|
||||
@@ -26,72 +26,32 @@
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* requests - Array of data requests.
|
||||
* newdatarequesturl string The URL pointing to the data request creation page.
|
||||
* datarequests string The HTML of the data requests table.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": 1,
|
||||
"foruser" : {
|
||||
"fullname": "Oscar Olsen",
|
||||
"profileurl": "#"
|
||||
"newdatarequesturl": "#",
|
||||
"datarequests": "<table><tr><td>This is the table where the list of data requests will be rendered</td></tr></table>",
|
||||
"filter": {
|
||||
"action": "#",
|
||||
"filteroptions": [
|
||||
{
|
||||
"value": "1",
|
||||
"label": "Option 1"
|
||||
},
|
||||
"typenameshort" : "Export",
|
||||
"comments": "I would like to download all of my daughter's personal data",
|
||||
"statuslabelclass": "label-default",
|
||||
"statuslabel": "Pending",
|
||||
"timecreated" : 1517902435
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"foruser" : {
|
||||
"fullname": "Alexandre Denys",
|
||||
"profileurl": "#"
|
||||
{
|
||||
"value": "2",
|
||||
"label": "Option 2",
|
||||
"selected": true
|
||||
},
|
||||
"typenameshort" : "Export",
|
||||
"comments": "Please give me all of the information you have about me...",
|
||||
"statuslabelclass": "label-warning",
|
||||
"statuslabel": "Awaiting completion",
|
||||
"timecreated" : 1517902435
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"foruser" : {
|
||||
"fullname": "Hirondino Moura",
|
||||
"profileurl": "#"
|
||||
},
|
||||
"typenameshort" : "Delete",
|
||||
"comments": "Please delete all of my son's personal data.",
|
||||
"statuslabelclass": "label-success",
|
||||
"statuslabel": "Complete",
|
||||
"timecreated" : 1517902435
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"foruser" : {
|
||||
"fullname": "Florian Krause",
|
||||
"profileurl": "#"
|
||||
},
|
||||
"typenameshort" : "Delete",
|
||||
"comments": "I would like to request for my personal data to be deleted from your site. Thanks!",
|
||||
"statuslabelclass": "label-danger",
|
||||
"statuslabel": "Rejected",
|
||||
"timecreated" : 1517902435
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"foruser" : {
|
||||
"fullname": "Nicklas Sørensen",
|
||||
"profileurl": "#"
|
||||
},
|
||||
"typenameshort" : "Export",
|
||||
"comments": "Please let me download my data",
|
||||
"statuslabelclass": "label-info",
|
||||
"statuslabel": "Processing",
|
||||
"timecreated" : 1517902435
|
||||
}
|
||||
]
|
||||
{
|
||||
"value": "3",
|
||||
"label": "Option 3",
|
||||
"selected": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -101,51 +61,19 @@
|
||||
|
||||
<div data-region="datarequests">
|
||||
<div class="m-t-1 m-b-1">
|
||||
<a href="{{newdatarequesturl}}" class="btn btn-primary" data-action="new-request">
|
||||
{{#str}}newrequest, tool_dataprivacy{{/str}}
|
||||
</a>
|
||||
<div class="pull-right">
|
||||
<a href="{{newdatarequesturl}}" class="btn btn-primary" data-action="new-request">
|
||||
{{#str}}newrequest, tool_dataprivacy{{/str}}
|
||||
</a>
|
||||
</div>
|
||||
{{#filter}}
|
||||
{{>tool_dataprivacy/request_filter}}
|
||||
{{/filter}}
|
||||
</div>
|
||||
|
||||
<div class="m-t-1 m-b-1" data-region="data-requests-table">
|
||||
{{{datarequests}}}
|
||||
</div>
|
||||
<table class="generaltable fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{#str}}requesttype, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}user, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}daterequested, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}requeststatus, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col" colspan="2">{{#str}}message, tool_dataprivacy{{/str}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#requests}}
|
||||
<tr {{!
|
||||
}} data-region="request-node"{{!
|
||||
}} data-id="{{id}}"{{!
|
||||
}} data-type="{{type}}"{{!
|
||||
}} data-status="{{status}}"{{!
|
||||
}}>
|
||||
<td>{{typenameshort}}</td>
|
||||
<td><a href="{{foruser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{foruser.fullname}}</a></td>
|
||||
<td>{{#userdate}} {{timecreated}}, {{#str}} strftimedate {{/str}} {{/userdate}}</td>
|
||||
<td>
|
||||
<span class="label {{statuslabelclass}}">{{statuslabel}}</span>
|
||||
</td>
|
||||
<td>{{#shortentext}}60, {{comments}}{{/shortentext}}</td>
|
||||
<td>
|
||||
{{#actions}}
|
||||
{{> core/action_menu}}
|
||||
{{/actions}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/requests}}
|
||||
{{^requests}}
|
||||
<tr>
|
||||
<td class="text-muted" colspan="6">
|
||||
{{#str}}nodatarequests, tool_dataprivacy{{/str}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/requests}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{#js}}
|
||||
|
||||
@@ -37,7 +37,11 @@
|
||||
"comments": "I would like to download all of my daughter's personal data",
|
||||
"statuslabelclass": "label-default",
|
||||
"statuslabel": "Pending",
|
||||
"timecreated" : 1517902087
|
||||
"timecreated" : 1517902087,
|
||||
"requestedbyuser" : {
|
||||
"fullname": "Martha Smith",
|
||||
"profileurl": "#"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
@@ -45,15 +49,23 @@
|
||||
"comments": "Give me all of the information you have about me, or else...",
|
||||
"statuslabelclass": "label-warning",
|
||||
"statuslabel": "Awaiting completion",
|
||||
"timecreated" : 1517902087
|
||||
"timecreated" : 1517902087,
|
||||
"requestedbyuser" : {
|
||||
"fullname": "Martha Smith",
|
||||
"profileurl": "#"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"typename" : "Data deletion",
|
||||
"comments": "Please delete all of my son's personal data.",
|
||||
"statuslabelclass": "label-success",
|
||||
"statuslabel": "Complete",
|
||||
"timecreated" : 1517902087
|
||||
"statuslabel": "Deleted",
|
||||
"timecreated" : 1517902087,
|
||||
"requestedbyuser" : {
|
||||
"fullname": "Martha Smith",
|
||||
"profileurl": "#"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
@@ -61,7 +73,11 @@
|
||||
"comments": "Delete my data or I'm coming for you...",
|
||||
"statuslabelclass": "label-danger",
|
||||
"statuslabel": "Rejected",
|
||||
"timecreated" : 1517902087
|
||||
"timecreated" : 1517902087,
|
||||
"requestedbyuser" : {
|
||||
"fullname": "Martha Smith",
|
||||
"profileurl": "#"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
@@ -69,7 +85,24 @@
|
||||
"comments": "Please let me download my data",
|
||||
"statuslabelclass": "label-info",
|
||||
"statuslabel": "Processing",
|
||||
"timecreated" : 1517902087
|
||||
"timecreated" : 1517902087,
|
||||
"requestedbyuser" : {
|
||||
"fullname": "Martha Smith",
|
||||
"profileurl": "#"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"typename" : "Data export",
|
||||
"comments": "Please let me download my data",
|
||||
"statuslabelclass": "label",
|
||||
"statuslabel": "Expired",
|
||||
"statuslabeltitle": "Download has expired. Submit a new request if you wish to export your personal data.",
|
||||
"timecreated" : 1517902087,
|
||||
"requestedbyuser" : {
|
||||
"fullname": "Martha Smith",
|
||||
"profileurl": "#"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -90,6 +123,7 @@
|
||||
<tr>
|
||||
<th scope="col">{{#str}}requesttype, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}daterequested, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}requestby, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}requeststatus, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col" colspan="2">{{#str}}message, tool_dataprivacy{{/str}}</th>
|
||||
</tr>
|
||||
@@ -103,9 +137,10 @@
|
||||
}} data-status="{{status}}"{{!
|
||||
}}>
|
||||
<td>{{typename}}</td>
|
||||
<td>{{#userdate}} {{timecreated}}, {{#str}} strftimedate {{/str}} {{/userdate}}</td>
|
||||
<td>{{#userdate}} {{timecreated}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}</td>
|
||||
<td><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></td>
|
||||
<td>
|
||||
<span class="label {{statuslabelclass}}">{{statuslabel}}</span>
|
||||
<span class="label {{statuslabelclass}}" title="{{statuslabeltitle}}">{{statuslabel}}</span>
|
||||
</td>
|
||||
<td>{{comments}}</td>
|
||||
<td>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<div data-region="purposes" class="m-t-3 m-b-1">
|
||||
<h3>{{#str}}purposes, tool_dataprivacy{{/str}}</h3>
|
||||
<div class="m-y-1">
|
||||
<button class="btn btn-secondary" data-add-element="purpose">
|
||||
<button class="btn btn-secondary" data-add-element="purpose" title="{{#str}}addpurpose, tool_dataprivacy{{/str}}">
|
||||
{{#pix}}t/add, moodle, {{#str}}addpurpose, tool_dataprivacy{{/str}}{{/pix}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -44,6 +44,10 @@
|
||||
"canreview": true,
|
||||
"reviewurl": "#",
|
||||
"timecreated": 1517561224,
|
||||
"requestedbyuser" : {
|
||||
"fullname": "Martha Smith",
|
||||
"profileurl": "#"
|
||||
},
|
||||
"statuslabel": "Pending",
|
||||
"statuslabelclass": "label-default",
|
||||
"messagehtml": "<p>Hello,</p><p>I would like to download all of my personal data.</p><p>Thanks!</p>"
|
||||
@@ -63,12 +67,16 @@
|
||||
<a href="mailto:{{foruser.email}}">{{foruser.email}}</a>
|
||||
<div class="clearfix m-t-1 m-b-1">
|
||||
<span class="pull-left m-r-1">
|
||||
<strong>{{#str}}daterequesteddetail, tool_dataprivacy{{/str}}</strong> {{#userdate}} {{timecreated}}, {{#str}} strftimedate {{/str}} {{/userdate}}
|
||||
<strong>{{#str}}daterequesteddetail, tool_dataprivacy{{/str}}</strong> {{#userdate}} {{timecreated}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}
|
||||
</span>
|
||||
<span class="pull-left m-r-1">
|
||||
<strong>{{#str}}statusdetail, tool_dataprivacy{{/str}}</strong>
|
||||
<span class="label {{statuslabelclass}}">{{statuslabel}}</span>
|
||||
</span>
|
||||
<span class="pull-left m-r-1">
|
||||
<strong>{{#str}}requestbydetail, tool_dataprivacy{{/str}}</strong>
|
||||
<span><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></span>
|
||||
</span>
|
||||
</div>
|
||||
{{#canreview}}
|
||||
<!--a href="{{reviewurl}}" class="btn btn-default">{{#str}}reviewdata, tool_dataprivacy{{/str}}</a-->
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template tool_dataprivacy/request_filter
|
||||
|
||||
Template for the request filter element.
|
||||
|
||||
Context variables required for this template:
|
||||
* action string - The action URL for the form.
|
||||
* filteroptions - Array of filter options.
|
||||
* value string - The option value.
|
||||
* label string - The option label.
|
||||
* selected boolean - Whether the option is selected
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"action": "#",
|
||||
"filteroptions": [
|
||||
{
|
||||
"value": "1",
|
||||
"label": "Option 1"
|
||||
},
|
||||
{
|
||||
"value": "2",
|
||||
"label": "Option 2",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"value": "3",
|
||||
"label": "Option 3",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"value": "4",
|
||||
"label": "Option 4"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<form method="post" action="{{action}}" class="m-b-1" role="search" id="request_filter_form">
|
||||
<label for="request-filters" class="sr-only">{{#str}}filters{{/str}}</label>
|
||||
<select name="request-filters[]" id="request-filters" multiple="multiple" class="form-autocomplete-original-select">
|
||||
{{#filteroptions}}
|
||||
<option value="{{value}}" {{#selected}}selected="selected"{{/selected}}>{{{label}}}</option>
|
||||
{{/filteroptions}}
|
||||
</select>
|
||||
<input type="hidden" id="filters-cleared" name="filters-cleared" value="0" />
|
||||
</form>
|
||||
{{#js}}
|
||||
require(['tool_dataprivacy/request_filter'], function(Filter) {
|
||||
Filter.init();
|
||||
});
|
||||
{{/js}}
|
||||
@@ -29,6 +29,7 @@ use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\data_registry;
|
||||
use tool_dataprivacy\expired_context;
|
||||
use tool_dataprivacy\data_request;
|
||||
use tool_dataprivacy\local\helper;
|
||||
use tool_dataprivacy\task\initiate_data_request_task;
|
||||
use tool_dataprivacy\task\process_data_request_task;
|
||||
|
||||
@@ -65,12 +66,12 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
|
||||
$requestid = $datarequest->get('id');
|
||||
|
||||
// Update with a valid status.
|
||||
$result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE);
|
||||
$result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_DOWNLOAD_READY);
|
||||
$this->assertTrue($result);
|
||||
|
||||
// Fetch the request record again.
|
||||
$datarequest = new data_request($requestid);
|
||||
$this->assertEquals(api::DATAREQUEST_STATUS_COMPLETE, $datarequest->get('status'));
|
||||
$this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $datarequest->get('status'));
|
||||
|
||||
// Update with an invalid status.
|
||||
$this->expectException(invalid_persistent_exception::class);
|
||||
@@ -275,6 +276,57 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
|
||||
$this->assertFalse(api::can_manage_data_requests($nondpoincapable->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for api::can_download_data_request_for_user()
|
||||
*/
|
||||
public function test_can_download_data_request_for_user() {
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
// Three victims.
|
||||
$victim1 = $generator->create_user();
|
||||
$victim2 = $generator->create_user();
|
||||
$victim3 = $generator->create_user();
|
||||
|
||||
// Assign a user as victim 1's parent.
|
||||
$systemcontext = \context_system::instance();
|
||||
$parentrole = $generator->create_role();
|
||||
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
|
||||
$parent = $generator->create_user();
|
||||
role_assign($parentrole, $parent->id, \context_user::instance($victim1->id));
|
||||
|
||||
// Assign another user as data access wonder woman.
|
||||
$wonderrole = $generator->create_role();
|
||||
assign_capability('tool/dataprivacy:downloadallrequests', CAP_ALLOW, $wonderrole, $systemcontext);
|
||||
$staff = $generator->create_user();
|
||||
role_assign($wonderrole, $staff->id, $systemcontext);
|
||||
|
||||
// Finally, victim 3 has been naughty; stop them accessing their own data.
|
||||
$naughtyrole = $generator->create_role();
|
||||
assign_capability('tool/dataprivacy:downloadownrequest', CAP_PROHIBIT, $naughtyrole, $systemcontext);
|
||||
role_assign($naughtyrole, $victim3->id, $systemcontext);
|
||||
|
||||
// Victims 1 and 2 can access their own data, regardless of who requested it.
|
||||
$this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $victim1->id));
|
||||
$this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $victim2->id));
|
||||
|
||||
// Victim 3 cannot access his own data.
|
||||
$this->assertFalse(api::can_download_data_request_for_user($victim3->id, $victim3->id, $victim3->id));
|
||||
|
||||
// Victims 1 and 2 cannot access another victim's data.
|
||||
$this->assertFalse(api::can_download_data_request_for_user($victim2->id, $victim1->id, $victim1->id));
|
||||
$this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $victim2->id));
|
||||
|
||||
// Staff can access everyone's data.
|
||||
$this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $staff->id));
|
||||
$this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $staff->id));
|
||||
$this->assertTrue(api::can_download_data_request_for_user($victim3->id, $staff->id, $staff->id));
|
||||
|
||||
// Parent can access victim 1's data only if they requested it.
|
||||
$this->assertTrue(api::can_download_data_request_for_user($victim1->id, $parent->id, $parent->id));
|
||||
$this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $parent->id));
|
||||
$this->assertFalse(api::can_download_data_request_for_user($victim2->id, $parent->id, $parent->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for api::create_data_request()
|
||||
*/
|
||||
@@ -411,42 +463,140 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for api::get_data_requests()
|
||||
* Data provider for \tool_dataprivacy_api_testcase::test_get_data_requests().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function test_get_data_requests() {
|
||||
public function get_data_requests_provider() {
|
||||
$completeonly = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_DOWNLOAD_READY, api::DATAREQUEST_STATUS_DELETED];
|
||||
$completeandcancelled = array_merge($completeonly, [api::DATAREQUEST_STATUS_CANCELLED]);
|
||||
|
||||
return [
|
||||
// Own data requests.
|
||||
['user', false, $completeonly],
|
||||
// Non-DPO fetching all requets.
|
||||
['user', true, $completeonly],
|
||||
// Admin fetching all completed and cancelled requests.
|
||||
['dpo', true, $completeandcancelled],
|
||||
// Admin fetching all completed requests.
|
||||
['dpo', true, $completeonly],
|
||||
// Guest fetching all requests.
|
||||
['guest', true, $completeonly],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for api::get_data_requests()
|
||||
*
|
||||
* @dataProvider get_data_requests_provider
|
||||
* @param string $usertype The type of the user logging in.
|
||||
* @param boolean $fetchall Whether to fetch all records.
|
||||
* @param int[] $statuses Status filters.
|
||||
*/
|
||||
public function test_get_data_requests($usertype, $fetchall, $statuses) {
|
||||
$generator = new testing_data_generator();
|
||||
$user1 = $generator->create_user();
|
||||
$user2 = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
$user3 = $generator->create_user();
|
||||
$user4 = $generator->create_user();
|
||||
$user5 = $generator->create_user();
|
||||
$users = [$user1, $user2, $user3, $user4, $user5];
|
||||
|
||||
// Make a data request as user 1.
|
||||
$this->setUser($user1);
|
||||
$d1 = api::create_data_request($user1->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
// Make a data request as user 2.
|
||||
$this->setUser($user2);
|
||||
$d2 = api::create_data_request($user2->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
switch ($usertype) {
|
||||
case 'user':
|
||||
$loggeduser = $user1;
|
||||
break;
|
||||
case 'dpo':
|
||||
$loggeduser = get_admin();
|
||||
break;
|
||||
case 'guest':
|
||||
$loggeduser = guest_user();
|
||||
break;
|
||||
}
|
||||
|
||||
// Fetching data requests of specific users.
|
||||
$requests = api::get_data_requests($user1->id);
|
||||
$this->assertCount(1, $requests);
|
||||
$datarequest = reset($requests);
|
||||
$this->assertEquals($d1->to_record(), $datarequest->to_record());
|
||||
$comment = 'Data %s request comment by user %d';
|
||||
$exportstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_EXPORT);
|
||||
$deletionstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_DELETE);
|
||||
// Make a data requests for the users.
|
||||
foreach ($users as $user) {
|
||||
$this->setUser($user);
|
||||
api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $exportstring, $user->id));
|
||||
api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $deletionstring, $user->id));
|
||||
}
|
||||
|
||||
$requests = api::get_data_requests($user2->id);
|
||||
$this->assertCount(1, $requests);
|
||||
$datarequest = reset($requests);
|
||||
$this->assertEquals($d2->to_record(), $datarequest->to_record());
|
||||
// Log in as the target user.
|
||||
$this->setUser($loggeduser);
|
||||
// Get records count based on the filters.
|
||||
$userid = $loggeduser->id;
|
||||
if ($fetchall) {
|
||||
$userid = 0;
|
||||
}
|
||||
$count = api::get_data_requests_count($userid);
|
||||
if (api::is_site_dpo($loggeduser->id)) {
|
||||
// DPOs should see all the requests.
|
||||
$this->assertEquals(count($users) * 2, $count);
|
||||
} else {
|
||||
if (empty($userid)) {
|
||||
// There should be no data requests for this user available.
|
||||
$this->assertEquals(0, $count);
|
||||
} else {
|
||||
// There should be only one (request with pending status).
|
||||
$this->assertEquals(2, $count);
|
||||
}
|
||||
}
|
||||
// Get data requests.
|
||||
$requests = api::get_data_requests($userid);
|
||||
// The number of requests should match the count.
|
||||
$this->assertCount($count, $requests);
|
||||
|
||||
// Fetching data requests of all users.
|
||||
// As guest.
|
||||
$this->setGuestUser();
|
||||
$requests = api::get_data_requests();
|
||||
$this->assertEmpty($requests);
|
||||
// Test filtering by status.
|
||||
if ($count && !empty($statuses)) {
|
||||
$filteredcount = api::get_data_requests_count($userid, $statuses);
|
||||
// There should be none as they are all pending.
|
||||
$this->assertEquals(0, $filteredcount);
|
||||
$filteredrequests = api::get_data_requests($userid, $statuses);
|
||||
$this->assertCount($filteredcount, $filteredrequests);
|
||||
|
||||
// As DPO (admin in this case, which is default if no site DPOs are set).
|
||||
$this->setAdminUser();
|
||||
$requests = api::get_data_requests();
|
||||
$this->assertCount(2, $requests);
|
||||
$statuscounts = [];
|
||||
foreach ($statuses as $stat) {
|
||||
$statuscounts[$stat] = 0;
|
||||
}
|
||||
$numstatus = count($statuses);
|
||||
// Get all requests with status filter and update statuses, randomly.
|
||||
foreach ($requests as $request) {
|
||||
if (rand(0, 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($numstatus > 1) {
|
||||
$index = rand(0, $numstatus - 1);
|
||||
$status = $statuses[$index];
|
||||
} else {
|
||||
$status = reset($statuses);
|
||||
}
|
||||
$statuscounts[$status]++;
|
||||
api::update_request_status($request->get('id'), $status);
|
||||
}
|
||||
$total = array_sum($statuscounts);
|
||||
$filteredcount = api::get_data_requests_count($userid, $statuses);
|
||||
$this->assertEquals($total, $filteredcount);
|
||||
$filteredrequests = api::get_data_requests($userid, $statuses);
|
||||
$this->assertCount($filteredcount, $filteredrequests);
|
||||
// Confirm the filtered requests match the status filter(s).
|
||||
foreach ($filteredrequests as $request) {
|
||||
$this->assertContains($request->get('status'), $statuses);
|
||||
}
|
||||
|
||||
if ($numstatus > 1) {
|
||||
// Fetch by individual status to check the numbers match.
|
||||
foreach ($statuses as $status) {
|
||||
$filteredcount = api::get_data_requests_count($userid, [$status]);
|
||||
$this->assertEquals($statuscounts[$status], $filteredcount);
|
||||
$filteredrequests = api::get_data_requests($userid, [$status]);
|
||||
$this->assertCount($filteredcount, $filteredrequests);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -462,6 +612,9 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
|
||||
[api::DATAREQUEST_STATUS_COMPLETE, false],
|
||||
[api::DATAREQUEST_STATUS_CANCELLED, false],
|
||||
[api::DATAREQUEST_STATUS_REJECTED, false],
|
||||
[api::DATAREQUEST_STATUS_DOWNLOAD_READY, false],
|
||||
[api::DATAREQUEST_STATUS_EXPIRED, false],
|
||||
[api::DATAREQUEST_STATUS_DELETED, false],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
@tool @tool_dataprivacy
|
||||
Feature: Contact the privacy officer
|
||||
As a user
|
||||
In order to reach out to the site's privacy officer
|
||||
I need to be able to contact the site's privacy officer in Moodle
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | 1 | s1@example.com |
|
||||
And I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
| contactdataprotectionofficer | 1 |
|
||||
And I log out
|
||||
|
||||
@javascript
|
||||
Scenario: Contacting the privacy officer
|
||||
Given I log in as "student1"
|
||||
And I follow "Profile" in the user menu
|
||||
And I should see "Contact the privacy officer"
|
||||
And I click on "Contact the privacy officer" "link"
|
||||
And I set the field "Message" to "Hello DPO!"
|
||||
And I press "Send"
|
||||
And I should see "Your request has been submitted to the privacy officer"
|
||||
And I click on "Data requests" "link"
|
||||
And I should see "Hello DPO!" in the "General inquiry" "table_row"
|
||||
@@ -0,0 +1,119 @@
|
||||
@tool @tool_dataprivacy
|
||||
Feature: Data delete from the privacy API
|
||||
In order to delete data for users and meet legal requirements
|
||||
As an admin, user, or parent
|
||||
I need to be able to request a user and their data data be deleted
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname |
|
||||
| victim | Victim User | 1 |
|
||||
| parent | Long-suffering | Parent |
|
||||
And the following "roles" exist:
|
||||
| shortname | name | archetype |
|
||||
| tired | Tired | |
|
||||
And the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| tool/dataprivacy:makedatarequestsforchildren | Allow | tired | System | |
|
||||
And the following "role assigns" exist:
|
||||
| user | role | contextlevel | reference |
|
||||
| parent | tired | User | victim |
|
||||
And the following config values are set as admin:
|
||||
| contactdataprotectionofficer | 1 | tool_dataprivacy |
|
||||
|
||||
@javascript
|
||||
Scenario: As admin, delete a user and their data
|
||||
Given I log in as "victim"
|
||||
And I should see "Victim User 1"
|
||||
And I log out
|
||||
|
||||
And I log in as "admin"
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
And I follow "New request"
|
||||
And I set the field "Requesting for" to "Victim User 1"
|
||||
And I set the field "Type" to "Delete all of my personal data"
|
||||
And I press "Save changes"
|
||||
Then I should see "Victim User 1"
|
||||
And I should see "Pending" in the "Victim User 1" "table_row"
|
||||
And I run all adhoc tasks
|
||||
And I reload the page
|
||||
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
|
||||
And I follow "Actions"
|
||||
And I follow "Approve request"
|
||||
And I press "Approve request"
|
||||
And I should see "Approved" in the "Victim User 1" "table_row"
|
||||
And I run all adhoc tasks
|
||||
And I reload the page
|
||||
And I should see "Deleted" in the "Victim User 1" "table_row"
|
||||
|
||||
And I log out
|
||||
And I log in as "victim"
|
||||
And I should see "Invalid login"
|
||||
|
||||
@javascript
|
||||
Scenario: As a student, request deletion of account and data
|
||||
Given I log in as "victim"
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Data requests"
|
||||
And I follow "New request"
|
||||
And I set the field "Type" to "Delete all of my personal data"
|
||||
And I press "Save changes"
|
||||
Then I should see "Delete all of my personal data"
|
||||
And I should see "Pending" in the "Delete all of my personal data" "table_row"
|
||||
And I run all adhoc tasks
|
||||
And I reload the page
|
||||
And I should see "Awaiting approval" in the "Delete all of my personal data" "table_row"
|
||||
|
||||
And I log out
|
||||
And I log in as "admin"
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
And I follow "Actions"
|
||||
And I follow "Approve request"
|
||||
And I press "Approve request"
|
||||
|
||||
And I log out
|
||||
And I log in as "victim"
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Data requests"
|
||||
And I should see "Approved" in the "Delete all of my personal data" "table_row"
|
||||
And I run all adhoc tasks
|
||||
And I reload the page
|
||||
And I should see "Your session has timed out"
|
||||
And I log in as "victim"
|
||||
And I should see "Invalid login"
|
||||
|
||||
And I log in as "admin"
|
||||
And I am on site homepage
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
And I should see "Deleted"
|
||||
|
||||
@javascript
|
||||
Scenario: As a parent, request account and data deletion for my child
|
||||
Given I log in as "parent"
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Data requests"
|
||||
And I follow "New request"
|
||||
And I set the field "Requesting for" to "Victim User 1"
|
||||
And I set the field "Type" to "Delete all of my personal data"
|
||||
And I press "Save changes"
|
||||
Then I should see "Victim User 1"
|
||||
And I should see "Pending" in the "Victim User 1" "table_row"
|
||||
And I run all adhoc tasks
|
||||
And I reload the page
|
||||
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
|
||||
|
||||
And I log out
|
||||
And I log in as "admin"
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
And I follow "Actions"
|
||||
And I follow "Approve request"
|
||||
And I press "Approve request"
|
||||
|
||||
And I log out
|
||||
And I log in as "parent"
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Data requests"
|
||||
And I should see "Approved" in the "Victim User 1" "table_row"
|
||||
And I run all adhoc tasks
|
||||
And I reload the page
|
||||
And I should see "You don't have any personal data requests"
|
||||
@@ -0,0 +1,131 @@
|
||||
@tool @tool_dataprivacy
|
||||
Feature: Data export from the privacy API
|
||||
In order to export data for users and meet legal requirements
|
||||
As an admin, user, or parent
|
||||
I need to be able to export data for a user
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname |
|
||||
| victim | Victim User | 1 |
|
||||
| parent | Long-suffering | Parent |
|
||||
And the following "roles" exist:
|
||||
| shortname | name | archetype |
|
||||
| tired | Tired | |
|
||||
And the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| tool/dataprivacy:makedatarequestsforchildren | Allow | tired | System | |
|
||||
And the following "role assigns" exist:
|
||||
| user | role | contextlevel | reference |
|
||||
| parent | tired | User | victim |
|
||||
And the following config values are set as admin:
|
||||
| contactdataprotectionofficer | 1 | tool_dataprivacy |
|
||||
| privacyrequestexpiry | 55 | tool_dataprivacy |
|
||||
|
||||
@javascript
|
||||
Scenario: As admin, export data for a user and download it, unless it has expired
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
And I follow "New request"
|
||||
And I set the field "Requesting for" to "Victim User 1"
|
||||
And I press "Save changes"
|
||||
Then I should see "Victim User 1"
|
||||
And I should see "Pending" in the "Victim User 1" "table_row"
|
||||
And I run all adhoc tasks
|
||||
And I reload the page
|
||||
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
|
||||
And I follow "Actions"
|
||||
And I follow "Approve request"
|
||||
And I press "Approve request"
|
||||
And I should see "Approved" in the "Victim User 1" "table_row"
|
||||
And I run all adhoc tasks
|
||||
And I reload the page
|
||||
And I should see "Download ready" in the "Victim User 1" "table_row"
|
||||
And I follow "Actions"
|
||||
And following "Download" should download between "1" and "100000" bytes
|
||||
And the following config values are set as admin:
|
||||
| privacyrequestexpiry | 1 | tool_dataprivacy |
|
||||
And I wait "1" seconds
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
And I should see "Expired" in the "Victim User 1" "table_row"
|
||||
And I follow "Actions"
|
||||
And I should not see "Download"
|
||||
|
||||
@javascript
|
||||
Scenario: As a student, request data export and then download it when approved, unless it has expired
|
||||
Given I log in as "victim"
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Data requests"
|
||||
And I follow "New request"
|
||||
And I press "Save changes"
|
||||
Then I should see "Export all of my personal data"
|
||||
And I should see "Pending" in the "Export all of my personal data" "table_row"
|
||||
And I run all adhoc tasks
|
||||
And I reload the page
|
||||
And I should see "Awaiting approval" in the "Export all of my personal data" "table_row"
|
||||
|
||||
And I log out
|
||||
And I log in as "admin"
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
And I follow "Actions"
|
||||
And I follow "Approve request"
|
||||
And I press "Approve request"
|
||||
|
||||
And I log out
|
||||
And I log in as "victim"
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Data requests"
|
||||
And I should see "Approved" in the "Export all of my personal data" "table_row"
|
||||
And I run all adhoc tasks
|
||||
And I reload the page
|
||||
And I should see "Download ready" in the "Export all of my personal data" "table_row"
|
||||
And I follow "Actions"
|
||||
And following "Download" should download between "1" and "100000" bytes
|
||||
|
||||
And the following config values are set as admin:
|
||||
| privacyrequestexpiry | 1 | tool_dataprivacy |
|
||||
And I wait "1" seconds
|
||||
And I reload the page
|
||||
|
||||
And I should see "Expired" in the "Export all of my personal data" "table_row"
|
||||
And I should not see "Actions"
|
||||
|
||||
@javascript
|
||||
Scenario: As a parent, request data export for my child because I don't trust the little blighter
|
||||
Given I log in as "parent"
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Data requests"
|
||||
And I follow "New request"
|
||||
And I set the field "Requesting for" to "Victim User 1"
|
||||
And I press "Save changes"
|
||||
Then I should see "Victim User 1"
|
||||
And I should see "Pending" in the "Victim User 1" "table_row"
|
||||
And I run all adhoc tasks
|
||||
And I reload the page
|
||||
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
|
||||
|
||||
And I log out
|
||||
And I log in as "admin"
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
And I follow "Actions"
|
||||
And I follow "Approve request"
|
||||
And I press "Approve request"
|
||||
|
||||
And I log out
|
||||
And I log in as "parent"
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Data requests"
|
||||
And I should see "Approved" in the "Victim User 1" "table_row"
|
||||
And I run all adhoc tasks
|
||||
And I reload the page
|
||||
And I should see "Download ready" in the "Victim User 1" "table_row"
|
||||
And I follow "Actions"
|
||||
And following "Download" should download between "1" and "100000" bytes
|
||||
|
||||
And the following config values are set as admin:
|
||||
| privacyrequestexpiry | 1 | tool_dataprivacy |
|
||||
And I wait "1" seconds
|
||||
And I reload the page
|
||||
|
||||
And I should see "Expired" in the "Victim User 1" "table_row"
|
||||
And I should not see "Actions"
|
||||
@@ -0,0 +1,34 @@
|
||||
@tool @tool_dataprivacy @javascript
|
||||
Feature: Manage data categories
|
||||
As the privacy officer
|
||||
In order to manage the data registry
|
||||
I need to be able to manage the data categories for the data registry
|
||||
|
||||
Background:
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Edit" "link"
|
||||
And I choose "Categories" in the open action menu
|
||||
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 press "Save"
|
||||
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"
|
||||
|
||||
Scenario: Update a data category
|
||||
Given I click on "Actions" "link" in the "Category 1" "table_row"
|
||||
And I choose "Edit" in the open action menu
|
||||
And I set the field "Name" to "Category 1 edited"
|
||||
And I set the field "Description" to "Category 1 description edited"
|
||||
When I press "Save changes"
|
||||
Then I should see "Category 1 edited" in the "List of data categories" "table"
|
||||
And I should see "Category 1 description edited" in the "List of data categories" "table"
|
||||
|
||||
Scenario: Delete a data category
|
||||
Given I click on "Actions" "link" in the "Category 1" "table_row"
|
||||
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 press "Delete"
|
||||
Then I should not see "Category 1" in the "List of data categories" "table"
|
||||
@@ -0,0 +1,58 @@
|
||||
@tool @tool_dataprivacy
|
||||
Feature: Manage data requests
|
||||
As the privacy officer
|
||||
In order to address the privacy-related requests
|
||||
I need to be able to manage the data requests of the site's users
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | John | Doe | s1@example.com |
|
||||
| student2 | Jane | Doe | s2@example.com |
|
||||
And I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
| contactdataprotectionofficer | 1 |
|
||||
And I log out
|
||||
|
||||
@javascript
|
||||
Scenario: Marking general enquiries as complete
|
||||
Given I log in as "student1"
|
||||
And I follow "Profile" in the user menu
|
||||
And I should see "Contact the privacy officer"
|
||||
And I click on "Contact the privacy officer" "link"
|
||||
And I set the field "Message" to "Hi PO! Can others access my information on your site?"
|
||||
And I press "Send"
|
||||
And I should see "Your request has been submitted to the privacy officer"
|
||||
And I log out
|
||||
And I log in as "student2"
|
||||
And I follow "Profile" in the user menu
|
||||
And I click on "Contact the privacy officer" "link"
|
||||
And I set the field "Message" to "Dear Mr. Privacy Officer, I'd like to know more about GDPR. Thanks!"
|
||||
And I press "Send"
|
||||
And I should see "Your request has been submitted to the privacy officer"
|
||||
And I log out
|
||||
When I log in as "admin"
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
Then I should see "Hi PO!" in the "John Doe" "table_row"
|
||||
And I should see "Dear Mr. Privacy Officer" in the "Jane Doe" "table_row"
|
||||
And I click on "Actions" "link" in the "John Doe" "table_row"
|
||||
And I should see "View the request"
|
||||
And I should see "Mark as complete"
|
||||
And I choose "View the request" in the open action menu
|
||||
And I should see "Hi PO! Can others access my information on your site?"
|
||||
And I press "Mark as complete"
|
||||
And I wait until the page is ready
|
||||
And I should see "Complete" in the "John Doe" "table_row"
|
||||
And I click on "Actions" "link" in the "John Doe" "table_row"
|
||||
And I should see "View the request"
|
||||
But I should not see "Mark as complete"
|
||||
And I press key "27" in ".moodle-actionmenu" "css_element"
|
||||
And I click on "Actions" "link" in the "Jane Doe" "table_row"
|
||||
And I choose "Mark as complete" in the open action menu
|
||||
And I should see "Do you really want to mark this user enquiry as complete?"
|
||||
And I press "Mark as complete"
|
||||
And I wait until the page is ready
|
||||
And I should see "Complete" in the "Jane Doe" "table_row"
|
||||
And I click on "Actions" "link" in the "Jane Doe" "table_row"
|
||||
And I should see "View the request"
|
||||
But I should not see "Mark as complete"
|
||||
@@ -0,0 +1,56 @@
|
||||
@tool @tool_dataprivacy @javascript
|
||||
Feature: Manage data storage purposes
|
||||
As the privacy officer
|
||||
In order to manage the data registry
|
||||
I need to be able to manage the data storage purposes for the data registry
|
||||
|
||||
Background:
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Edit" "link"
|
||||
And I choose "Purposes" in the open action menu
|
||||
And I press "Add purpose"
|
||||
And I set the field "Name" to "Purpose 1"
|
||||
And I set the field "Description" to "Purpose 1 description"
|
||||
And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
|
||||
And I click on "Contract (GDPR Art. 6.1(b))" "list_item"
|
||||
And I click on "Legal obligation (GDPR Art 6.1(c))" "list_item"
|
||||
And I press key "27" in the field "Lawful bases"
|
||||
And I click on ".form-autocomplete-downarrow" "css_element" in the "Sensitive personal data processing reasons" "form_row"
|
||||
And I click on "Explicit consent (GDPR Art. 9.2(a))" "list_item"
|
||||
And I press key "27" in the field "Sensitive personal data processing reasons"
|
||||
And I set the field "retentionperiodnumber" to "2"
|
||||
When I press "Save"
|
||||
Then I should see "Purpose 1" in the "List of data purposes" "table"
|
||||
And I should see "Contract (GDPR Art. 6.1(b))" in the "Purpose 1" "table_row"
|
||||
And I should see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1" "table_row"
|
||||
And I should see "Explicit consent (GDPR Art. 9.2(a))" in the "Purpose 1" "table_row"
|
||||
And I should see "2 years" in the "Purpose 1" "table_row"
|
||||
And I should see "No" in the "Purpose 1" "table_row"
|
||||
|
||||
Scenario: Update a data storage purpose
|
||||
Given I click on "Actions" "link" in the "Purpose 1" "table_row"
|
||||
And I choose "Edit" in the open action menu
|
||||
And I set the field "Name" to "Purpose 1 edited"
|
||||
And I set the field "Description" to "Purpose 1 description edited"
|
||||
And I click on "Legal obligation (GDPR Art 6.1(c))" "text" in the ".form-autocomplete-selection" "css_element"
|
||||
And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
|
||||
And I click on "Vital interests (GDPR Art. 6.1(d))" "list_item"
|
||||
And I press key "27" in the field "Lawful bases"
|
||||
And I set the field "retentionperiodnumber" to "3"
|
||||
And I click on "protected" "checkbox"
|
||||
When I press "Save changes"
|
||||
Then I should see "Purpose 1 edited" in the "List of data purposes" "table"
|
||||
And I should see "Purpose 1 description edited" in the "Purpose 1 edited" "table_row"
|
||||
And I should see "Vital interests (GDPR Art. 6.1(d))" in the "Purpose 1 edited" "table_row"
|
||||
And I should see "3 years" in the "Purpose 1 edited" "table_row"
|
||||
But I should not see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1 edited" "table_row"
|
||||
And I should not see "No" in the "Purpose 1 edited" "table_row"
|
||||
|
||||
Scenario: Delete a data storage purpose
|
||||
Given I click on "Actions" "link" in the "Purpose 1" "table_row"
|
||||
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 press "Delete"
|
||||
Then I should not see "Purpose 1" in the "List of data purposes" "table"
|
||||
@@ -0,0 +1,64 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Parent class for tests which need data privacy functionality.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Michael Hawkins
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Parent class for tests which need data privacy functionality.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Michael Hawkins
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class data_privacy_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Assign one or more user IDs as site DPO
|
||||
*
|
||||
* @param stdClass|array $users User ID or array of user IDs to be assigned as site DPO
|
||||
* @return void
|
||||
*/
|
||||
protected function assign_site_dpo($users) {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
|
||||
if (!is_array($users)) {
|
||||
$users = array($users);
|
||||
}
|
||||
|
||||
$context = context_system::instance();
|
||||
|
||||
// Give the manager role with the capability to manage data requests.
|
||||
$managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
|
||||
assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
|
||||
|
||||
// Assign user(s) as manager.
|
||||
foreach ($users as $user) {
|
||||
role_assign($managerroleid, $user->id, $context->id);
|
||||
}
|
||||
|
||||
// Only map the manager role to the DPO role.
|
||||
set_config('dporoles', $managerroleid, 'tool_dataprivacy');
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
$purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']);
|
||||
$purpose2 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'P1000Y', 'lawfulbases' => 'gdpr_art_6_1_b']);
|
||||
$purpose2 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'P1Y', 'lawfulbases' => 'gdpr_art_6_1_b']);
|
||||
$cat = api::create_category((object)['name' => 'a']);
|
||||
|
||||
$record = (object)[
|
||||
@@ -165,8 +165,12 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
|
||||
// Old course.
|
||||
$course2 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => '2']);
|
||||
// Course finished last week (so purpose1 retention period does delete stuff but purpose2 retention period does not).
|
||||
$dt = new \DateTime();
|
||||
$di = new \DateInterval('P7D');
|
||||
$dt->sub($di);
|
||||
|
||||
$course2 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => $dt->getTimestamp()]);
|
||||
$forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
|
||||
$forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Expired data requests tests.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Michael Hawkins
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\data_request;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
global $CFG;
|
||||
|
||||
require_once('data_privacy_testcase.php');
|
||||
|
||||
/**
|
||||
* Expired data requests tests.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Michael Hawkins
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_dataprivacy_expired_data_requests_testcase extends data_privacy_testcase {
|
||||
|
||||
/**
|
||||
* Test tearDown.
|
||||
*/
|
||||
public function tearDown() {
|
||||
\core_privacy\local\request\writer::reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test finding and deleting expired data requests
|
||||
*/
|
||||
public function test_data_request_expiry() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
\core_privacy\local\request\writer::setup_real_writer_instance();
|
||||
|
||||
// Set up test users.
|
||||
$this->setAdminUser();
|
||||
$studentuser = $this->getDataGenerator()->create_user();
|
||||
$studentusercontext = context_user::instance($studentuser->id);
|
||||
|
||||
$dpouser = $this->getDataGenerator()->create_user();
|
||||
$this->assign_site_dpo($dpouser);
|
||||
|
||||
// Set request expiry to 5 minutes.
|
||||
set_config('privacyrequestexpiry', 300, 'tool_dataprivacy');
|
||||
|
||||
// Create and approve data request.
|
||||
$this->setUser($studentuser->id);
|
||||
$datarequest = api::create_data_request($studentuser->id, api::DATAREQUEST_TYPE_EXPORT);
|
||||
$this->setAdminUser();
|
||||
ob_start();
|
||||
$this->runAdhocTasks('\tool_dataprivacy\task\initiate_data_request_task');
|
||||
$requestid = $datarequest->get('id');
|
||||
$this->setUser($dpouser->id);
|
||||
api::approve_data_request($requestid);
|
||||
$this->setAdminUser();
|
||||
$this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task');
|
||||
ob_end_clean();
|
||||
|
||||
// Confirm approved and exported.
|
||||
$request = new data_request($requestid);
|
||||
$this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
|
||||
$fileconditions = array(
|
||||
'userid' => $studentuser->id,
|
||||
'component' => 'tool_dataprivacy',
|
||||
'filearea' => 'export',
|
||||
'itemid' => $requestid,
|
||||
'contextid' => $studentusercontext->id,
|
||||
);
|
||||
$this->assertEquals(2, $DB->count_records('files', $fileconditions));
|
||||
|
||||
// Run expiry deletion - should not affect test export.
|
||||
$expiredrequests = data_request::get_expired_requests();
|
||||
$this->assertEquals(0, count($expiredrequests));
|
||||
data_request::expire($expiredrequests);
|
||||
|
||||
// Confirm test export was not deleted.
|
||||
$request = new data_request($requestid);
|
||||
$this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
|
||||
$this->assertEquals(2, $DB->count_records('files', $fileconditions));
|
||||
|
||||
// Change request expiry to 1 second and allow it to elapse.
|
||||
set_config('privacyrequestexpiry', 1, 'tool_dataprivacy');
|
||||
$this->waitForSecond();
|
||||
|
||||
// Re-run expiry deletion, confirm the request expires and export is deleted.
|
||||
$expiredrequests = data_request::get_expired_requests();
|
||||
$this->assertEquals(1, count($expiredrequests));
|
||||
data_request::expire($expiredrequests);
|
||||
|
||||
$request = new data_request($requestid);
|
||||
$this->assertEquals(api::DATAREQUEST_STATUS_EXPIRED, $request->get('status'));
|
||||
$this->assertEquals(0, $DB->count_records('files', $fileconditions));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test for \tool_dataprivacy\data_request::is_expired()
|
||||
* Tests for the expected request status to protect from false positive/negative,
|
||||
* then tests is_expired() is returning the expected response.
|
||||
*/
|
||||
public function test_is_expired() {
|
||||
$this->resetAfterTest();
|
||||
\core_privacy\local\request\writer::setup_real_writer_instance();
|
||||
|
||||
// Set request expiry beyond this test.
|
||||
set_config('privacyrequestexpiry', 20, 'tool_dataprivacy');
|
||||
|
||||
$admin = get_admin();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Create export request.
|
||||
$datarequest = api::create_data_request($admin->id, api::DATAREQUEST_TYPE_EXPORT);
|
||||
$requestid = $datarequest->get('id');
|
||||
|
||||
// Approve the request.
|
||||
ob_start();
|
||||
$this->runAdhocTasks('\tool_dataprivacy\task\initiate_data_request_task');
|
||||
$this->setAdminUser();
|
||||
api::approve_data_request($requestid);
|
||||
$this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task');
|
||||
ob_end_clean();
|
||||
|
||||
// Test Download ready (not expired) response.
|
||||
$request = new data_request($requestid);
|
||||
$this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
|
||||
$result = data_request::is_expired($request);
|
||||
$this->assertFalse($result);
|
||||
|
||||
// Let request expiry time lapse.
|
||||
set_config('privacyrequestexpiry', 1, 'tool_dataprivacy');
|
||||
$this->waitForSecond();
|
||||
|
||||
// Test Download ready (time expired) response.
|
||||
$request = new data_request($requestid);
|
||||
$this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
|
||||
$result = data_request::is_expired($request);
|
||||
$this->assertTrue($result);
|
||||
|
||||
// Run the expiry task to properly expire the request.
|
||||
ob_start();
|
||||
$task = \core\task\manager::get_scheduled_task('\tool_dataprivacy\task\delete_expired_requests');
|
||||
$task->execute();
|
||||
ob_end_clean();
|
||||
|
||||
// Test Expired response status response.
|
||||
$request = new data_request($requestid);
|
||||
$this->assertEquals(api::DATAREQUEST_STATUS_EXPIRED, $request->get('status'));
|
||||
$result = data_request::is_expired($request);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
require_once('data_privacy_testcase.php');
|
||||
|
||||
/**
|
||||
* API tests.
|
||||
@@ -31,35 +32,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_dataprivacy_manager_observer_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Helper to set andn return two users who are DPOs.
|
||||
*/
|
||||
protected function setup_site_dpos() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$u1 = $this->getDataGenerator()->create_user();
|
||||
$u2 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$context = context_system::instance();
|
||||
|
||||
// Give the manager role with the capability to manage data requests.
|
||||
$managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
|
||||
assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
|
||||
|
||||
// Assign both users as manager.
|
||||
role_assign($managerroleid, $u1->id, $context->id);
|
||||
role_assign($managerroleid, $u2->id, $context->id);
|
||||
|
||||
// Only map the manager role to the DPO role.
|
||||
set_config('dporoles', $managerroleid, 'tool_dataprivacy');
|
||||
|
||||
return \tool_dataprivacy\api::get_site_dpos();
|
||||
}
|
||||
|
||||
class tool_dataprivacy_manager_observer_testcase extends data_privacy_testcase {
|
||||
/**
|
||||
* Ensure that when users are configured as DPO, they are sent an message upon failure.
|
||||
*/
|
||||
@@ -69,8 +42,11 @@ class tool_dataprivacy_manager_observer_testcase extends advanced_testcase {
|
||||
// Create another user who is not a DPO.
|
||||
$this->getDataGenerator()->create_user();
|
||||
|
||||
// Create the DPOs.
|
||||
$dpos = $this->setup_site_dpos();
|
||||
// Create two DPOs.
|
||||
$dpo1 = $this->getDataGenerator()->create_user();
|
||||
$dpo2 = $this->getDataGenerator()->create_user();
|
||||
$this->assign_site_dpo(array($dpo1, $dpo2));
|
||||
$dpos = \tool_dataprivacy\api::get_site_dpos();
|
||||
|
||||
$observer = new \tool_dataprivacy\manager_observer();
|
||||
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
$plugin->version = 2018051401;
|
||||
$plugin->version = 2018051407;
|
||||
$plugin->requires = 2018050800; // Moodle 3.5dev (Build 2018031600) and upwards.
|
||||
$plugin->component = 'tool_dataprivacy';
|
||||
|
||||
@@ -99,8 +99,8 @@ class helper {
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'timecreated' => transform::datetime($record->timecreated),
|
||||
'ip' => $record->ip,
|
||||
'origin' => static::transform_origin($record->origin),
|
||||
'ip' => $isauthor ? $record->ip : '',
|
||||
'other' => $other ? $other : []
|
||||
];
|
||||
|
||||
|
||||
@@ -74,7 +74,9 @@ class provider implements
|
||||
* @param approved_contextlist $contextlist The approved contexts to export information for.
|
||||
*/
|
||||
public static function export_user_data(approved_contextlist $contextlist) {
|
||||
static::call_subplugins_method_with_args('export_user_data', [$contextlist]);
|
||||
if (get_config('tool_log', 'exportlog')) {
|
||||
static::call_subplugins_method_with_args('export_user_data', [$contextlist]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,5 +42,8 @@ function xmldb_tool_log_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v3.4.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
// Automatically generated Moodle v3.5.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
$string['actlogshdr'] = 'Available log stores';
|
||||
$string['configlogplugins'] = 'Please enable all required plugins and arrange them in appropriate order.';
|
||||
$string['exportlog'] = 'Include logs when exporting.';
|
||||
$string['exportlogdetail'] = 'Include logs that relate to the user when exporting.';
|
||||
$string['logging'] = 'Logging';
|
||||
$string['managelogging'] = 'Manage log stores';
|
||||
$string['pluginname'] = 'Log store manager';
|
||||
|
||||
@@ -25,6 +25,16 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
if ($hassiteconfig) {
|
||||
|
||||
$privacysettings = $ADMIN->locate('privacysettings');
|
||||
|
||||
if ($ADMIN->fulltree) {
|
||||
$privacysettings->add(new admin_setting_configcheckbox('tool_log/exportlog',
|
||||
new lang_string('exportlog', 'tool_log'),
|
||||
new lang_string('exportlogdetail', 'tool_log'), 1)
|
||||
);
|
||||
}
|
||||
|
||||
$ADMIN->add('modules', new admin_category('logging', new lang_string('logging', 'tool_log')));
|
||||
|
||||
$temp = new admin_settingpage('managelogging', new lang_string('managelogging', 'tool_log'));
|
||||
|
||||
@@ -36,5 +36,8 @@ function xmldb_logstore_database_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v3.4.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
// Automatically generated Moodle v3.5.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ $string['privacy:metadata:log:origin'] = 'The origin of the event';
|
||||
$string['privacy:metadata:log:other'] = 'Additional information about the event';
|
||||
$string['privacy:metadata:log:realuserid'] = 'The ID of the real user behind the event, when masquerading a user.';
|
||||
$string['privacy:metadata:log:relateduserid'] = 'The ID of a user related to this event';
|
||||
$string['privacy:metadata:log:timecreated'] = 'The time at which the event occurred';
|
||||
$string['privacy:metadata:log:timecreated'] = 'The time when the event occurred';
|
||||
$string['privacy:metadata:log:userid'] = 'The ID of the user who triggered this event';
|
||||
$string['read'] = 'Read';
|
||||
$string['tablenotfound'] = 'Specified table was not found';
|
||||
|
||||
@@ -31,7 +31,7 @@ $string['privacy:metadata:log'] = 'A collection of past events';
|
||||
$string['privacy:metadata:log:action'] = 'A description of the action';
|
||||
$string['privacy:metadata:log:info'] = 'Additional information';
|
||||
$string['privacy:metadata:log:ip'] = 'The IP address used at the time of the event';
|
||||
$string['privacy:metadata:log:time'] = 'The date at wich the action took place';
|
||||
$string['privacy:metadata:log:time'] = 'The time when the action took place';
|
||||
$string['privacy:metadata:log:url'] = 'The URL related to the event';
|
||||
$string['privacy:metadata:log:userid'] = 'The ID of the user who performed the action';
|
||||
$string['taskcleanup'] = 'Legacy log table cleanup';
|
||||
|
||||
@@ -36,5 +36,8 @@ function xmldb_logstore_standard_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v3.4.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
// Automatically generated Moodle v3.5.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user