Compare commits
630 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9650f3f56f | |||
| d44c73e438 | |||
| 3020929da8 | |||
| 3f4f2da9e0 | |||
| 5a89ac9640 | |||
| 6bbf56b9e2 | |||
| 6be97cb9ac | |||
| 2fc307e4d4 | |||
| c426429a08 | |||
| b848c97164 | |||
| df5502034a | |||
| c2c3db84b1 | |||
| 52bd62f2be | |||
| 53392df219 | |||
| 344386f873 | |||
| 8a2e9542c9 | |||
| 1f01b678df | |||
| 90740716be | |||
| 2cebb87682 | |||
| 907b377e51 | |||
| b77dcd23d8 | |||
| 1a3612bb5b | |||
| fba7dcd90a | |||
| e836242e1c | |||
| 6f05d22622 | |||
| 141271b726 | |||
| aae3a2fdd4 | |||
| abd634d64c | |||
| e24f8994b5 | |||
| d16e730116 | |||
| 3cc11668c8 | |||
| 3c72d44b18 | |||
| 4546269170 | |||
| ad4dfb1e8c | |||
| 4f6373bf05 | |||
| 31c05704b8 | |||
| 384c4c29db | |||
| 2282d6932b | |||
| 47265ccf62 | |||
| 2d0d9a4748 | |||
| 0cbb46b944 | |||
| 3dcf5f9930 | |||
| 3456505930 | |||
| 18c80ae8a5 | |||
| 4fccbafa46 | |||
| 11fc8b3d02 | |||
| 4e190d7eae | |||
| acd27d90c8 | |||
| d81447751e | |||
| eb3df7ecad | |||
| a9d153e61d | |||
| c2c8ce4b6d | |||
| 27f608a5c9 | |||
| 181cf34341 | |||
| 56c15f5c99 | |||
| 87711249c3 | |||
| 0a03b2fd63 | |||
| 398c4c107d | |||
| 5789961754 | |||
| 587a8a9732 | |||
| 53d0efba1f | |||
| 16045c4846 | |||
| 57d2a28425 | |||
| 33cbe249dd | |||
| 82e23b9443 | |||
| ea1d1cf9e1 | |||
| 7bd7754ad0 | |||
| 734ce33b99 | |||
| 4f19471b9e | |||
| 78275d44f0 | |||
| cfde0b8d38 | |||
| 74746ce412 | |||
| 5dd590a3bd | |||
| 7b97be9f25 | |||
| aaf4b2b489 | |||
| 5b1d246758 | |||
| 3aef542a35 | |||
| 07b8bb1ae7 | |||
| 18c55facc0 | |||
| e8a62dc4c2 | |||
| b63c41d762 | |||
| 1aa56ceaca | |||
| fdf3d59a00 | |||
| f648211569 | |||
| f8de60ce57 | |||
| 48b2284317 | |||
| 1720738ca9 | |||
| f6d48eeb87 | |||
| 66774319b3 | |||
| c3238ac39b | |||
| a922416edb | |||
| fef96d285e | |||
| bd38b68684 | |||
| 8a5095d453 | |||
| c9cd95223a | |||
| 80221982fa | |||
| 03edaa4290 | |||
| 0401ae362c | |||
| f5fcf31947 | |||
| e5e3cb4577 | |||
| 999c187559 | |||
| b5fa0b3893 | |||
| 9e75d8d7fc | |||
| 0f908de1e3 | |||
| bbd79a469a | |||
| a67ed33ed3 | |||
| f757055b9f | |||
| 1b1465210b | |||
| f06b51262f | |||
| e4a6f46527 | |||
| ec12901c8c | |||
| b871a7a7e1 | |||
| 876302149f | |||
| 1164fa8370 | |||
| e087069258 | |||
| 8275b2d502 | |||
| c49c373209 | |||
| 2e80552524 | |||
| d9cab9de67 | |||
| 6546fd1043 | |||
| 4c39ccf8a3 | |||
| 270ba66408 | |||
| 8fdf8a4ed5 | |||
| 9d7e968130 | |||
| 668e1a2ade | |||
| 8e3bb05807 | |||
| 6d67532717 | |||
| 529d402ca6 | |||
| 6c9d494f35 | |||
| 327acfdadb | |||
| d1e834866e | |||
| fc8544f5a8 | |||
| cdb82d9b38 | |||
| fb14aa9291 | |||
| b8d51a20e4 | |||
| 6e3c97c71c | |||
| b5b4734ba0 | |||
| 14b0c3f56c | |||
| 6a8a4cb217 | |||
| ec9b96c5b3 | |||
| a273b993e9 | |||
| 3758b9c6ab | |||
| 693929f632 | |||
| 529f4cff0e | |||
| 052d678465 | |||
| f00facd901 | |||
| 2c8c29f2d5 | |||
| ce6d64be15 | |||
| f08e3a0cb9 | |||
| 6adc784d16 | |||
| 054e9ecbe4 | |||
| 5190239c44 | |||
| e5e418338e | |||
| 1d56c59038 | |||
| db1002a36a | |||
| 8e8029a4d0 | |||
| 22aabbbc48 | |||
| 18f218dc3a | |||
| e48cc26687 | |||
| 0c3857ca51 | |||
| a5f813e039 | |||
| 6ac295cd26 | |||
| 41a0a8f97f | |||
| d59a7b781f | |||
| 6be14b1ff0 | |||
| ce6f4feac9 | |||
| 42487b3544 | |||
| cef56ddf5e | |||
| 28bb3a62bb | |||
| 0878ecb537 | |||
| 6997ab6b25 | |||
| ab192796be | |||
| 3623efa604 | |||
| 2ac7cdf32a | |||
| 7fb9023bec | |||
| ea5f2ed91d | |||
| a8e3df906d | |||
| daf0893181 | |||
| 8f3b93f686 | |||
| ed2d5791ed | |||
| 75bcad753d | |||
| 8f57f1940f | |||
| bacac1e7e0 | |||
| 496281a0b9 | |||
| 4d82bfa076 | |||
| aa3befa1e1 | |||
| 7b7665f35f | |||
| 273486c49e | |||
| d85a4a9900 | |||
| 0b62f4608f | |||
| 2115bda424 | |||
| 7db764172f | |||
| 9faabb3400 | |||
| a168c3ed17 | |||
| 41a03384f7 | |||
| ba27222184 | |||
| c80fbc1762 | |||
| 1955206566 | |||
| 3899e71bf2 | |||
| ea40b6fb60 | |||
| e606b429f3 | |||
| 3d4bef51fa | |||
| 95733c47af | |||
| 030cc62697 | |||
| 465256bf63 | |||
| 80ea64da26 | |||
| 92efc5c352 | |||
| 53ccb0ac47 | |||
| a04d18dbaa | |||
| 7453948947 | |||
| f7f4e3022c | |||
| 3725fa78d4 | |||
| d86001b150 | |||
| 622be7e630 | |||
| 99f56b6847 | |||
| d0851ea944 | |||
| a6589f69f5 | |||
| 18a1d5100f | |||
| b5d14e08a2 | |||
| 036efee8b5 | |||
| 9898c108a3 | |||
| a05d4df146 | |||
| 23d4767f8c | |||
| 734d9f403b | |||
| 708dd97405 | |||
| dfc0106255 | |||
| 39e707450a | |||
| 850050ed87 | |||
| 63ca7583d6 | |||
| bf81ba3456 | |||
| 6a4e92da5c | |||
| 7e163a6473 | |||
| 925c732053 | |||
| 035c8b349c | |||
| 39ce011f9d | |||
| 4f32e3af8c | |||
| ae7c8cd2e0 | |||
| d57c7e7c94 | |||
| 9b669f6787 | |||
| 0cdf08265a | |||
| 684740b24e | |||
| a2890d9301 | |||
| e2f33953c8 | |||
| 534842a3a1 | |||
| e5a82f8e18 | |||
| 646de1f8a8 | |||
| 51c8b3543f | |||
| 7aed850593 | |||
| ba6125b60f | |||
| 3f557f8beb | |||
| 589bfad95e | |||
| 69e66bf86f | |||
| 7a132e9418 | |||
| 0a76c61d4e | |||
| 45def3e813 | |||
| 8c75fc2dd7 | |||
| f47eb4322a | |||
| 4b681328f3 | |||
| 2ca0f3f174 | |||
| c2a596ba08 | |||
| 66ec209ac6 | |||
| d2f8852662 | |||
| 1873af51cc | |||
| f54f0adc7f | |||
| 3a0288dfef | |||
| 45bcb6111e | |||
| cc678d7593 | |||
| 26babcb57b | |||
| da8d77b521 | |||
| 572bd2fb5e | |||
| 4ab60c91f4 | |||
| ed40358c90 | |||
| 99eefdd8b2 | |||
| b463e023b2 | |||
| 747e8433e5 | |||
| 793f109279 | |||
| 7ce597a687 | |||
| 30c5170ec2 | |||
| 65c19b684c | |||
| e250be6d66 | |||
| d7ac44446e | |||
| 47e0ed7b3e | |||
| f6020ec3aa | |||
| a97a654851 | |||
| 8f31f87f2f | |||
| 327707245c | |||
| 152a5227b9 | |||
| a8c87c20e9 | |||
| 54944a93f7 | |||
| 82047d18a6 | |||
| 0eb0612886 | |||
| 9109e547ae | |||
| ded33b32cc | |||
| 48bc997f74 | |||
| 7fa74a2020 | |||
| 821a5d9efb | |||
| 89c6cddff5 | |||
| 05aca6a266 | |||
| 323c407874 | |||
| 0ada140505 | |||
| e936dd2040 | |||
| d505813ce6 | |||
| f41f2d53cf | |||
| f32c037c7a | |||
| f2d07ac911 | |||
| 01189bf5a4 | |||
| 632aac365f | |||
| fe3a63d8a4 | |||
| d22b87614d | |||
| 6522b3d418 | |||
| 526b2cfc90 | |||
| 81fa52d9ca | |||
| bcbc20b6d9 | |||
| 7646385cdc | |||
| 737a2f46ce | |||
| 98e2aa1669 | |||
| 5be7156473 | |||
| 4e30dbbef2 | |||
| b36794f0f8 | |||
| 7fff8f46e8 | |||
| 22f9a36ead | |||
| 50d6a67262 | |||
| 1d97b8af8a | |||
| e93868b26e | |||
| c47dbbbd5d | |||
| 73069b9b59 | |||
| 65b16113c0 | |||
| a2572fb75c | |||
| 6d3187bec4 | |||
| b25a9d2ce0 | |||
| 6e27e3201f | |||
| 7448bba870 | |||
| a83e5b8f03 | |||
| b3e8985ce9 | |||
| 61d6025b0e | |||
| d5ec4f8378 | |||
| c90f269158 | |||
| f2423ec4df | |||
| ff3a01cdac | |||
| 13bcb76d5a | |||
| 1ad86e27d0 | |||
| 8fbb8bc0fd | |||
| 83edd966a1 | |||
| 5ee7ec1b9c | |||
| b3ad855ded | |||
| e5a542a73e | |||
| 991aa49b7b | |||
| a9564af46a | |||
| 1378ac0662 | |||
| 10dfab520c | |||
| 6118d537c3 | |||
| 674b262d9e | |||
| b8fa9168ba | |||
| aefebb9f84 | |||
| dc59b9475d | |||
| a4cca8f5e3 | |||
| 023ff07502 | |||
| 8f71325a00 | |||
| 9a1fd38274 | |||
| 8bae3f76d8 | |||
| 6882588c8f | |||
| 6372d0c328 | |||
| f7c431b2a5 | |||
| f359a73542 | |||
| fd239233c7 | |||
| 88e2805888 | |||
| c1d856ed28 | |||
| fe3c821528 | |||
| 23d749161a | |||
| f335392b2f | |||
| 5f136c284a | |||
| fdef31e3c5 | |||
| a33f0b6b18 | |||
| 5b71e0236f | |||
| 376c39da69 | |||
| 68eabea2f9 | |||
| fcdb29da6c | |||
| 6840d1b424 | |||
| 20e16adcde | |||
| 9d8e74cc4a | |||
| edd54f38c3 | |||
| 6f0f9e9816 | |||
| 0967aa3fde | |||
| fbfb768f07 | |||
| f4b06c4fb3 | |||
| 19f8412015 | |||
| 7e6bcd9424 | |||
| 4db5681054 | |||
| 4a973080dd | |||
| 85439fc637 | |||
| 1b8dd3096e | |||
| 461b388358 | |||
| 7de1d37f80 | |||
| 200916ba98 | |||
| c79943ca14 | |||
| ebbf35d9a2 | |||
| c445946955 | |||
| 2642039f65 | |||
| 9e96c5292a | |||
| 518ebb3323 | |||
| 1335e0bc3e | |||
| 6ea63bc54c | |||
| e3b6fe667d | |||
| 699847af7b | |||
| 5a49fe3f51 | |||
| 3fcb71bd53 | |||
| 3afb545307 | |||
| cfe3ba39e8 | |||
| 1b0fc6098e | |||
| 8f30bd5945 | |||
| 181da22a4d | |||
| 549232ade8 | |||
| 7a5ad3c9f1 | |||
| 1b78edeb44 | |||
| 37dffc7b4d | |||
| 1ec78d3549 | |||
| 3b34e951b9 | |||
| 8be7d9a52f | |||
| 4fa885d003 | |||
| 5cccea615a | |||
| bb1e0fdbb9 | |||
| b859bed2eb | |||
| 5f69056fe5 | |||
| e670975845 | |||
| c757afbac1 | |||
| a28eccf172 | |||
| 532bd1c768 | |||
| da1507c1dd | |||
| 6270847e8f | |||
| f777a0b0dc | |||
| 86543cb1db | |||
| 59045c0a75 | |||
| 957e3dbbd6 | |||
| 6f9e7f7989 | |||
| d6be60ed51 | |||
| 1a710d2dd8 | |||
| 2e14a686ea | |||
| 8b73e0f6fe | |||
| 6cb74724a3 | |||
| 6871e90c9e | |||
| 0ede37a30c | |||
| 1a3a8a9d6d | |||
| be3a6f83c2 | |||
| d15cbdd72f | |||
| 7fa07866b7 | |||
| ab4d4619c2 | |||
| 8464779f2a | |||
| 5775e12509 | |||
| 7ffafab4f7 | |||
| b6a8e4db77 | |||
| 121f12aa3c | |||
| 924881fba9 | |||
| b11130cf15 | |||
| f3e39f4bce | |||
| 037f741214 | |||
| c71a358c69 | |||
| f2c6d5cc11 | |||
| c600fd5712 | |||
| 67634c4102 | |||
| 668934466b | |||
| a183d18041 | |||
| c0e7ba026d | |||
| 9211b03e86 | |||
| b43f9b39fa | |||
| e098de35bc | |||
| b6ff1269e8 | |||
| 4f88dff731 | |||
| cdc68e910a | |||
| 1694162476 | |||
| c9e89351aa | |||
| a08235c914 | |||
| bcd99539b7 | |||
| acca768961 | |||
| c5d2f4d0b3 | |||
| 0dfa4a3c18 | |||
| c8ed9615de | |||
| ffee9dcc59 | |||
| edbc4b5816 | |||
| aa29f93afe | |||
| 7918ae0457 | |||
| 33b264e80a | |||
| 7e4872e91e | |||
| 57429ec39b | |||
| 837ac1b79d | |||
| 447b4bdf5a | |||
| 3d92367a80 | |||
| 957c839884 | |||
| cde9c8ccc0 | |||
| 8d6c617e59 | |||
| a474308f74 | |||
| 8dc8bfdb1d | |||
| 42129c29b4 | |||
| bcad932ef8 | |||
| 40d08b4fb1 | |||
| f68ad5439b | |||
| 2d065b56c7 | |||
| e5088f07a8 | |||
| bc09a4398c | |||
| 5e77c5f24f | |||
| 372476328c | |||
| 98dec1b08c | |||
| 474ee16ec7 | |||
| 4105b2d351 | |||
| ec259ef812 | |||
| 3e90c7df33 | |||
| 6b0512cc52 | |||
| a9c0a87ffd | |||
| 4de8dd37c7 | |||
| 32052e77f8 | |||
| 35f6db9232 | |||
| c5ab666ece | |||
| 06579f587c | |||
| 152b534b6d | |||
| 4081f82d5c | |||
| ef0688bf35 | |||
| 57241c56c0 | |||
| 994d82f64d | |||
| 4db4682464 | |||
| 591a54cd72 | |||
| 0f6973ac13 | |||
| 8c9a724672 | |||
| 1f700517cd | |||
| 7a2da2f137 | |||
| 7277817938 | |||
| 6187351760 | |||
| f65a5b5f34 | |||
| ef1e76b490 | |||
| df3cc8c9c5 | |||
| 9e026e05df | |||
| ed19765c8d | |||
| 31252432a1 | |||
| d14393fb3d | |||
| 8fe630d49b | |||
| bd7573e471 | |||
| 571ee73a77 | |||
| ce3f7aeb16 | |||
| 75d95d140b | |||
| 54b432fb4f | |||
| 0c5279e4c5 | |||
| 514dce5460 | |||
| 8e20e22be9 | |||
| 61859ec680 | |||
| 816f9a560b | |||
| abb789b0fb | |||
| 0e7711084a | |||
| 70b3253d46 | |||
| fd5cc5ee03 | |||
| 4f1725493d | |||
| eb43bdbbb0 | |||
| a27808cd1b | |||
| 1157c4c29a | |||
| 65dc7eba8d | |||
| f05c493776 | |||
| 761179ce00 | |||
| 3763566864 | |||
| 5acd022bf6 | |||
| 5bf7359a98 | |||
| a0b0988443 | |||
| cd475230e3 | |||
| 7ef99fc213 | |||
| 39d9d254f8 | |||
| cb5fb29cd7 | |||
| 693959a524 | |||
| 4a4f563f26 | |||
| 2eccec9542 | |||
| 4c9c186fdc | |||
| 0cd1bc0b67 | |||
| 90a6d883b1 | |||
| c883e9bf96 | |||
| 0b07e65fba | |||
| 390dcea39c | |||
| ae7dc5dcfb | |||
| b333c94a01 | |||
| 6385c8f0b7 | |||
| 8b16f4d7f2 | |||
| 5f01d507cb | |||
| 449b4461ee | |||
| 980e81275b | |||
| aa21de4c84 | |||
| da2d40de4c | |||
| 41e73ee448 | |||
| 5dca702169 | |||
| cd579c4b1a | |||
| ac97d778c7 | |||
| 308fda5fb1 | |||
| 6ebe20ee9b | |||
| 09b00ee19c | |||
| 73fb05948f | |||
| 10da17c8b8 | |||
| 2b58ab0274 | |||
| e0f7f1ef95 | |||
| 9fc96f244c | |||
| 236636ea00 | |||
| a49d2c334a | |||
| 8c4e26977c | |||
| 35e28afab4 | |||
| c718e9f671 | |||
| 7eddff8e55 | |||
| da81219682 | |||
| 43bd47b67a | |||
| f251d75803 | |||
| fc9a4136a7 | |||
| b0412aa454 | |||
| 8fde61edd4 | |||
| b736aab921 | |||
| 95a0dce3d4 | |||
| 21f3395f7a | |||
| d21dd7fbfc | |||
| 2035a7a55e | |||
| 3becb16f53 | |||
| cd6a9957fa | |||
| 7b0cd65883 | |||
| 402eec348e | |||
| 42e3825a92 | |||
| d6c0be9438 | |||
| 0d17150a0c | |||
| b8d91beab4 | |||
| 8f8093ac0f | |||
| 5eae317838 | |||
| 445be5614a | |||
| ae9af4e486 | |||
| 08392add89 | |||
| 75bad7de80 | |||
| 056dd08cd2 | |||
| 1f9034982e | |||
| ace999418c | |||
| 5892083e98 | |||
| 4179c7d022 | |||
| 3cb6f1de33 | |||
| ef12348101 |
@@ -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
|
||||
|
||||
@@ -263,7 +263,9 @@ script:
|
||||
grunt ;
|
||||
# Add all files to the git index and then run diff --cached to see all changes.
|
||||
# This ensures that we get the status of all files, including new files.
|
||||
# We ignore npm-shrinkwrap.json to make the tasks immune to npm changes.
|
||||
git add . ;
|
||||
git reset -- npm-shrinkwrap.json ;
|
||||
git diff --cached --exit-code ;
|
||||
fi
|
||||
|
||||
|
||||
+2
-1
@@ -214,7 +214,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'));
|
||||
};
|
||||
|
||||
@@ -2097,6 +2097,7 @@
|
||||
<VENDOR name="oracle" version="10.2" />
|
||||
</DATABASE>
|
||||
<PHP version="7.0.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_73" message="unsupportedphpversion73" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
@@ -2273,6 +2274,7 @@
|
||||
<VENDOR name="oracle" version="10.2" />
|
||||
</DATABASE>
|
||||
<PHP version="7.0.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_73" message="unsupportedphpversion73" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
@@ -2616,4 +2618,185 @@
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="3.7" requires="3.2">
|
||||
<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.1.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>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Privacy Subsystem implementation for core_role.
|
||||
*
|
||||
@@ -20,7 +21,9 @@
|
||||
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_role\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \core_privacy\local\metadata\collection;
|
||||
@@ -28,6 +31,8 @@ use \core_privacy\local\request\contextlist;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\transform;
|
||||
use \core_privacy\local\request\writer;
|
||||
use \core_privacy\local\request\userlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy provider for core_role.
|
||||
@@ -39,7 +44,8 @@ class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\subsystem\provider,
|
||||
\core_privacy\local\request\subsystem\plugin_provider,
|
||||
\core_privacy\local\request\user_preference_provider {
|
||||
\core_privacy\local\request\user_preference_provider,
|
||||
\core_privacy\local\request\core_userlist_provider {
|
||||
|
||||
/**
|
||||
* Get information about the user data stored by this plugin.
|
||||
@@ -151,6 +157,50 @@ class provider implements
|
||||
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users within a specific context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
|
||||
if (empty($userlist)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context = $userlist->get_context();
|
||||
|
||||
// Include users who created or modified role capabilities.
|
||||
$sql = "SELECT modifierid as userid
|
||||
FROM {role_capabilities}
|
||||
WHERE contextid = :contextid";
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id
|
||||
];
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
// Include users that have a role assigned to them.
|
||||
$sql = "SELECT userid
|
||||
FROM {role_assignments}
|
||||
WHERE contextid = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
// Include users who created or modified the role assignment.
|
||||
// Differentiate and exclude special cases where tool_cohortroles adds records through the
|
||||
// "Assign user roles to cohort" feature into the role_assignments table.
|
||||
// These records should be separately processed in tool_cohortroles.
|
||||
$sql = "SELECT modifierid as userid
|
||||
FROM {role_assignments}
|
||||
WHERE contextid = :contextid
|
||||
AND component != 'tool_cohortroles'";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -315,6 +365,32 @@ class provider implements
|
||||
// Remove data from role_assignments.
|
||||
$DB->delete_records('role_assignments', ['contextid' => $context->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
// Don't remove data from role_capabilities.
|
||||
// Because this data affects the whole Moodle, there are override capabilities.
|
||||
// Don't belong to the modifier user.
|
||||
$context = $userlist->get_context();
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
if (empty($userids)) {
|
||||
return;
|
||||
}
|
||||
list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
$params = ['contextid' => $context->id] + $userparams;
|
||||
|
||||
// Remove data from role_assignments.
|
||||
$DB->delete_records_select('role_assignments',
|
||||
"contextid = :contextid AND userid {$usersql}", $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for this user only.
|
||||
*
|
||||
@@ -332,10 +408,14 @@ class provider implements
|
||||
return;
|
||||
}
|
||||
$userid = $contextlist->get_user()->id;
|
||||
foreach ($contextlist->get_contexts() as $context) {
|
||||
// Only delete the roles assignments where the user is assigned in all contexts.
|
||||
$DB->delete_records('role_assignments', ['userid' => $userid, 'contextid' => $context->id]);
|
||||
}
|
||||
$contextids = $contextlist->get_contextids();
|
||||
|
||||
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
|
||||
$params = ['userid' => $userid] + $contextparams;
|
||||
|
||||
// Only delete the roles assignments where the user is assigned in all contexts.
|
||||
$DB->delete_records_select('role_assignments',
|
||||
"userid = :userid AND contextid {$contextsql}", $params);
|
||||
}
|
||||
/**
|
||||
* Delete user entries in role_assignments related to the feature
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Privacy test for core_role
|
||||
*
|
||||
@@ -21,13 +22,16 @@
|
||||
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \core_role\privacy\provider;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\writer;
|
||||
use \core_privacy\tests\provider_testcase;
|
||||
use \core_privacy\local\request\transform;
|
||||
use \tool_cohortroles\api;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy test for core_role
|
||||
@@ -54,6 +58,7 @@ class core_role_privacy_testcase extends provider_testcase {
|
||||
$this->assertEquals(get_string('privacy:metadata:preference:showadvanced', 'core_role'),
|
||||
$prefs->definerole_showadvanced->description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all contexts are returned if there is any user data for this user.
|
||||
*/
|
||||
@@ -231,6 +236,7 @@ class core_role_privacy_testcase extends provider_testcase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_all_users_in_context().
|
||||
*/
|
||||
@@ -316,6 +322,7 @@ class core_role_privacy_testcase extends provider_testcase {
|
||||
$count = $DB->count_records('role_assignments', ['contextid' => $usercontext2->id]);
|
||||
$this->assertEquals(0, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_user().
|
||||
*/
|
||||
@@ -373,6 +380,7 @@ class core_role_privacy_testcase extends provider_testcase {
|
||||
$count = $DB->count_records('role_assignments', ['modifierid' => $user->id]);
|
||||
$this->assertEquals(2, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export for a user with a key against a script where no instance is specified.
|
||||
*/
|
||||
@@ -426,6 +434,7 @@ class core_role_privacy_testcase extends provider_testcase {
|
||||
$exported = $writer->get_related_data($subcontextteacher, 'cohortroles');
|
||||
$this->assertEquals($user2->id, reset($exported)->userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_user_role_to_cohort().
|
||||
*/
|
||||
@@ -460,6 +469,267 @@ class core_role_privacy_testcase extends provider_testcase {
|
||||
$count = $DB->count_records('role_assignments', ['userid' => $user->id, 'component' => 'tool_cohortroles']);
|
||||
$this->assertEquals(0, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only users within a course context are fetched.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$component = 'core_role';
|
||||
|
||||
$this->setAdminUser();
|
||||
$admin = \core_user::get_user_by_username('admin');
|
||||
// Create user1.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$usercontext1 = \context_user::instance($user1->id);
|
||||
// Create user2.
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$usercontext2 = \context_user::instance($user2->id);
|
||||
// Create course1.
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$coursecontext1 = \context_course::instance($course1->id);
|
||||
// Create course category.
|
||||
$coursecat = $this->getDataGenerator()->create_category();
|
||||
$coursecatcontext = \context_coursecat::instance($coursecat->id);
|
||||
// Create chat module.
|
||||
$cm = $this->getDataGenerator()->create_module('chat', ['course' => $course1->id]);
|
||||
$cmcontext = \context_module::instance($cm->cmid);
|
||||
|
||||
$systemcontext = \context_system::instance();
|
||||
// Create a block.
|
||||
$block = $this->getDataGenerator()->create_block('online_users');
|
||||
$blockcontext = \context_block::instance($block->id);
|
||||
|
||||
$studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
|
||||
$managerrole = $DB->get_record('role', array('shortname' => 'manager'), '*', MUST_EXIST);
|
||||
|
||||
// Role assignments CONTEXT_COURSE.
|
||||
role_assign($studentrole->id, $user1->id, $coursecontext1->id);
|
||||
role_assign($studentrole->id, $user2->id, $coursecontext1->id);
|
||||
// Role assignments CONTEXT_COURSECAT.
|
||||
role_assign($studentrole->id, $user2->id, $coursecatcontext->id);
|
||||
// Role assignments CONTEXT_SYSTEM.
|
||||
role_assign($studentrole->id, $user1->id, $systemcontext->id);
|
||||
// Role assignments CONTEXT_MODULE.
|
||||
role_assign($studentrole->id, $user2->id, $cmcontext->id);
|
||||
// Role assigments CONTEXT_BLOCK.
|
||||
role_assign($studentrole->id, $user1->id, $blockcontext->id);
|
||||
// Role assigments CONTEXT_USER.
|
||||
role_assign($managerrole->id, $user1->id, $usercontext2->id);
|
||||
|
||||
// Role capabilities.
|
||||
$this->setUser($user1);
|
||||
assign_capability('moodle/backup:backupcourse', CAP_ALLOW, $studentrole->id, $cmcontext->id);
|
||||
|
||||
// The user list for usercontext1 should not return any users.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(0, $userlist1);
|
||||
// The user list for usercontext2 should user1 and admin (role creator).
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(2, $userlist2);
|
||||
$expected = [
|
||||
$user1->id,
|
||||
$admin->id
|
||||
];
|
||||
$this->assertEquals($expected, $userlist2->get_userids(), '', 0.0, 10, true);
|
||||
|
||||
// The user list for coursecontext1 should user1, user2 and admin (role creator).
|
||||
$userlist3 = new \core_privacy\local\request\userlist($coursecontext1, $component);
|
||||
provider::get_users_in_context($userlist3);
|
||||
$this->assertCount(3, $userlist3);
|
||||
$expected = [
|
||||
$user1->id,
|
||||
$user2->id,
|
||||
$admin->id
|
||||
];
|
||||
$this->assertEquals($expected, $userlist3->get_userids(), '', 0.0, 10, true);
|
||||
|
||||
// The user list for coursecatcontext should user2 and admin (role creator).
|
||||
$userlist4 = new \core_privacy\local\request\userlist($coursecatcontext, $component);
|
||||
provider::get_users_in_context($userlist4);
|
||||
$this->assertCount(2, $userlist4);
|
||||
$expected = [
|
||||
$user2->id,
|
||||
$admin->id
|
||||
];
|
||||
$this->assertEquals($expected, $userlist4->get_userids(), '', 0.0, 10, true);
|
||||
|
||||
// The user list for systemcontext should user1 and admin (role creator).
|
||||
$userlist6 = new \core_privacy\local\request\userlist($systemcontext, $component);
|
||||
provider::get_users_in_context($userlist6);
|
||||
$this->assertCount(2, $userlist6);
|
||||
$expected = [
|
||||
$user1->id,
|
||||
$admin->id
|
||||
];
|
||||
$this->assertEquals($expected, $userlist6->get_userids(), '', 0.0, 10, true);
|
||||
|
||||
// The user list for cmcontext should user1, user2 and admin (role creator).
|
||||
$userlist7 = new \core_privacy\local\request\userlist($cmcontext, $component);
|
||||
provider::get_users_in_context($userlist7);
|
||||
$this->assertCount(3, $userlist7);
|
||||
$expected = [
|
||||
$user1->id,
|
||||
$user2->id,
|
||||
$admin->id
|
||||
];
|
||||
$this->assertEquals($expected, $userlist7->get_userids(), '', 0.0, 10, true);
|
||||
|
||||
// The user list for blockcontext should user1 and admin (role creator).
|
||||
$userlist8 = new \core_privacy\local\request\userlist($blockcontext, $component);
|
||||
provider::get_users_in_context($userlist8);
|
||||
$this->assertCount(2, $userlist8);
|
||||
$expected = [
|
||||
$user1->id,
|
||||
$admin->id
|
||||
];
|
||||
$this->assertEquals($expected, $userlist8->get_userids(), '', 0.0, 10, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data for users in approved userlist is deleted.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$component = 'core_role';
|
||||
|
||||
$this->setAdminUser();
|
||||
$admin = \core_user::get_user_by_username('admin');
|
||||
// Create user1.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
// Create user2.
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$usercontext2 = \context_user::instance($user2->id);
|
||||
// Create course1.
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$coursecontext1 = \context_course::instance($course1->id);
|
||||
// Create course category.
|
||||
$coursecat = $this->getDataGenerator()->create_category();
|
||||
$coursecatcontext = \context_coursecat::instance($coursecat->id);
|
||||
// Create chat module.
|
||||
$cm = $this->getDataGenerator()->create_module('chat', ['course' => $course1->id]);
|
||||
$cmcontext = \context_module::instance($cm->cmid);
|
||||
|
||||
$systemcontext = \context_system::instance();
|
||||
// Create a block.
|
||||
$block = $this->getDataGenerator()->create_block('online_users');
|
||||
$blockcontext = \context_block::instance($block->id);
|
||||
|
||||
$studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
|
||||
$managerrole = $DB->get_record('role', array('shortname' => 'manager'), '*', MUST_EXIST);
|
||||
|
||||
// Role assignments CONTEXT_COURSE.
|
||||
role_assign($studentrole->id, $user1->id, $coursecontext1->id);
|
||||
role_assign($studentrole->id, $user2->id, $coursecontext1->id);
|
||||
// Role assignments CONTEXT_COURSECAT.
|
||||
role_assign($studentrole->id, $user2->id, $coursecatcontext->id);
|
||||
// Role assignments CONTEXT_SYSTEM.
|
||||
role_assign($studentrole->id, $user1->id, $systemcontext->id);
|
||||
// Role assignments CONTEXT_MODULE.
|
||||
role_assign($studentrole->id, $user2->id, $cmcontext->id);
|
||||
// Role assigments CONTEXT_BLOCK.
|
||||
role_assign($studentrole->id, $user1->id, $blockcontext->id);
|
||||
// Role assigments CONTEXT_USER.
|
||||
role_assign($managerrole->id, $user1->id, $usercontext2->id);
|
||||
|
||||
// Role capabilities.
|
||||
$this->setUser($user1);
|
||||
assign_capability('moodle/backup:backupcourse', CAP_ALLOW, $studentrole->id, $cmcontext->id);
|
||||
|
||||
// The user list for usercontext2 should user1 and admin (role creator).
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(2, $userlist1);
|
||||
// The user list for coursecontext1 should user1, user2 and admin (role creator).
|
||||
$userlist2 = new \core_privacy\local\request\userlist($coursecontext1, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(3, $userlist2);
|
||||
// The user list for coursecatcontext should user2 and admin (role creator).
|
||||
$userlist3 = new \core_privacy\local\request\userlist($coursecatcontext, $component);
|
||||
provider::get_users_in_context($userlist3);
|
||||
$this->assertCount(2, $userlist3);
|
||||
// The user list for systemcontext should user1 and admin (role creator).
|
||||
$userlist4 = new \core_privacy\local\request\userlist($systemcontext, $component);
|
||||
provider::get_users_in_context($userlist4);
|
||||
$this->assertCount(2, $userlist4);
|
||||
// The user list for cmcontext should user1, user2 and admin (role creator).
|
||||
$userlist5 = new \core_privacy\local\request\userlist($cmcontext, $component);
|
||||
provider::get_users_in_context($userlist5);
|
||||
$this->assertCount(3, $userlist5);
|
||||
// The user list for blockcontext should user1 and admin (role creator).
|
||||
$userlist6 = new \core_privacy\local\request\userlist($blockcontext, $component);
|
||||
provider::get_users_in_context($userlist6);
|
||||
$this->assertCount(2, $userlist6);
|
||||
|
||||
// Convert $userlist1 into an approved_contextlist.
|
||||
$approvedlist1 = new approved_userlist($usercontext2, $component, $userlist1->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist1);
|
||||
// Re-fetch users in usercontext2.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(0, $userlist1);
|
||||
|
||||
// Convert $userlist2 into an approved_contextlist.
|
||||
$approvedlist2 = new approved_userlist($coursecontext1, $component, $userlist2->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist2);
|
||||
// Re-fetch users in coursecontext1.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($coursecontext1, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(0, $userlist2);
|
||||
|
||||
// Convert $userlist3 into an approved_contextlist.
|
||||
$approvedlist3 = new approved_userlist($coursecatcontext, $component, $userlist3->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist3);
|
||||
// Re-fetch users in coursecatcontext.
|
||||
$userlist3 = new \core_privacy\local\request\userlist($coursecatcontext, $component);
|
||||
provider::get_users_in_context($userlist3);
|
||||
$this->assertCount(0, $userlist3);
|
||||
|
||||
// Convert $userlist4 into an approved_contextlist.
|
||||
$approvedlist4 = new approved_userlist($systemcontext, $component, $userlist4->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist4);
|
||||
// Re-fetch users in systemcontext.
|
||||
$userlist4 = new \core_privacy\local\request\userlist($systemcontext, $component);
|
||||
provider::get_users_in_context($userlist4);
|
||||
// The data from role_capabilities should still be present. The user list should return the admin user.
|
||||
$this->assertCount(1, $userlist4);
|
||||
$expected = [$admin->id];
|
||||
$this->assertEquals($expected, $userlist4->get_userids());
|
||||
|
||||
// Convert $userlist5 into an approved_contextlist.
|
||||
$approvedlist5 = new approved_userlist($cmcontext, $component, $userlist5->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist5);
|
||||
// Re-fetch users in cmcontext.
|
||||
$userlist5 = new \core_privacy\local\request\userlist($cmcontext, $component);
|
||||
provider::get_users_in_context($userlist5);
|
||||
// The data from role_capabilities should still be present. The user list should return user1.
|
||||
$this->assertCount(1, $userlist5);
|
||||
$expected = [$user1->id];
|
||||
$this->assertEquals($expected, $userlist5->get_userids());
|
||||
|
||||
// Convert $userlist6 into an approved_contextlist.
|
||||
$approvedlist6 = new approved_userlist($blockcontext, $component, $userlist6->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist6);
|
||||
// Re-fetch users in blockcontext.
|
||||
$userlist6 = new \core_privacy\local\request\userlist($blockcontext, $component);
|
||||
provider::get_users_in_context($userlist6);
|
||||
$this->assertCount(0, $userlist6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supoort function to get all the localised roles name
|
||||
* in a simple array for testing.
|
||||
|
||||
@@ -109,6 +109,17 @@ require_once(__DIR__ . '/../../../../lib/behat/lib.php');
|
||||
require_once(__DIR__ . '/../../../../lib/behat/classes/behat_command.php');
|
||||
require_once(__DIR__ . '/../../../../lib/behat/classes/behat_config_manager.php');
|
||||
|
||||
// Remove error handling overrides done in config.php. This is consistent with admin/tool/behat/cli/util_single_run.php.
|
||||
$CFG->debug = (E_ALL | E_STRICT);
|
||||
$CFG->debugdisplay = 1;
|
||||
error_reporting($CFG->debug);
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('log_errors', '1');
|
||||
|
||||
// Import the necessary libraries.
|
||||
require_once($CFG->libdir . '/setuplib.php');
|
||||
require_once($CFG->libdir . '/behat/classes/util.php');
|
||||
|
||||
// For drop option check if parallel site.
|
||||
if ((empty($options['parallel'])) && ($options['drop']) || $options['updatesteps']) {
|
||||
$options['parallel'] = behat_config_manager::get_behat_run_config_value('parallel');
|
||||
|
||||
@@ -23,29 +23,29 @@
|
||||
*/
|
||||
|
||||
$string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically. Step definitions available for use in .feature files are listed below.';
|
||||
$string['allavailablesteps'] = 'All the available steps definitions';
|
||||
$string['allavailablesteps'] = 'All available step definitions';
|
||||
$string['errorbehatcommand'] = 'Error running behat CLI command. Try running "{$a} --help" manually from CLI to find out more about the problem.';
|
||||
$string['errorcomposer'] = 'Composer dependencies are not installed.';
|
||||
$string['errordataroot'] = '$CFG->behat_dataroot is not set or is invalid.';
|
||||
$string['errorsetconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot need to be set in config.php.';
|
||||
$string['erroruniqueconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot values need to be different than $CFG->dataroot, $CFG->prefix, $CFG->wwwroot, $CFG->phpunit_dataroot and $CFG->phpunit_prefix values.';
|
||||
$string['fieldvalueargument'] = 'Field value arguments';
|
||||
$string['fieldvalueargument_help'] = 'This argument should be completed by a field value, there are many field types, simple ones like checkboxes, selects or textareas or complex ones like date selectors. You can check <a href="http://docs.moodle.org/dev/Acceptance_testing#Providing_values_to_steps" target="_blank">Field values</a> to see the expected field value depending on the field type you provide.';
|
||||
$string['fieldvalueargument_help'] = 'This argument should be completed by a field value. There are many field types, including simple ones like checkboxes, selects or textareas, or complex ones like date selectors. See the dev documentation <a href="http://docs.moodle.org/dev/Acceptance_testing" target="_blank">Acceptance_testing</a> for details of expected field values.';
|
||||
$string['giveninfo'] = 'Given. Processes to set up the environment';
|
||||
$string['infoheading'] = 'Info';
|
||||
$string['installinfo'] = 'Read {$a} for installation and tests execution info';
|
||||
$string['newstepsinfo'] = 'Read {$a} for info about how to add new steps definitions';
|
||||
$string['newstepsinfo'] = 'Read {$a} for info about how to add new step definitions';
|
||||
$string['newtestsinfo'] = 'Read {$a} for info about how to write new tests';
|
||||
$string['nostepsdefinitions'] = 'There aren\'t steps definitions matching this filters';
|
||||
$string['nostepsdefinitions'] = 'There aren\'t any step definitions matching this filter';
|
||||
$string['pluginname'] = 'Acceptance testing';
|
||||
$string['stepsdefinitionscomponent'] = 'Area';
|
||||
$string['stepsdefinitionscontains'] = 'Contains';
|
||||
$string['stepsdefinitionsfilters'] = 'Steps definitions';
|
||||
$string['stepsdefinitionsfilters'] = 'Step definitions';
|
||||
$string['stepsdefinitionstype'] = 'Type';
|
||||
$string['theninfo'] = 'Then. Checkings to ensure the outcomes are the expected ones';
|
||||
$string['unknownexceptioninfo'] = 'There was a problem with Selenium or your browser. Please ensure you are using the latest version of Selenium. Error:';
|
||||
$string['viewsteps'] = 'Filter';
|
||||
$string['wheninfo'] = 'When. Actions that provokes an event';
|
||||
$string['wheninfo'] = 'When. Action that provokes an event';
|
||||
$string['wrongbehatsetup'] = 'Something is wrong with the behat setup and so step definitions cannot be listed: <b>{$a->errormsg}</b><br/><br/>Please check:<ul>
|
||||
<li>$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot are set in config.php with different values from $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.</li>
|
||||
<li>You ran "{$a->behatinit}" from your Moodle root directory.</li>
|
||||
|
||||
@@ -11,7 +11,7 @@ Feature: List the system steps definitions
|
||||
|
||||
@javascript
|
||||
Scenario: Accessing the list
|
||||
Then I should see "Steps definitions"
|
||||
Then I should see "Step definitions"
|
||||
And I should not see "There aren't steps definitions matching this filter"
|
||||
|
||||
@javascript
|
||||
|
||||
@@ -30,6 +30,8 @@ use core_privacy\local\request\context;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\local\request\userlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -39,7 +41,10 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @copyright 2018 Zig Tan <zig@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
@@ -75,15 +80,25 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
public static function get_contexts_for_userid(int $userid) : contextlist {
|
||||
$contextlist = new contextlist();
|
||||
|
||||
// Retrieve the User context associated with tool_cohortroles records.
|
||||
$sql = "SELECT DISTINCT c.id
|
||||
FROM {context} c
|
||||
JOIN {tool_cohortroles} cr ON cr.userid = c.instanceid AND c.contextlevel = :contextuser
|
||||
WHERE cr.userid = :userid";
|
||||
// When we process user deletions and expiries, we always delete from the user context.
|
||||
// As a result the cohort role assignments would be deleted, which has a knock-on effect with courses
|
||||
// as roles may change and data may be removed earlier than it should be.
|
||||
|
||||
// Retrieve the context associated with tool_cohortroles records.
|
||||
$sql = "SELECT DISTINCT c.contextid
|
||||
FROM {tool_cohortroles} tc
|
||||
JOIN {cohort} c
|
||||
ON tc.cohortid = c.id
|
||||
JOIN {context} ctx
|
||||
ON ctx.id = c.contextid
|
||||
WHERE tc.userid = :userid
|
||||
AND (ctx.contextlevel = :contextlevel1
|
||||
OR ctx.contextlevel = :contextlevel2)";
|
||||
|
||||
$params = [
|
||||
'contextuser' => CONTEXT_USER,
|
||||
'userid' => $userid
|
||||
'userid' => $userid,
|
||||
'contextlevel1' => CONTEXT_SYSTEM,
|
||||
'contextlevel2' => CONTEXT_COURSECAT
|
||||
];
|
||||
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
@@ -91,6 +106,40 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users within a specific context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
// When we process user deletions and expiries, we always delete from the user context.
|
||||
// As a result the cohort role assignments would be deleted, which has a knock-on effect with courses
|
||||
// as roles may change and data may be removed earlier than it should be.
|
||||
|
||||
$allowedcontextlevels = [
|
||||
CONTEXT_SYSTEM,
|
||||
CONTEXT_COURSECAT
|
||||
];
|
||||
|
||||
if (!in_array($context->contextlevel, $allowedcontextlevels)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sql = "SELECT tc.userid as userid
|
||||
FROM {tool_cohortroles} tc
|
||||
JOIN {cohort} c
|
||||
ON tc.cohortid = c.id
|
||||
WHERE c.contextid = :contextid";
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id
|
||||
];
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -99,24 +148,28 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
public static function export_user_data(approved_contextlist $contextlist) {
|
||||
global $DB;
|
||||
|
||||
// If the user has tool_cohortroles data, then only the User context should be present so get the first context.
|
||||
$contexts = $contextlist->get_contexts();
|
||||
if (count($contexts) == 0) {
|
||||
return;
|
||||
}
|
||||
$context = reset($contexts);
|
||||
// Remove contexts different from SYSTEM or COURSECAT.
|
||||
$contextids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
|
||||
if ($context->contextlevel == CONTEXT_SYSTEM || $context->contextlevel == CONTEXT_COURSECAT) {
|
||||
$carry[] = $context->id;
|
||||
}
|
||||
return $carry;
|
||||
}, []);
|
||||
|
||||
// Sanity check that context is at the User context level, then get the userid.
|
||||
if ($context->contextlevel !== CONTEXT_USER) {
|
||||
if (empty($contextids)) {
|
||||
return;
|
||||
}
|
||||
$userid = $context->instanceid;
|
||||
|
||||
$userid = $contextlist->get_user()->id;
|
||||
|
||||
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
|
||||
|
||||
// Retrieve the tool_cohortroles records created for the user.
|
||||
$sql = 'SELECT cr.id as cohortroleid,
|
||||
$sql = "SELECT cr.id as cohortroleid,
|
||||
c.name as cohortname,
|
||||
c.idnumber as cohortidnumber,
|
||||
c.description as cohortdescription,
|
||||
c.contextid as contextid,
|
||||
r.shortname as roleshortname,
|
||||
cr.userid as userid,
|
||||
cr.timecreated as timecreated,
|
||||
@@ -124,13 +177,13 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
FROM {tool_cohortroles} cr
|
||||
JOIN {cohort} c ON c.id = cr.cohortid
|
||||
JOIN {role} r ON r.id = cr.roleid
|
||||
WHERE cr.userid = :userid';
|
||||
WHERE cr.userid = :userid
|
||||
AND c.contextid {$contextsql}";
|
||||
|
||||
$params = [
|
||||
'userid' => $userid
|
||||
];
|
||||
$params = ['userid' => $userid] + $contextparams;
|
||||
|
||||
$cohortroles = $DB->get_records_sql($sql, $params);
|
||||
|
||||
foreach ($cohortroles as $cohortrole) {
|
||||
// The tool_cohortroles data export is organised in:
|
||||
// {User Context}/Cohort roles management/{cohort name}/{role shortname}/data.json.
|
||||
@@ -150,6 +203,8 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
'timemodified' => transform::datetime($cohortrole->timemodified)
|
||||
];
|
||||
|
||||
$context = \context::instance_by_id($cohortrole->contextid);
|
||||
|
||||
writer::with_context($context)->export_data($subcontext, $data);
|
||||
}
|
||||
}
|
||||
@@ -162,14 +217,70 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
public static function delete_data_for_all_users_in_context(\context $context) {
|
||||
global $DB;
|
||||
|
||||
// Sanity check that context is at the User context level, then get the userid.
|
||||
if ($context->contextlevel !== CONTEXT_USER) {
|
||||
// When we process user deletions and expiries, we always delete from the user context.
|
||||
// As a result the cohort role assignments would be deleted, which has a knock-on effect with courses
|
||||
// as roles may change and data may be removed earlier than it should be.
|
||||
|
||||
$allowedcontextlevels = [
|
||||
CONTEXT_SYSTEM,
|
||||
CONTEXT_COURSECAT
|
||||
];
|
||||
|
||||
if (!in_array($context->contextlevel, $allowedcontextlevels)) {
|
||||
return;
|
||||
}
|
||||
$userid = $context->instanceid;
|
||||
|
||||
// Delete the tool_cohortroles records created for the userid.
|
||||
$DB->delete_records('tool_cohortroles', ['userid' => $userid]);
|
||||
$cohortids = $DB->get_fieldset_select('cohort', 'id', 'contextid = :contextid',
|
||||
['contextid' => $context->id]);
|
||||
|
||||
// Delete the tool_cohortroles records created in the specific context.
|
||||
$DB->delete_records_list('tool_cohortroles', 'cohortid', $cohortids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
// When we process user deletions and expiries, we always delete from the user context.
|
||||
// As a result the cohort role assignments would be deleted, which has a knock-on effect with courses
|
||||
// as roles may change and data may be removed earlier than it should be.
|
||||
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
if (empty($userids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context = $userlist->get_context();
|
||||
|
||||
$allowedcontextlevels = [
|
||||
CONTEXT_SYSTEM,
|
||||
CONTEXT_COURSECAT
|
||||
];
|
||||
|
||||
if (!in_array($context->contextlevel, $allowedcontextlevels)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cohortids = $DB->get_fieldset_select('cohort', 'id', 'contextid = :contextid',
|
||||
['contextid' => $context->id]);
|
||||
|
||||
if (empty($cohortids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
list($cohortsql, $cohortparams) = $DB->get_in_or_equal($cohortids, SQL_PARAMS_NAMED);
|
||||
list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
|
||||
$params = $cohortparams + $userparams;
|
||||
$select = "cohortid {$cohortsql} AND userid {$usersql}";
|
||||
|
||||
// Delete the tool_cohortroles records created in the specific context for an approved list of users.
|
||||
$DB->delete_records_select('tool_cohortroles', $select, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,21 +291,38 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
global $DB;
|
||||
|
||||
// If the user has tool_cohortroles data, then only the User context should be present so get the first context.
|
||||
$contexts = $contextlist->get_contexts();
|
||||
if (count($contexts) == 0) {
|
||||
return;
|
||||
}
|
||||
$context = reset($contexts);
|
||||
// When we process user deletions and expiries, we always delete from the user context.
|
||||
// As a result the cohort role assignments would be deleted, which has a knock-on effect with courses
|
||||
// as roles may change and data may be removed earlier than it should be.
|
||||
|
||||
// Sanity check that context is at the User context level, then get the userid.
|
||||
if ($context->contextlevel !== CONTEXT_USER) {
|
||||
// Remove contexts different from SYSTEM or COURSECAT.
|
||||
$contextids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
|
||||
if ($context->contextlevel == CONTEXT_SYSTEM || $context->contextlevel == CONTEXT_COURSECAT) {
|
||||
$carry[] = $context->id;
|
||||
}
|
||||
return $carry;
|
||||
}, []);
|
||||
|
||||
if (empty($contextids)) {
|
||||
return;
|
||||
}
|
||||
$userid = $context->instanceid;
|
||||
|
||||
$userid = $contextlist->get_user()->id;
|
||||
|
||||
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
|
||||
$selectcontext = "contextid {$contextsql}";
|
||||
// Get the cohorts in the specified contexts.
|
||||
$cohortids = $DB->get_fieldset_select('cohort', 'id', $selectcontext, $contextparams);
|
||||
|
||||
if (empty($cohortids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
list($cohortsql, $cohortparams) = $DB->get_in_or_equal($cohortids, SQL_PARAMS_NAMED);
|
||||
$selectcohort = "cohortid {$cohortsql} AND userid = :userid";
|
||||
$params = ['userid' => $userid] + $cohortparams;
|
||||
|
||||
// Delete the tool_cohortroles records created for the userid.
|
||||
$DB->delete_records('tool_cohortroles', ['userid' => $userid]);
|
||||
$DB->delete_records_select('tool_cohortroles', $selectcohort, $params);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ use \core_privacy\local\request\writer;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \tool_cohortroles\api;
|
||||
use \tool_cohortroles\privacy\provider;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Unit tests for the tool_cohortroles implementation of the privacy API.
|
||||
@@ -57,22 +58,36 @@ class tool_cohortroles_privacy_testcase extends \core_privacy\tests\provider_tes
|
||||
$this->setUser($user);
|
||||
$this->setAdminUser();
|
||||
|
||||
$nocohortroles = 3;
|
||||
$this->setup_test_scenario_data($user->id, $nocohortroles);
|
||||
// Create course category.
|
||||
$coursecategory = $this->getDataGenerator()->create_category();
|
||||
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
|
||||
$systemctx = context_system::instance();
|
||||
// Create course.
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$coursectx = \context_course::instance($course->id);
|
||||
|
||||
$this->setup_test_scenario_data($user->id, $systemctx, 1);
|
||||
$this->setup_test_scenario_data($user->id, $coursecategoryctx, 1, 'Sausage roll 2',
|
||||
'sausageroll2');
|
||||
$this->setup_test_scenario_data($user->id, $coursectx, 1, 'Sausage roll 3',
|
||||
'sausageroll3');
|
||||
|
||||
// Test the User's assigned cohortroles matches 3.
|
||||
$cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
|
||||
$this->assertCount($nocohortroles, $cohortroles);
|
||||
$this->assertCount(3, $cohortroles);
|
||||
|
||||
// Test the User's retrieved contextlist contains only one context.
|
||||
// Test the User's retrieved contextlist returns only the system and course category context.
|
||||
$contextlist = provider::get_contexts_for_userid($user->id);
|
||||
$contexts = $contextlist->get_contexts();
|
||||
$this->assertCount(1, $contexts);
|
||||
$this->assertCount(2, $contexts);
|
||||
|
||||
// Test the User's contexts equal the User's own context.
|
||||
$context = reset($contexts);
|
||||
$this->assertEquals(CONTEXT_USER, $context->contextlevel);
|
||||
$this->assertEquals($user->id, $context->instanceid);
|
||||
$contextlevels = array_column($contexts, 'contextlevel');
|
||||
$expected = [
|
||||
CONTEXT_SYSTEM,
|
||||
CONTEXT_COURSECAT
|
||||
];
|
||||
// Test the User's contexts equal the system and course category context.
|
||||
$this->assertEquals($expected, $contextlevels, '', 0, 10, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,26 +99,45 @@ class tool_cohortroles_privacy_testcase extends \core_privacy\tests\provider_tes
|
||||
$this->setUser($user);
|
||||
$this->setAdminUser();
|
||||
|
||||
$nocohortroles = 3;
|
||||
$this->setup_test_scenario_data($user->id, $nocohortroles);
|
||||
// Create course category.
|
||||
$coursecategory = $this->getDataGenerator()->create_category();
|
||||
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
|
||||
$systemctx = context_system::instance();
|
||||
// Create course.
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$coursectx = \context_course::instance($course->id);
|
||||
|
||||
// Test the User's retrieved contextlist contains only one context.
|
||||
$this->setup_test_scenario_data($user->id, $systemctx, 1);
|
||||
$this->setup_test_scenario_data($user->id, $coursecategoryctx, 1, 'Sausage roll 2',
|
||||
'sausageroll2');
|
||||
$this->setup_test_scenario_data($user->id, $coursectx, 1, 'Sausage roll 3',
|
||||
'sausageroll3');
|
||||
|
||||
// Test the User's retrieved contextlist contains two contexts.
|
||||
$contextlist = provider::get_contexts_for_userid($user->id);
|
||||
$contexts = $contextlist->get_contexts();
|
||||
$this->assertCount(1, $contexts);
|
||||
$this->assertCount(2, $contexts);
|
||||
|
||||
// Test the User's contexts equal the User's own context.
|
||||
$context = reset($contexts);
|
||||
$this->assertEquals(CONTEXT_USER, $context->contextlevel);
|
||||
$this->assertEquals($user->id, $context->instanceid);
|
||||
// Add a system, course category and course context to the approved context list.
|
||||
$approvedcontextids = [
|
||||
$systemctx->id,
|
||||
$coursecategoryctx->id,
|
||||
$coursectx->id
|
||||
];
|
||||
|
||||
// Retrieve the User's tool_cohortroles data.
|
||||
$approvedcontextlist = new approved_contextlist($user, 'tool_cohortroles', $contextlist->get_contextids());
|
||||
$approvedcontextlist = new approved_contextlist($user, 'tool_cohortroles', $approvedcontextids);
|
||||
provider::export_user_data($approvedcontextlist);
|
||||
|
||||
// Test the tool_cohortroles data is exported at the User context level.
|
||||
$writer = writer::with_context($context);
|
||||
// Test the tool_cohortroles data is exported at the system context level.
|
||||
$writer = writer::with_context($systemctx);
|
||||
$this->assertTrue($writer->has_any_data());
|
||||
// Test the tool_cohortroles data is exported at the course category context level.
|
||||
$writer = writer::with_context($coursecategoryctx);
|
||||
$this->assertTrue($writer->has_any_data());
|
||||
// Test the tool_cohortroles data is not exported at the course context level.
|
||||
$writer = writer::with_context($coursectx);
|
||||
$this->assertFalse($writer->has_any_data());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,29 +151,52 @@ class tool_cohortroles_privacy_testcase extends \core_privacy\tests\provider_tes
|
||||
$this->setUser($user);
|
||||
$this->setAdminUser();
|
||||
|
||||
$nocohortroles = 4;
|
||||
$this->setup_test_scenario_data($user->id, $nocohortroles);
|
||||
// Create course category.
|
||||
$coursecategory = $this->getDataGenerator()->create_category();
|
||||
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
|
||||
$systemctx = context_system::instance();
|
||||
|
||||
// Test the User's assigned cohortroles matches 4.
|
||||
$this->setup_test_scenario_data($user->id, $systemctx, 1);
|
||||
$this->setup_test_scenario_data($user->id, $coursecategoryctx, 1, 'Sausage roll 2',
|
||||
'sausageroll2');
|
||||
|
||||
// Test the User's assigned cohortroles matches 2.
|
||||
$cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
|
||||
$this->assertCount($nocohortroles, $cohortroles);
|
||||
$this->assertCount(2, $cohortroles);
|
||||
|
||||
// Test the User's retrieved contextlist contains only one context.
|
||||
// Test the User's retrieved contextlist contains two contexts.
|
||||
$contextlist = provider::get_contexts_for_userid($user->id);
|
||||
$contexts = $contextlist->get_contexts();
|
||||
$this->assertCount(2, $contexts);
|
||||
|
||||
// Make sure the user data is only being deleted in within the system and course category context.
|
||||
$usercontext = context_user::instance($user->id);
|
||||
// Delete all the User's records in mdl_tool_cohortroles table by the user context.
|
||||
provider::delete_data_for_all_users_in_context($usercontext);
|
||||
|
||||
// Test the cohort roles records in mdl_tool_cohortroles table is still present.
|
||||
$cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
|
||||
$this->assertCount(2, $cohortroles);
|
||||
|
||||
// Delete all the User's records in mdl_tool_cohortroles table by the specified system context.
|
||||
provider::delete_data_for_all_users_in_context($systemctx);
|
||||
|
||||
// The user data in the system context should be deleted.
|
||||
// Test the User's retrieved contextlist contains one context (course category).
|
||||
$contextlist = provider::get_contexts_for_userid($user->id);
|
||||
$contexts = $contextlist->get_contexts();
|
||||
$this->assertCount(1, $contexts);
|
||||
|
||||
// Test the User's contexts equal the User's own context.
|
||||
$context = reset($contexts);
|
||||
$this->assertEquals(CONTEXT_USER, $context->contextlevel);
|
||||
$this->assertEquals($user->id, $context->instanceid);
|
||||
|
||||
// Delete all the User's records in mdl_tool_cohortroles table by the specified User context.
|
||||
provider::delete_data_for_all_users_in_context($context);
|
||||
// Delete all the User's records in mdl_tool_cohortroles table by the specified course category context.
|
||||
provider::delete_data_for_all_users_in_context($coursecategoryctx);
|
||||
|
||||
// Test the cohort roles records in mdl_tool_cohortroles table is equals zero.
|
||||
$cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
|
||||
$this->assertCount(0, $cohortroles);
|
||||
|
||||
$contextlist = provider::get_contexts_for_userid($user->id);
|
||||
$contexts = $contextlist->get_contexts();
|
||||
$this->assertCount(0, $contexts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,24 +210,35 @@ class tool_cohortroles_privacy_testcase extends \core_privacy\tests\provider_tes
|
||||
$this->setUser($user);
|
||||
$this->setAdminUser();
|
||||
|
||||
$nocohortroles = 4;
|
||||
$this->setup_test_scenario_data($user->id, $nocohortroles);
|
||||
// Create course category.
|
||||
$coursecategory = $this->getDataGenerator()->create_category();
|
||||
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
|
||||
$systemctx = context_system::instance();
|
||||
|
||||
// Test the User's assigned cohortroles matches 4.
|
||||
$this->setup_test_scenario_data($user->id, $systemctx, 1);
|
||||
$this->setup_test_scenario_data($user->id, $coursecategoryctx, 1, 'Sausage roll 2',
|
||||
'sausageroll2');
|
||||
|
||||
// Test the User's assigned cohortroles matches 2.
|
||||
$cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
|
||||
$this->assertCount($nocohortroles, $cohortroles);
|
||||
$this->assertCount(2, $cohortroles);
|
||||
|
||||
// Test the User's retrieved contextlist contains only one context.
|
||||
// Test the User's retrieved contextlist contains two contexts.
|
||||
$contextlist = provider::get_contexts_for_userid($user->id);
|
||||
$contexts = $contextlist->get_contexts();
|
||||
$this->assertCount(1, $contexts);
|
||||
$this->assertCount(2, $contexts);
|
||||
|
||||
// Test the User's contexts equal the User's own context.
|
||||
$context = reset($contexts);
|
||||
$this->assertEquals(CONTEXT_USER, $context->contextlevel);
|
||||
$this->assertEquals($user->id, $context->instanceid);
|
||||
// Make sure the user data is only being deleted in within the system and the course category contexts.
|
||||
$usercontext = context_user::instance($user->id);
|
||||
// Delete all the User's records in mdl_tool_cohortroles table by the specified approved context list.
|
||||
$approvedcontextlist = new approved_contextlist($user, 'tool_cohortroles', [$usercontext->id]);
|
||||
provider::delete_data_for_user($approvedcontextlist);
|
||||
|
||||
// Delete all the User's records in mdl_tool_cohortroles table by the specified User approved context list.
|
||||
// Test the cohort roles records in mdl_tool_cohortroles table are still present.
|
||||
$cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
|
||||
$this->assertCount(2, $cohortroles);
|
||||
|
||||
// Delete all the User's records in mdl_tool_cohortroles table by the specified approved context list.
|
||||
$approvedcontextlist = new approved_contextlist($user, 'tool_cohortroles', $contextlist->get_contextids());
|
||||
provider::delete_data_for_user($approvedcontextlist);
|
||||
|
||||
@@ -179,19 +247,124 @@ class tool_cohortroles_privacy_testcase extends \core_privacy\tests\provider_tes
|
||||
$this->assertCount(0, $cohortroles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only users within a course context are fetched.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$component = 'tool_cohortroles';
|
||||
|
||||
// Create a user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$usercontext = context_user::instance($user->id);
|
||||
|
||||
// Create course category.
|
||||
$coursecategory = $this->getDataGenerator()->create_category();
|
||||
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
|
||||
$systemctx = context_system::instance();
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($systemctx, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(0, $userlist);
|
||||
|
||||
$this->setup_test_scenario_data($user->id, $systemctx, 1);
|
||||
$this->setup_test_scenario_data($user->id, $coursecategoryctx, 1, 'Sausage roll 2',
|
||||
'sausageroll2');
|
||||
|
||||
// The list of users within the system context should contain user.
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(1, $userlist);
|
||||
$this->assertTrue(in_array($user->id, $userlist->get_userids()));
|
||||
|
||||
// The list of users within the course category context should contain user.
|
||||
$userlist = new \core_privacy\local\request\userlist($coursecategoryctx, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(1, $userlist);
|
||||
$this->assertTrue(in_array($user->id, $userlist->get_userids()));
|
||||
|
||||
// The list of users within the user context should be empty.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(0, $userlist2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data for users in approved userlist is deleted.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
$component = 'tool_cohortroles';
|
||||
|
||||
// Create user1.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
// Create user2.
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
// Create user3.
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
$usercontext3 = context_user::instance($user3->id);
|
||||
|
||||
// Create course category.
|
||||
$coursecategory = $this->getDataGenerator()->create_category();
|
||||
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
|
||||
$systemctx = context_system::instance();
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
$this->setup_test_scenario_data($user1->id, $systemctx, 1);
|
||||
$this->setup_test_scenario_data($user2->id, $systemctx, 1, 'Sausage roll 2',
|
||||
'sausageroll2');
|
||||
$this->setup_test_scenario_data($user3->id, $coursecategoryctx, 1, 'Sausage roll 3',
|
||||
'sausageroll3');
|
||||
|
||||
$userlist1 = new \core_privacy\local\request\userlist($systemctx, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(2, $userlist1);
|
||||
$this->assertTrue(in_array($user1->id, $userlist1->get_userids()));
|
||||
$this->assertTrue(in_array($user2->id, $userlist1->get_userids()));
|
||||
|
||||
// Convert $userlist1 into an approved_contextlist.
|
||||
$approvedlist1 = new approved_userlist($systemctx, $component, [$user1->id]);
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist1);
|
||||
|
||||
// Re-fetch users in systemcontext.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($systemctx, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
// The user data of user1in systemcontext should be deleted.
|
||||
// The user data of user2 in systemcontext should be still present.
|
||||
$this->assertCount(1, $userlist1);
|
||||
$this->assertTrue(in_array($user2->id, $userlist1->get_userids()));
|
||||
|
||||
// Convert $userlist1 into an approved_contextlist in the user context.
|
||||
$approvedlist2 = new approved_userlist($usercontext3, $component, $userlist1->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist2);
|
||||
// Re-fetch users in systemcontext.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($systemctx, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
// The user data in systemcontext should not be deleted.
|
||||
$this->assertCount(1, $userlist1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to setup tool_cohortroles records for testing a specific user.
|
||||
*
|
||||
* @param int $userid The ID of the user used for testing.
|
||||
* @param int $nocohortroles The number of tool_cohortroles to create for the user.
|
||||
* @param string $rolename The name of the role to be created.
|
||||
* @param string $roleshortname The short name of the role to be created.
|
||||
* @throws \core_competency\invalid_persistent_exception
|
||||
* @throws coding_exception
|
||||
*/
|
||||
protected function setup_test_scenario_data($userid, $nocohortroles) {
|
||||
$roleid = create_role('Sausage Roll', 'sausageroll', 'mmmm');
|
||||
protected function setup_test_scenario_data($userid, $context, $nocohortroles, $rolename = 'Sausage Roll',
|
||||
$roleshortname = 'sausageroll') {
|
||||
$roleid = create_role($rolename, $roleshortname, 'mmmm');
|
||||
|
||||
$result = new \stdClass();
|
||||
$result->contextid = $context->id;
|
||||
|
||||
for ($c = 0; $c < $nocohortroles; $c++) {
|
||||
$cohort = $this->getDataGenerator()->create_cohort();
|
||||
$cohort = $this->getDataGenerator()->create_cohort($result);
|
||||
|
||||
$params = (object)array(
|
||||
'userid' => $userid,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates"],function(a,b,c,d,e,f,g){function h(b,d,h,j,k,l,m,n){null!==h&&l.forEach(function(a){a.id===h&&(a.selected=!0)}),null!==j&&m.forEach(function(a){a.id===j&&(a.selected=!0)});var o={contextlevel:d,categoryoptions:l,purposeoptions:m};null!==n&&n.length&&(null===k?o.newactivitydefaults=!0:n.forEach(function(a){k===a.name&&(a.selected=!0)}),o.modemodule=!0,o.activityoptions=n),e.create({title:b,body:g.render("tool_dataprivacy/category_purpose_form",o),type:e.types.SAVE_CANCEL,large:!0}).then(function(b){return b.getRoot().on(f.save,function(){var b=a("#activity"),c="undefined"!=typeof b?b.val():null,d=a("#override"),e="undefined"!=typeof d&&d.is(":checked");i(a("#contextlevel").val(),a("#category").val(),a("#purpose").val(),c,e)}),b.getRoot().on(f.hidden,function(){b.destroy()}),b.show(),b})["catch"](c.exception)}function i(a,c,d,e,f){var g={methodname:"tool_dataprivacy_set_context_defaults",args:{contextlevel:a,category:c,purpose:d,override:f,activity:e}};b.call([g])[0].done(function(a){a.result&&window.location.reload()})}var j={EDIT_LEVEL_DEFAULTS:'[data-action="edit-level-defaults"]',NEW_ACTIVITY_DEFAULTS:'[data-action="new-activity-defaults"]',EDIT_ACTIVITY_DEFAULTS:'[data-action="edit-activity-defaults"]',DELETE_ACTIVITY_DEFAULTS:'[data-action="delete-activity-defaults"]'},k=-1,l=function(){this.registerEvents()};return l.prototype.registerEvents=function(){a(j.EDIT_LEVEL_DEFAULTS).click(function(e){e.preventDefault();var f=a(this),g=f.data("contextlevel"),i=f.data("category"),j=f.data("purpose"),k=[{methodname:"tool_dataprivacy_get_category_options",args:{}},{methodname:"tool_dataprivacy_get_purpose_options",args:{}}],l=b.call(k),m=d.get_string("editdefaults","tool_dataprivacy",a("#defaults-header").text());a.when(l[0],l[1],m).then(function(a,b,c){var d=a.options,e=b.options;return h(c,g,i,j,null,d,e,null),!0})["catch"](c.exception)}),a(j.NEW_ACTIVITY_DEFAULTS).click(function(e){e.preventDefault();var f=a(this),g=f.data("contextlevel"),i=[{methodname:"tool_dataprivacy_get_category_options",args:{}},{methodname:"tool_dataprivacy_get_purpose_options",args:{}},{methodname:"tool_dataprivacy_get_activity_options",args:{nodefaults:!0}}],j=b.call(i),k=d.get_string("addnewdefaults","tool_dataprivacy");a.when(j[0],j[1],j[2],k).then(function(a,b,c,d){var e=a.options,f=b.options,i=c.options;return h(d,g,null,null,null,e,f,i),!0})["catch"](c.exception)}),a(j.EDIT_ACTIVITY_DEFAULTS).click(function(e){e.preventDefault();var f=a(this),g=f.data("contextlevel"),i=f.data("category"),j=f.data("purpose"),k=f.data("activityname"),l=[{methodname:"tool_dataprivacy_get_category_options",args:{}},{methodname:"tool_dataprivacy_get_purpose_options",args:{}},{methodname:"tool_dataprivacy_get_activity_options",args:{}}],m=b.call(l),n=d.get_string("editmoduledefaults","tool_dataprivacy");a.when(m[0],m[1],m[2],n).then(function(a,b,c,d){var e=a.options,f=b.options,l=c.options;return h(d,g,i,j,k,e,f,l),!0})["catch"](c.exception)}),a(j.DELETE_ACTIVITY_DEFAULTS).click(function(b){b.preventDefault();var h=a(this),j=h.data("contextlevel"),l=h.data("activityname"),m=h.data("activitydisplayname"),n=k,o=k;e.create({title:d.get_string("deletedefaults","tool_dataprivacy",m),body:g.render("tool_dataprivacy/delete_activity_defaults",{activityname:m}),type:e.types.SAVE_CANCEL,large:!0}).then(function(a){return a.setSaveButtonText(d.get_string("delete")),a.getRoot().on(f.save,function(){i(j,n,o,l,!1)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a.show(),!0})["catch"](c.exception)})},{init:function(){return new l}}});
|
||||
+1
-1
@@ -1 +1 @@
|
||||
define([],function(){return{approve:"tool_dataprivacy-data_request:approve",deny:"tool_dataprivacy-data_request:deny",complete:"tool_dataprivacy-data_request:complete"}});
|
||||
define([],function(){return{approve:"tool_dataprivacy-data_request:approve",bulkApprove:"tool_dataprivacy-data_request:bulk_approve",deny:"tool_dataprivacy-data_request:deny",bulkDeny:"tool_dataprivacy-data_request:bulk_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","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});
|
||||
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){return{wsfunction:"tool_dataprivacy_approve_data_request",wsparams:{requestid:a}}}function k(a){return{wsfunction:"tool_dataprivacy_bulk_approve_data_requests",wsparams:{requestids:a}}}function l(a){return{wsfunction:"tool_dataprivacy_deny_data_request",wsparams:{requestid:a}}}function m(a){return{wsfunction:"tool_dataprivacy_bulk_deny_data_requests",wsparams:{requestids:a}}}function n(a){return{wsfunction:"tool_dataprivacy_mark_complete",wsparams:{requestid:a}}}function o(a,b){var g=[];switch(a){case i.approve:g=[{key:"approverequest",component:"tool_dataprivacy"},{key:"confirmapproval",component:"tool_dataprivacy"}];break;case i.bulkApprove:g=[{key:"bulkapproverequests",component:"tool_dataprivacy"},{key:"confirmbulkapproval",component:"tool_dataprivacy"}];break;case i.deny:g=[{key:"denyrequest",component:"tool_dataprivacy"},{key:"confirmdenial",component:"tool_dataprivacy"}];break;case i.bulkDeny:g=[{key:"bulkdenyrequests",component:"tool_dataprivacy"},{key:"confirmbulkdenial",component:"tool_dataprivacy"}];break;case i.complete:g=[{key:"markcomplete",component:"tool_dataprivacy"},{key:"confirmcompletion",component:"tool_dataprivacy"}]}var h="";d.get_strings(g).then(function(a){h=a[0];var b=a[1];return e.create({title:h,body:b,type:e.types.SAVE_CANCEL})}).then(function(a){a.setSaveButtonText(h),a.getRoot().on(f.save,function(){p(b.wsfunction,b.wsparams)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a.show()})["catch"](c.exception)}function p(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 q={APPROVE_REQUEST:'[data-action="approve"]',DENY_REQUEST:'[data-action="deny"]',VIEW_REQUEST:'[data-action="view"]',MARK_COMPLETE:'[data-action="complete"]',CHANGE_BULK_ACTION:'[id="bulk-action"]',CONFIRM_BULK_ACTION:'[id="confirm-bulk-action"]',SELECT_ALL:'[data-action="selectall"]'},r={APPROVE:1,DENY:2},s={SELECT_REQUEST:".selectrequests"},t=function(){this.registerEvents()};return t.prototype.registerEvents=function(){a(q.VIEW_REQUEST).click(function(d){d.preventDefault();var k=a(this).data("requestid"),m={requestid:k},n={methodname:"tool_dataprivacy_get_data_request",args:m},q=b.call([n]);a.when(q[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(){o(i.approve,j(k))}),a.getRoot().on(i.deny,function(){o(i.deny,l(k))}),a.getRoot().on(i.complete,function(){var a={requestid:k};p("tool_dataprivacy_mark_complete",a)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a.show()})["catch"](c.exception)}),a(q.APPROVE_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");o(i.approve,j(c))}),a(q.DENY_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");o(i.deny,l(c))}),a(q.MARK_COMPLETE).click(function(b){b.preventDefault();var c=a(this).data("requestid");o(i.complete,n(c))}),a(q.CONFIRM_BULK_ACTION).click(function(){var b=[],e="",f={},g=[{key:"selectbulkaction",component:"tool_dataprivacy"},{key:"selectdatarequests",component:"tool_dataprivacy"},{key:"ok"}],h=parseInt(a("#bulk-action").val());if(h!=r.APPROVE&&h!=r.DENY)return void d.get_strings(g).done(function(a){c.alert("",a[0],a[2])}).fail(c.exception);if(a(".selectrequests:checked").each(function(){b.push(a(this).val())}),b.length<1)return void d.get_strings(g).done(function(a){c.alert("",a[1],a[2])}).fail(c.exception);switch(h){case r.APPROVE:e=i.bulkApprove,f=k(b);break;case r.DENY:e=i.bulkDeny,f=m(b)}o(e,f)}),a(q.SELECT_ALL).change(function(b){b.preventDefault();var c=a(this).is(":checked");a(s.SELECT_REQUEST).prop("checked",c)})},t});
|
||||
@@ -0,0 +1,315 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* AMD module for data registry defaults actions.
|
||||
*
|
||||
* @module tool_dataprivacy/defaultsactions
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define([
|
||||
'jquery',
|
||||
'core/ajax',
|
||||
'core/notification',
|
||||
'core/str',
|
||||
'core/modal_factory',
|
||||
'core/modal_events',
|
||||
'core/templates'],
|
||||
function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
|
||||
|
||||
/**
|
||||
* List of action selectors.
|
||||
*
|
||||
* @type {{EDIT_LEVEL_DEFAULTS: string}}
|
||||
* @type {{NEW_ACTIVITY_DEFAULTS: string}}
|
||||
* @type {{EDIT_ACTIVITY_DEFAULTS: string}}
|
||||
* @type {{DELETE_ACTIVITY_DEFAULTS: string}}
|
||||
*/
|
||||
var ACTIONS = {
|
||||
EDIT_LEVEL_DEFAULTS: '[data-action="edit-level-defaults"]',
|
||||
NEW_ACTIVITY_DEFAULTS: '[data-action="new-activity-defaults"]',
|
||||
EDIT_ACTIVITY_DEFAULTS: '[data-action="edit-activity-defaults"]',
|
||||
DELETE_ACTIVITY_DEFAULTS: '[data-action="delete-activity-defaults"]'
|
||||
};
|
||||
|
||||
/** @type {{INHERIT: Number}} **/
|
||||
var INHERIT = -1;
|
||||
|
||||
/**
|
||||
* DefaultsActions class.
|
||||
*/
|
||||
var DefaultsActions = function() {
|
||||
this.registerEvents();
|
||||
};
|
||||
|
||||
/**
|
||||
* Register event listeners.
|
||||
*/
|
||||
DefaultsActions.prototype.registerEvents = function() {
|
||||
$(ACTIONS.EDIT_LEVEL_DEFAULTS).click(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var button = $(this);
|
||||
var contextLevel = button.data('contextlevel');
|
||||
var category = button.data('category');
|
||||
var purpose = button.data('purpose');
|
||||
|
||||
// Get options.
|
||||
var requests = [
|
||||
{methodname: 'tool_dataprivacy_get_category_options', args: {}},
|
||||
{methodname: 'tool_dataprivacy_get_purpose_options', args: {}}
|
||||
];
|
||||
|
||||
var promises = Ajax.call(requests);
|
||||
var titlePromise = Str.get_string('editdefaults', 'tool_dataprivacy', $('#defaults-header').text());
|
||||
$.when(promises[0], promises[1], titlePromise).then(function(categoryResponse, purposeResponse, title) {
|
||||
var categories = categoryResponse.options;
|
||||
var purposes = purposeResponse.options;
|
||||
showDefaultsFormModal(title, contextLevel, category, purpose, null, categories, purposes, null);
|
||||
|
||||
return true;
|
||||
}).catch(Notification.exception);
|
||||
});
|
||||
|
||||
$(ACTIONS.NEW_ACTIVITY_DEFAULTS).click(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var button = $(this);
|
||||
var contextLevel = button.data('contextlevel');
|
||||
|
||||
// Get options.
|
||||
var requests = [
|
||||
{methodname: 'tool_dataprivacy_get_category_options', args: {}},
|
||||
{methodname: 'tool_dataprivacy_get_purpose_options', args: {}},
|
||||
{methodname: 'tool_dataprivacy_get_activity_options', args: {'nodefaults': true}}
|
||||
];
|
||||
|
||||
var promises = Ajax.call(requests);
|
||||
var titlePromise = Str.get_string('addnewdefaults', 'tool_dataprivacy');
|
||||
|
||||
$.when(promises[0], promises[1], promises[2], titlePromise).then(
|
||||
function(categoryResponse, purposeResponse, activityResponse, title) {
|
||||
var categories = categoryResponse.options;
|
||||
var purposes = purposeResponse.options;
|
||||
var activities = activityResponse.options;
|
||||
|
||||
showDefaultsFormModal(title, contextLevel, null, null, null, categories, purposes, activities);
|
||||
|
||||
return true;
|
||||
|
||||
}).catch(Notification.exception);
|
||||
}
|
||||
);
|
||||
|
||||
$(ACTIONS.EDIT_ACTIVITY_DEFAULTS).click(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var button = $(this);
|
||||
var contextLevel = button.data('contextlevel');
|
||||
var category = button.data('category');
|
||||
var purpose = button.data('purpose');
|
||||
var activity = button.data('activityname');
|
||||
|
||||
// Get options.
|
||||
var requests = [
|
||||
{methodname: 'tool_dataprivacy_get_category_options', args: {}},
|
||||
{methodname: 'tool_dataprivacy_get_purpose_options', args: {}},
|
||||
{methodname: 'tool_dataprivacy_get_activity_options', args: {}}
|
||||
];
|
||||
|
||||
var promises = Ajax.call(requests);
|
||||
var titlePromise = Str.get_string('editmoduledefaults', 'tool_dataprivacy');
|
||||
|
||||
$.when(promises[0], promises[1], promises[2], titlePromise).then(
|
||||
function(categoryResponse, purposeResponse, activityResponse, title) {
|
||||
var categories = categoryResponse.options;
|
||||
var purposes = purposeResponse.options;
|
||||
var activities = activityResponse.options;
|
||||
|
||||
showDefaultsFormModal(title, contextLevel, category, purpose, activity, categories, purposes, activities);
|
||||
|
||||
return true;
|
||||
|
||||
}).catch(Notification.exception);
|
||||
}
|
||||
);
|
||||
|
||||
$(ACTIONS.DELETE_ACTIVITY_DEFAULTS).click(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var button = $(this);
|
||||
var contextLevel = button.data('contextlevel');
|
||||
var activity = button.data('activityname');
|
||||
var activityDisplayName = button.data('activitydisplayname');
|
||||
// Set category and purpose to inherit (-1).
|
||||
var category = INHERIT;
|
||||
var purpose = INHERIT;
|
||||
|
||||
ModalFactory.create({
|
||||
title: Str.get_string('deletedefaults', 'tool_dataprivacy', activityDisplayName),
|
||||
body: Templates.render('tool_dataprivacy/delete_activity_defaults', {"activityname": activityDisplayName}),
|
||||
type: ModalFactory.types.SAVE_CANCEL,
|
||||
large: true
|
||||
}).then(function(modal) {
|
||||
modal.setSaveButtonText(Str.get_string('delete'));
|
||||
|
||||
// Handle save event.
|
||||
modal.getRoot().on(ModalEvents.save, function() {
|
||||
setContextDefaults(contextLevel, category, purpose, activity, false);
|
||||
});
|
||||
|
||||
// Handle hidden event.
|
||||
modal.getRoot().on(ModalEvents.hidden, function() {
|
||||
// Destroy when hidden.
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
modal.show();
|
||||
|
||||
return true;
|
||||
}).catch(Notification.exception);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepares and renders the modal for setting the defaults for the given context level/plugin.
|
||||
*
|
||||
* @param {String} title The modal's title.
|
||||
* @param {Number} contextLevel The context level to set defaults for.
|
||||
* @param {Number} category The current category ID.
|
||||
* @param {Number} purpose The current purpose ID.
|
||||
* @param {String} activity The plugin name of the activity. Optional.
|
||||
* @param {Array} categoryOptions The list of category options.
|
||||
* @param {Array} purposeOptions The list of purpose options.
|
||||
* @param {Array} activityOptions The list of activity options. Optional.
|
||||
*/
|
||||
function showDefaultsFormModal(title, contextLevel, category, purpose, activity,
|
||||
categoryOptions, purposeOptions, activityOptions) {
|
||||
|
||||
if (category !== null) {
|
||||
categoryOptions.forEach(function(currentValue) {
|
||||
if (currentValue.id === category) {
|
||||
currentValue.selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (purpose !== null) {
|
||||
purposeOptions.forEach(function(currentValue) {
|
||||
if (currentValue.id === purpose) {
|
||||
currentValue.selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var templateContext = {
|
||||
"contextlevel": contextLevel,
|
||||
"categoryoptions": categoryOptions,
|
||||
"purposeoptions": purposeOptions
|
||||
};
|
||||
|
||||
// Check the activityOptions parameter that was passed.
|
||||
if (activityOptions !== null && activityOptions.length) {
|
||||
// Check the activity parameter that was passed.
|
||||
if (activity === null) {
|
||||
// We're setting a new defaults for a module.
|
||||
templateContext.newactivitydefaults = true;
|
||||
|
||||
} else {
|
||||
// Edit mode. Set selection.
|
||||
activityOptions.forEach(function(currentValue) {
|
||||
if (activity === currentValue.name) {
|
||||
currentValue.selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
templateContext.modemodule = true;
|
||||
templateContext.activityoptions = activityOptions;
|
||||
}
|
||||
|
||||
ModalFactory.create({
|
||||
title: title,
|
||||
body: Templates.render('tool_dataprivacy/category_purpose_form', templateContext),
|
||||
type: ModalFactory.types.SAVE_CANCEL,
|
||||
large: true
|
||||
}).then(function(modal) {
|
||||
|
||||
// Handle save event.
|
||||
modal.getRoot().on(ModalEvents.save, function() {
|
||||
var activity = $('#activity');
|
||||
var activityVal = typeof activity !== 'undefined' ? activity.val() : null;
|
||||
var override = $('#override');
|
||||
var overrideVal = typeof override !== 'undefined' ? override.is(':checked') : false;
|
||||
|
||||
setContextDefaults($('#contextlevel').val(), $('#category').val(), $('#purpose').val(), activityVal, overrideVal);
|
||||
});
|
||||
|
||||
// Handle hidden event.
|
||||
modal.getRoot().on(ModalEvents.hidden, function() {
|
||||
// Destroy when hidden.
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
modal.show();
|
||||
|
||||
return modal;
|
||||
}).catch(Notification.exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a the tool_dataprivacy_set_context_defaults WS function.
|
||||
*
|
||||
* @param {Number} contextLevel The context level.
|
||||
* @param {Number} category The category ID.
|
||||
* @param {Number} purpose The purpose ID.
|
||||
* @param {String} activity The plugin name of the activity module.
|
||||
* @param {Boolean} override Whether to override custom instances.
|
||||
*/
|
||||
function setContextDefaults(contextLevel, category, purpose, activity, override) {
|
||||
var request = {
|
||||
methodname: 'tool_dataprivacy_set_context_defaults',
|
||||
args: {
|
||||
'contextlevel': contextLevel,
|
||||
'category': category,
|
||||
'purpose': purpose,
|
||||
'override': override,
|
||||
'activity': activity
|
||||
}
|
||||
};
|
||||
|
||||
Ajax.call([request])[0].done(function(data) {
|
||||
if (data.result) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return /** @alias module:tool_dataprivacy/defaultsactions */ {
|
||||
// Public variables and functions.
|
||||
|
||||
/**
|
||||
* Initialise the module.
|
||||
*
|
||||
* @method init
|
||||
* @return {DefaultsActions}
|
||||
*/
|
||||
'init': function() {
|
||||
return new DefaultsActions();
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -25,7 +25,9 @@
|
||||
define([], function() {
|
||||
return {
|
||||
approve: 'tool_dataprivacy-data_request:approve',
|
||||
bulkApprove: 'tool_dataprivacy-data_request:bulk_approve',
|
||||
deny: 'tool_dataprivacy-data_request:deny',
|
||||
bulkDeny: 'tool_dataprivacy-data_request:bulk_deny',
|
||||
complete: 'tool_dataprivacy-data_request:complete'
|
||||
};
|
||||
});
|
||||
|
||||
@@ -40,12 +40,38 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
* @type {{DENY_REQUEST: string}}
|
||||
* @type {{VIEW_REQUEST: string}}
|
||||
* @type {{MARK_COMPLETE: string}}
|
||||
* @type {{CHANGE_BULK_ACTION: string}}
|
||||
* @type {{CONFIRM_BULK_ACTION: string}}
|
||||
* @type {{SELECT_ALL: string}}
|
||||
*/
|
||||
var ACTIONS = {
|
||||
APPROVE_REQUEST: '[data-action="approve"]',
|
||||
DENY_REQUEST: '[data-action="deny"]',
|
||||
VIEW_REQUEST: '[data-action="view"]',
|
||||
MARK_COMPLETE: '[data-action="complete"]'
|
||||
MARK_COMPLETE: '[data-action="complete"]',
|
||||
CHANGE_BULK_ACTION: '[id="bulk-action"]',
|
||||
CONFIRM_BULK_ACTION: '[id="confirm-bulk-action"]',
|
||||
SELECT_ALL: '[data-action="selectall"]'
|
||||
};
|
||||
|
||||
/**
|
||||
* List of available bulk actions.
|
||||
*
|
||||
* @type {{APPROVE: number}}
|
||||
* @type {{DENY: number}}
|
||||
*/
|
||||
var BULK_ACTIONS = {
|
||||
APPROVE: 1,
|
||||
DENY: 2
|
||||
};
|
||||
|
||||
/**
|
||||
* List of selectors.
|
||||
*
|
||||
* @type {{SELECT_REQUEST: string}}
|
||||
*/
|
||||
var SELECTORS = {
|
||||
SELECT_REQUEST: '.selectrequests'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -103,12 +129,12 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
}).then(function(modal) {
|
||||
// Handle approve event.
|
||||
modal.getRoot().on(DataPrivacyEvents.approve, function() {
|
||||
showConfirmation(DataPrivacyEvents.approve, requestId);
|
||||
showConfirmation(DataPrivacyEvents.approve, approveEventWsData(requestId));
|
||||
});
|
||||
|
||||
// Handle deny event.
|
||||
modal.getRoot().on(DataPrivacyEvents.deny, function() {
|
||||
showConfirmation(DataPrivacyEvents.deny, requestId);
|
||||
showConfirmation(DataPrivacyEvents.deny, denyEventWsData(requestId));
|
||||
});
|
||||
|
||||
// Handle send event.
|
||||
@@ -137,34 +163,158 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
e.preventDefault();
|
||||
|
||||
var requestId = $(this).data('requestid');
|
||||
showConfirmation(DataPrivacyEvents.approve, requestId);
|
||||
showConfirmation(DataPrivacyEvents.approve, approveEventWsData(requestId));
|
||||
});
|
||||
|
||||
$(ACTIONS.DENY_REQUEST).click(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var requestId = $(this).data('requestid');
|
||||
showConfirmation(DataPrivacyEvents.deny, requestId);
|
||||
showConfirmation(DataPrivacyEvents.deny, denyEventWsData(requestId));
|
||||
});
|
||||
|
||||
$(ACTIONS.MARK_COMPLETE).click(function(e) {
|
||||
e.preventDefault();
|
||||
showConfirmation(DataPrivacyEvents.complete, $(this).data('requestid'));
|
||||
|
||||
var requestId = $(this).data('requestid');
|
||||
showConfirmation(DataPrivacyEvents.complete, completeEventWsData(requestId));
|
||||
});
|
||||
|
||||
$(ACTIONS.CONFIRM_BULK_ACTION).click(function() {
|
||||
var requestIds = [];
|
||||
var actionEvent = '';
|
||||
var wsdata = {};
|
||||
var bulkActionKeys = [
|
||||
{
|
||||
key: 'selectbulkaction',
|
||||
component: 'tool_dataprivacy'
|
||||
},
|
||||
{
|
||||
key: 'selectdatarequests',
|
||||
component: 'tool_dataprivacy'
|
||||
},
|
||||
{
|
||||
key: 'ok'
|
||||
}
|
||||
];
|
||||
|
||||
var bulkaction = parseInt($('#bulk-action').val());
|
||||
|
||||
if (bulkaction != BULK_ACTIONS.APPROVE && bulkaction != BULK_ACTIONS.DENY) {
|
||||
Str.get_strings(bulkActionKeys).done(function(langStrings) {
|
||||
Notification.alert('', langStrings[0], langStrings[2]);
|
||||
}).fail(Notification.exception);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$(".selectrequests:checked").each(function() {
|
||||
requestIds.push($(this).val());
|
||||
});
|
||||
|
||||
if (requestIds.length < 1) {
|
||||
Str.get_strings(bulkActionKeys).done(function(langStrings) {
|
||||
Notification.alert('', langStrings[1], langStrings[2]);
|
||||
}).fail(Notification.exception);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (bulkaction) {
|
||||
case BULK_ACTIONS.APPROVE:
|
||||
actionEvent = DataPrivacyEvents.bulkApprove;
|
||||
wsdata = bulkApproveEventWsData(requestIds);
|
||||
break;
|
||||
case BULK_ACTIONS.DENY:
|
||||
actionEvent = DataPrivacyEvents.bulkDeny;
|
||||
wsdata = bulkDenyEventWsData(requestIds);
|
||||
}
|
||||
|
||||
showConfirmation(actionEvent, wsdata);
|
||||
});
|
||||
|
||||
$(ACTIONS.SELECT_ALL).change(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var selectAll = $(this).is(':checked');
|
||||
$(SELECTORS.SELECT_REQUEST).prop('checked', selectAll);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the webservice data for the approve request action.
|
||||
*
|
||||
* @param {Number} requestId The ID of the request.
|
||||
* @return {Object}
|
||||
*/
|
||||
function approveEventWsData(requestId) {
|
||||
return {
|
||||
'wsfunction': 'tool_dataprivacy_approve_data_request',
|
||||
'wsparams': {'requestid': requestId}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the webservice data for the bulk approve request action.
|
||||
*
|
||||
* @param {Array} requestIds The array of request ID's.
|
||||
* @return {Object}
|
||||
*/
|
||||
function bulkApproveEventWsData(requestIds) {
|
||||
return {
|
||||
'wsfunction': 'tool_dataprivacy_bulk_approve_data_requests',
|
||||
'wsparams': {'requestids': requestIds}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the webservice data for the deny request action.
|
||||
*
|
||||
* @param {Number} requestId The ID of the request.
|
||||
* @return {Object}
|
||||
*/
|
||||
function denyEventWsData(requestId) {
|
||||
return {
|
||||
'wsfunction': 'tool_dataprivacy_deny_data_request',
|
||||
'wsparams': {'requestid': requestId}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the webservice data for the bulk deny request action.
|
||||
*
|
||||
* @param {Array} requestIds The array of request ID's.
|
||||
* @return {Object}
|
||||
*/
|
||||
function bulkDenyEventWsData(requestIds) {
|
||||
return {
|
||||
'wsfunction': 'tool_dataprivacy_bulk_deny_data_requests',
|
||||
'wsparams': {'requestids': requestIds}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the webservice data for the complete request action.
|
||||
*
|
||||
* @param {Number} requestId The ID of the request.
|
||||
* @return {Object}
|
||||
*/
|
||||
function completeEventWsData(requestId) {
|
||||
return {
|
||||
'wsfunction': 'tool_dataprivacy_mark_complete',
|
||||
'wsparams': {'requestid': requestId}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the confirmation dialogue.
|
||||
*
|
||||
* @param {String} action The action name.
|
||||
* @param {Number} requestId The request ID.
|
||||
* @param {Object} wsdata Object containing ws data.
|
||||
*/
|
||||
function showConfirmation(action, requestId) {
|
||||
function showConfirmation(action, wsdata) {
|
||||
var keys = [];
|
||||
var wsfunction = '';
|
||||
var params = {
|
||||
'requestid': requestId
|
||||
};
|
||||
|
||||
switch (action) {
|
||||
case DataPrivacyEvents.approve:
|
||||
keys = [
|
||||
@@ -177,7 +327,18 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
component: 'tool_dataprivacy'
|
||||
}
|
||||
];
|
||||
wsfunction = 'tool_dataprivacy_approve_data_request';
|
||||
break;
|
||||
case DataPrivacyEvents.bulkApprove:
|
||||
keys = [
|
||||
{
|
||||
key: 'bulkapproverequests',
|
||||
component: 'tool_dataprivacy'
|
||||
},
|
||||
{
|
||||
key: 'confirmbulkapproval',
|
||||
component: 'tool_dataprivacy'
|
||||
}
|
||||
];
|
||||
break;
|
||||
case DataPrivacyEvents.deny:
|
||||
keys = [
|
||||
@@ -190,7 +351,18 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
component: 'tool_dataprivacy'
|
||||
}
|
||||
];
|
||||
wsfunction = 'tool_dataprivacy_deny_data_request';
|
||||
break;
|
||||
case DataPrivacyEvents.bulkDeny:
|
||||
keys = [
|
||||
{
|
||||
key: 'bulkdenyrequests',
|
||||
component: 'tool_dataprivacy'
|
||||
},
|
||||
{
|
||||
key: 'confirmbulkdenial',
|
||||
component: 'tool_dataprivacy'
|
||||
}
|
||||
];
|
||||
break;
|
||||
case DataPrivacyEvents.complete:
|
||||
keys = [
|
||||
@@ -203,7 +375,6 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
component: 'tool_dataprivacy'
|
||||
}
|
||||
];
|
||||
wsfunction = 'tool_dataprivacy_mark_complete';
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -221,7 +392,7 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
|
||||
// Handle save event.
|
||||
modal.getRoot().on(ModalEvents.save, function() {
|
||||
handleSave(wsfunction, params);
|
||||
handleSave(wsdata.wsfunction, wsdata.wsparams);
|
||||
});
|
||||
|
||||
// Handle hidden event.
|
||||
|
||||
@@ -33,6 +33,7 @@ $title = get_string('editcategories', 'tool_dataprivacy');
|
||||
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->header();
|
||||
echo $output->heading($title);
|
||||
|
||||
$categories = \tool_dataprivacy\api::get_categories();
|
||||
$renderable = new \tool_dataprivacy\output\categories($categories);
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
namespace tool_dataprivacy;
|
||||
|
||||
use coding_exception;
|
||||
use context_course;
|
||||
use context_helper;
|
||||
use context_system;
|
||||
use core\invalid_persistent_exception;
|
||||
use core\message\message;
|
||||
@@ -41,6 +41,7 @@ use tool_dataprivacy\external\data_request_exporter;
|
||||
use tool_dataprivacy\local\helper;
|
||||
use tool_dataprivacy\task\initiate_data_request_task;
|
||||
use tool_dataprivacy\task\process_data_request_task;
|
||||
use tool_dataprivacy\data_request;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -94,6 +95,12 @@ class api {
|
||||
/** Data delete request completed, account is removed. */
|
||||
const DATAREQUEST_STATUS_DELETED = 10;
|
||||
|
||||
/** Approve data request. */
|
||||
const DATAREQUEST_ACTION_APPROVE = 1;
|
||||
|
||||
/** Reject data request. */
|
||||
const DATAREQUEST_ACTION_REJECT = 2;
|
||||
|
||||
/**
|
||||
* Determines whether the user can contact the site's Data Protection Officer via Moodle.
|
||||
*
|
||||
@@ -105,18 +112,14 @@ class api {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check's whether the current user has the capability to manage data requests.
|
||||
* Checks whether the current user has the capability to manage data requests.
|
||||
*
|
||||
* @param int $userid The user ID.
|
||||
* @return bool
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public static function can_manage_data_requests($userid) {
|
||||
$context = context_system::instance();
|
||||
|
||||
// A user can manage data requests if he/she has the site DPO role and has the capability to manage data requests.
|
||||
return self::is_site_dpo($userid) && has_capability('tool/dataprivacy:managedatarequests', $context, $userid);
|
||||
// Privacy officers can manage data requests.
|
||||
return self::is_site_dpo($userid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,6 +139,31 @@ class api {
|
||||
require_capability('tool/dataprivacy:managedataregistry', $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of configured privacy officer roles.
|
||||
*
|
||||
* Every time this function is called, it checks each role if they have the 'managedatarequests' capability and removes
|
||||
* any role that doesn't have the required capability anymore.
|
||||
*
|
||||
* @return int[]
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public static function get_assigned_privacy_officer_roles() {
|
||||
$roleids = [];
|
||||
|
||||
// Get roles from config.
|
||||
$configroleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
|
||||
if (!empty($configroleids)) {
|
||||
// Fetch roles that have the capability to manage data requests.
|
||||
$capableroles = array_keys(get_roles_with_capability('tool/dataprivacy:managedatarequests'));
|
||||
|
||||
// Extract the configured roles that have the capability from the list of capable roles.
|
||||
$roleids = array_intersect($capableroles, $configroleids);
|
||||
}
|
||||
|
||||
return $roleids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the role shortnames of Data Protection Officer roles.
|
||||
*
|
||||
@@ -144,7 +172,7 @@ class api {
|
||||
public static function get_dpo_role_names() : array {
|
||||
global $DB;
|
||||
|
||||
$dporoleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
|
||||
$dporoleids = self::get_assigned_privacy_officer_roles();
|
||||
$dponames = array();
|
||||
|
||||
if (!empty($dporoleids)) {
|
||||
@@ -156,20 +184,15 @@ class api {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of users with the Data Protection Officer role.
|
||||
*
|
||||
* @throws dml_exception
|
||||
* Fetches the list of users with the Privacy Officer role.
|
||||
*/
|
||||
public static function get_site_dpos() {
|
||||
// Get role(s) that can manage data requests.
|
||||
$dporoles = explode(',', get_config('tool_dataprivacy', 'dporoles'));
|
||||
$dporoles = self::get_assigned_privacy_officer_roles();
|
||||
|
||||
$dpos = [];
|
||||
$context = context_system::instance();
|
||||
foreach ($dporoles as $roleid) {
|
||||
if (empty($roleid)) {
|
||||
continue;
|
||||
}
|
||||
$allnames = get_all_user_name_fields(true, 'u');
|
||||
$fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
|
||||
'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
|
||||
@@ -189,15 +212,14 @@ class api {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given user is a site DPO.
|
||||
* Checks whether a given user is a site Privacy Officer.
|
||||
*
|
||||
* @param int $userid The user ID.
|
||||
* @return bool
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public static function is_site_dpo($userid) {
|
||||
$dpos = self::get_site_dpos();
|
||||
return array_key_exists($userid, $dpos);
|
||||
return array_key_exists($userid, $dpos) || is_siteadmin();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,33 +228,24 @@ class api {
|
||||
* @param int $foruser The user whom the request is being made for.
|
||||
* @param int $type The request type.
|
||||
* @param string $comments Request comments.
|
||||
* @param int $creationmethod The creation method of the data request.
|
||||
* @return data_request
|
||||
* @throws invalid_persistent_exception
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public static function create_data_request($foruser, $type, $comments = '') {
|
||||
public static function create_data_request($foruser, $type, $comments = '',
|
||||
$creationmethod = data_request::DATAREQUEST_CREATION_MANUAL) {
|
||||
global $USER;
|
||||
|
||||
$datarequest = new data_request();
|
||||
// The user the request is being made for.
|
||||
$datarequest->set('userid', $foruser);
|
||||
|
||||
$requestinguser = $USER->id;
|
||||
// Check when the user is making a request on behalf of another.
|
||||
if ($requestinguser != $foruser) {
|
||||
if (self::is_site_dpo($requestinguser)) {
|
||||
// The user making the request is a DPO. Should be fine.
|
||||
$datarequest->set('dpo', $requestinguser);
|
||||
} 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 (!self::can_create_data_request_for_user($foruser)) {
|
||||
$forusercontext = \context_user::instance($foruser);
|
||||
throw new required_capability_exception($forusercontext,
|
||||
'tool/dataprivacy:makedatarequestsforchildren', 'nopermissions', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
// The cron is considered to be a guest user when it creates a data request.
|
||||
// NOTE: This should probably be changed. We should leave the default value for $requestinguser if
|
||||
// the request is not explicitly created by a specific user.
|
||||
$requestinguser = (isguestuser() && $creationmethod == data_request::DATAREQUEST_CREATION_AUTO) ?
|
||||
get_admin()->id : $USER->id;
|
||||
// The user making the request.
|
||||
$datarequest->set('requestedby', $requestinguser);
|
||||
// Set status.
|
||||
@@ -241,6 +254,8 @@ class api {
|
||||
$datarequest->set('type', $type);
|
||||
// Set request comments.
|
||||
$datarequest->set('comments', $comments);
|
||||
// Set the creation method.
|
||||
$datarequest->set('creationmethod', $creationmethod);
|
||||
|
||||
// Store subject access request.
|
||||
$datarequest->create();
|
||||
@@ -263,6 +278,7 @@ class api {
|
||||
* @param int $userid The User ID.
|
||||
* @param int[] $statuses The status filters.
|
||||
* @param int[] $types The request type filters.
|
||||
* @param int[] $creationmethods The request creation method filters.
|
||||
* @param string $sort The order by clause.
|
||||
* @param int $offset Amount of records to skip.
|
||||
* @param int $limit Amount of records to fetch.
|
||||
@@ -270,7 +286,8 @@ class api {
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public static function get_data_requests($userid = 0, $statuses = [], $types = [], $sort = '', $offset = 0, $limit = 0) {
|
||||
public static function get_data_requests($userid = 0, $statuses = [], $types = [], $creationmethods = [],
|
||||
$sort = '', $offset = 0, $limit = 0) {
|
||||
global $DB, $USER;
|
||||
$results = [];
|
||||
$sqlparams = [];
|
||||
@@ -294,6 +311,13 @@ class api {
|
||||
$sqlparams = array_merge($sqlparams, $typeparams);
|
||||
}
|
||||
|
||||
// Set request creation method filter.
|
||||
if (!empty($creationmethods)) {
|
||||
list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
|
||||
$sqlconditions[] = "creationmethod $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)";
|
||||
@@ -336,7 +360,7 @@ class api {
|
||||
|
||||
if (!empty($expiredrequests)) {
|
||||
data_request::expire($expiredrequests);
|
||||
$results = self::get_data_requests($userid, $statuses, $types, $sort, $offset, $limit);
|
||||
$results = self::get_data_requests($userid, $statuses, $types, $creationmethods, $sort, $offset, $limit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,11 +373,12 @@ class api {
|
||||
* @param int $userid The User ID.
|
||||
* @param int[] $statuses The status filters.
|
||||
* @param int[] $types The request type filters.
|
||||
* @param int[] $creationmethods The request creation method filters.
|
||||
* @return int
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public static function get_data_requests_count($userid = 0, $statuses = [], $types = []) {
|
||||
public static function get_data_requests_count($userid = 0, $statuses = [], $types = [], $creationmethods = []) {
|
||||
global $DB, $USER;
|
||||
$count = 0;
|
||||
$sqlparams = [];
|
||||
@@ -367,6 +392,11 @@ class api {
|
||||
$sqlconditions[] = "type $typeinsql";
|
||||
$sqlparams = array_merge($sqlparams, $typeparams);
|
||||
}
|
||||
if (!empty($creationmethods)) {
|
||||
list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
|
||||
$sqlconditions[] = "creationmethod $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)";
|
||||
@@ -425,8 +455,8 @@ class api {
|
||||
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;
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
|
||||
$select = "type = :type AND userid = :userid AND status {$insql}";
|
||||
$params = array_merge([
|
||||
'type' => $type,
|
||||
'userid' => $userid
|
||||
@@ -435,6 +465,48 @@ class api {
|
||||
return data_request::record_exists_select($select, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find whether any ongoing requests exist for a set of users.
|
||||
*
|
||||
* @param array $userids
|
||||
* @return array
|
||||
*/
|
||||
public static function find_ongoing_request_types_for_users(array $userids) : array {
|
||||
global $DB;
|
||||
|
||||
if (empty($userids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if the user already has an incomplete data request of the same type.
|
||||
$nonpendingstatuses = [
|
||||
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($statusinsql, $statusparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
|
||||
list($userinsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'us');
|
||||
|
||||
$select = "userid {$userinsql} AND status {$statusinsql}";
|
||||
$params = array_merge($statusparams, $userparams);
|
||||
|
||||
$requests = $DB->get_records_select(data_request::TABLE, $select, $params, 'userid', 'id, userid, type');
|
||||
|
||||
$returnval = [];
|
||||
foreach ($userids as $userid) {
|
||||
$returnval[$userid] = (object) [];
|
||||
}
|
||||
|
||||
foreach ($requests as $request) {
|
||||
$returnval[$request->userid]->{$request->type} = true;
|
||||
}
|
||||
|
||||
return $returnval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a request is active or not based on its status.
|
||||
*
|
||||
@@ -619,6 +691,8 @@ class api {
|
||||
'requestedby' => $requestedby->fullname,
|
||||
'requesttype' => $typetext,
|
||||
'requestdate' => userdate($requestdata->timecreated),
|
||||
'requestorigin' => $SITE->fullname,
|
||||
'requestoriginurl' => new moodle_url('/'),
|
||||
'requestcomments' => $requestdata->messagehtml,
|
||||
'datarequestsurl' => $datarequestsurl
|
||||
];
|
||||
@@ -644,16 +718,31 @@ class api {
|
||||
/**
|
||||
* Checks whether a non-DPO user can make a data request for another user.
|
||||
*
|
||||
* @param int $user The user ID of the target user.
|
||||
* @param int $requester The user ID of the user making the request.
|
||||
* @return bool
|
||||
* @throws coding_exception
|
||||
* @param int $user The user ID of the target user.
|
||||
* @param int $requester The user ID of the user making the request.
|
||||
* @return bool
|
||||
*/
|
||||
public static function can_create_data_request_for_user($user, $requester = null) {
|
||||
$usercontext = \context_user::instance($user);
|
||||
|
||||
return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
|
||||
}
|
||||
|
||||
/**
|
||||
* Require that the current user can make a data request for the specified other user.
|
||||
*
|
||||
* @param int $user The user ID of the target user.
|
||||
* @param int $requester The user ID of the user making the request.
|
||||
* @return bool
|
||||
*/
|
||||
public static function require_can_create_data_request_for_user($user, $requester = null) {
|
||||
$usercontext = \context_user::instance($user);
|
||||
|
||||
require_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user can download a data request.
|
||||
*
|
||||
@@ -709,8 +798,6 @@ class api {
|
||||
* @return \tool_dataprivacy\purpose.
|
||||
*/
|
||||
public static function create_purpose(stdClass $record) {
|
||||
self::check_can_manage_data_registry();
|
||||
|
||||
$purpose = new purpose(0, $record);
|
||||
$purpose->create();
|
||||
|
||||
@@ -724,8 +811,6 @@ class api {
|
||||
* @return \tool_dataprivacy\purpose.
|
||||
*/
|
||||
public static function update_purpose(stdClass $record) {
|
||||
self::check_can_manage_data_registry();
|
||||
|
||||
if (!isset($record->sensitivedatareasons)) {
|
||||
$record->sensitivedatareasons = '';
|
||||
}
|
||||
@@ -745,8 +830,6 @@ class api {
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete_purpose($id) {
|
||||
self::check_can_manage_data_registry();
|
||||
|
||||
$purpose = new purpose($id);
|
||||
if ($purpose->is_used()) {
|
||||
throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.');
|
||||
@@ -760,8 +843,6 @@ class api {
|
||||
* @return \tool_dataprivacy\purpose[]
|
||||
*/
|
||||
public static function get_purposes() {
|
||||
self::check_can_manage_data_registry();
|
||||
|
||||
return purpose::get_records([], 'name', 'ASC');
|
||||
}
|
||||
|
||||
@@ -772,8 +853,6 @@ class api {
|
||||
* @return \tool_dataprivacy\category.
|
||||
*/
|
||||
public static function create_category(stdClass $record) {
|
||||
self::check_can_manage_data_registry();
|
||||
|
||||
$category = new category(0, $record);
|
||||
$category->create();
|
||||
|
||||
@@ -787,8 +866,6 @@ class api {
|
||||
* @return \tool_dataprivacy\category.
|
||||
*/
|
||||
public static function update_category(stdClass $record) {
|
||||
self::check_can_manage_data_registry();
|
||||
|
||||
$category = new category($record->id);
|
||||
$category->from_record($record);
|
||||
|
||||
@@ -804,8 +881,6 @@ class api {
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete_category($id) {
|
||||
self::check_can_manage_data_registry();
|
||||
|
||||
$category = new category($id);
|
||||
if ($category->is_used()) {
|
||||
throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.');
|
||||
@@ -819,8 +894,6 @@ class api {
|
||||
* @return \tool_dataprivacy\category[]
|
||||
*/
|
||||
public static function get_categories() {
|
||||
self::check_can_manage_data_registry();
|
||||
|
||||
return category::get_records([], 'name', 'ASC');
|
||||
}
|
||||
|
||||
@@ -831,8 +904,6 @@ class api {
|
||||
* @return \tool_dataprivacy\context_instance
|
||||
*/
|
||||
public static function set_context_instance($record) {
|
||||
self::check_can_manage_data_registry($record->contextid);
|
||||
|
||||
if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) {
|
||||
// Update.
|
||||
$instance->from_record($record);
|
||||
@@ -859,7 +930,6 @@ class api {
|
||||
* @return null
|
||||
*/
|
||||
public static function unset_context_instance(context_instance $instance) {
|
||||
self::check_can_manage_data_registry($instance->get('contextid'));
|
||||
$instance->delete();
|
||||
}
|
||||
|
||||
@@ -873,9 +943,6 @@ class api {
|
||||
public static function set_contextlevel($record) {
|
||||
global $DB;
|
||||
|
||||
// Only manager at system level can set this.
|
||||
self::check_can_manage_data_registry();
|
||||
|
||||
if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) {
|
||||
throw new \coding_exception('Only context system and context user can set a contextlevel ' .
|
||||
'purpose and retention');
|
||||
@@ -906,8 +973,7 @@ class api {
|
||||
* @param int $forcedvalue Use this categoryid value as if this was this context instance category.
|
||||
* @return category|false
|
||||
*/
|
||||
public static function get_effective_context_category(\context $context, $forcedvalue=false) {
|
||||
self::check_can_manage_data_registry($context->id);
|
||||
public static function get_effective_context_category(\context $context, $forcedvalue = false) {
|
||||
if (!data_registry::defaults_set()) {
|
||||
return false;
|
||||
}
|
||||
@@ -922,8 +988,7 @@ class api {
|
||||
* @param int $forcedvalue Use this purposeid value as if this was this context instance purpose.
|
||||
* @return purpose|false
|
||||
*/
|
||||
public static function get_effective_context_purpose(\context $context, $forcedvalue=false) {
|
||||
self::check_can_manage_data_registry($context->id);
|
||||
public static function get_effective_context_purpose(\context $context, $forcedvalue = false) {
|
||||
if (!data_registry::defaults_set()) {
|
||||
return false;
|
||||
}
|
||||
@@ -935,16 +1000,14 @@ class api {
|
||||
* Returns the effective category given a context level.
|
||||
*
|
||||
* @param int $contextlevel
|
||||
* @param int $forcedvalue Use this categoryid value as if this was this context level category.
|
||||
* @return category|false
|
||||
*/
|
||||
public static function get_effective_contextlevel_category($contextlevel, $forcedvalue=false) {
|
||||
self::check_can_manage_data_registry(\context_system::instance()->id);
|
||||
public static function get_effective_contextlevel_category($contextlevel) {
|
||||
if (!data_registry::defaults_set()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return data_registry::get_effective_contextlevel_value($contextlevel, 'category', $forcedvalue);
|
||||
return data_registry::get_effective_contextlevel_value($contextlevel, 'category');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -955,7 +1018,6 @@ class api {
|
||||
* @return purpose|false
|
||||
*/
|
||||
public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) {
|
||||
self::check_can_manage_data_registry(\context_system::instance()->id);
|
||||
if (!data_registry::defaults_set()) {
|
||||
return false;
|
||||
}
|
||||
@@ -970,8 +1032,6 @@ class api {
|
||||
* @return \tool_dataprivacy\expired_context
|
||||
*/
|
||||
public static function create_expired_context($contextid) {
|
||||
self::check_can_manage_data_registry();
|
||||
|
||||
$record = (object)[
|
||||
'contextid' => $contextid,
|
||||
'status' => expired_context::STATUS_EXPIRED,
|
||||
@@ -989,8 +1049,6 @@ class api {
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public static function delete_expired_context($id) {
|
||||
self::check_can_manage_data_registry();
|
||||
|
||||
$expiredcontext = new expired_context($id);
|
||||
return $expiredcontext->delete();
|
||||
}
|
||||
@@ -1003,8 +1061,6 @@ class api {
|
||||
* @return null
|
||||
*/
|
||||
public static function set_expired_context_status(expired_context $expiredctx, $status) {
|
||||
self::check_can_manage_data_registry();
|
||||
|
||||
$expiredctx->set('status', $status);
|
||||
$expiredctx->save();
|
||||
}
|
||||
@@ -1018,6 +1074,8 @@ class api {
|
||||
*/
|
||||
public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
|
||||
$request = new data_request($requestid);
|
||||
$user = \core_user::get_user($request->get('userid'));
|
||||
$isconfigured = data_registry::defaults_set();
|
||||
foreach ($clcollection as $contextlist) {
|
||||
// Convert the \core_privacy\local\request\contextlist into a contextlist persistent and store it.
|
||||
$clp = \tool_dataprivacy\contextlist::from_contextlist($contextlist);
|
||||
@@ -1026,12 +1084,20 @@ class api {
|
||||
|
||||
// Store the associated contexts in the contextlist.
|
||||
foreach ($contextlist->get_contextids() as $contextid) {
|
||||
if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
|
||||
if ($isconfigured && self::DATAREQUEST_TYPE_DELETE == $request->get('type')) {
|
||||
// Data can only be deleted from it if the context is either expired, or unprotected.
|
||||
// Note: We can only check whether a context is expired or unprotected if the site is configured and
|
||||
// defaults are set appropriately. If they are not, we treat all contexts as though they are
|
||||
// unprotected.
|
||||
$context = \context::instance_by_id($contextid);
|
||||
if (($purpose = static::get_effective_context_purpose($context)) && !empty($purpose->get('protected'))) {
|
||||
$purpose = static::get_effective_context_purpose($context);
|
||||
|
||||
// Data can only be deleted from it if the context is either expired, or unprotected.
|
||||
if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $user)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$context = new contextlist_context();
|
||||
$context->set('contextid', $contextid)
|
||||
->set('contextlistid', $contextlistid)
|
||||
@@ -1102,10 +1168,11 @@ class api {
|
||||
* @return contextlist_collection the collection of approved_contextlist objects.
|
||||
*/
|
||||
public static function get_approved_contextlist_collection_for_request(data_request $request) : contextlist_collection {
|
||||
global $DB;
|
||||
$foruser = core_user::get_user($request->get('userid'));
|
||||
$isconfigured = data_registry::defaults_set();
|
||||
|
||||
// Fetch all approved contextlists and create the core_privacy\local\request\contextlist objects here.
|
||||
global $DB;
|
||||
$sql = "SELECT cl.component, ctx.contextid
|
||||
FROM {" . request_contextlist::TABLE . "} rcl
|
||||
JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
|
||||
@@ -1129,6 +1196,18 @@ class api {
|
||||
$contexts = [];
|
||||
}
|
||||
|
||||
if ($isconfigured && $request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
|
||||
$context = \context::instance_by_id($record->contextid);
|
||||
$purpose = static::get_effective_context_purpose($context);
|
||||
// Data can only be deleted from it if the context is either expired, or unprotected.
|
||||
// Note: We can only check whether a context is expired or unprotected if the site is configured and
|
||||
// defaults are set appropriately. If they are not, we treat all contexts as though they are
|
||||
// unprotected.
|
||||
if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$contexts[] = $record->contextid;
|
||||
$lastcomponent = $record->component;
|
||||
}
|
||||
@@ -1141,4 +1220,110 @@ class api {
|
||||
|
||||
return $approvedcollection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the default category and purpose for a given context level (and optionally, a plugin).
|
||||
*
|
||||
* @param int $contextlevel The context level.
|
||||
* @param int $categoryid The ID matching the category.
|
||||
* @param int $purposeid The ID matching the purpose record.
|
||||
* @param int $activity The name of the activity that we're making a defaults configuration for.
|
||||
* @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
|
||||
* @return boolean True if set/unset config succeeds. Otherwise, it throws an exception.
|
||||
*/
|
||||
public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) {
|
||||
global $DB;
|
||||
|
||||
// Get the class name associated with this context level.
|
||||
$classname = context_helper::get_class_for_level($contextlevel);
|
||||
list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity);
|
||||
|
||||
// Check the default category to be set.
|
||||
if ($categoryid == context_instance::INHERIT) {
|
||||
unset_config($categoryvar, 'tool_dataprivacy');
|
||||
|
||||
} else {
|
||||
// Make sure the given category ID exists first.
|
||||
$categorypersistent = new category($categoryid);
|
||||
$categorypersistent->read();
|
||||
|
||||
// Then set the new default value.
|
||||
set_config($categoryvar, $categoryid, 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
// Check the default purpose to be set.
|
||||
if ($purposeid == context_instance::INHERIT) {
|
||||
// If the defaults is set to inherit, just unset the config value.
|
||||
unset_config($purposevar, 'tool_dataprivacy');
|
||||
|
||||
} else {
|
||||
// Make sure the given purpose ID exists first.
|
||||
$purposepersistent = new purpose($purposeid);
|
||||
$purposepersistent->read();
|
||||
|
||||
// Then set the new default value.
|
||||
set_config($purposevar, $purposeid, 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
// Unset instances that have been assigned with custom purpose and category, if override was specified.
|
||||
if ($override) {
|
||||
// We'd like to find context IDs that we want to unset.
|
||||
$statements = ["SELECT c.id as contextid FROM {context} c"];
|
||||
// Based on this context level.
|
||||
$params = ['contextlevel' => $contextlevel];
|
||||
|
||||
if ($contextlevel == CONTEXT_MODULE) {
|
||||
// If we're deleting module context instances, we need to make sure the instance ID is in the course modules table.
|
||||
$statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid";
|
||||
// And that the module is listed on the modules table.
|
||||
$statements[] = "JOIN {modules} m ON m.id = cm.module";
|
||||
|
||||
if ($activity) {
|
||||
// If we're overriding for an activity module, make sure that the context instance matches that activity.
|
||||
$statements[] = "AND m.name = :modname";
|
||||
$params['modname'] = $activity;
|
||||
}
|
||||
}
|
||||
// Make sure this context instance exists in the tool_dataprivacy_ctxinstance table.
|
||||
$statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id";
|
||||
// And that the context level of this instance matches the given context level.
|
||||
$statements[] = "WHERE c.contextlevel = :contextlevel";
|
||||
|
||||
// Build our SQL query by gluing the statements.
|
||||
$sql = implode("\n", $statements);
|
||||
|
||||
// Get the context records matching our query.
|
||||
$contextids = $DB->get_fieldset_sql($sql, $params);
|
||||
|
||||
// Delete the matching context instances.
|
||||
foreach ($contextids as $contextid) {
|
||||
if ($instance = context_instance::get_record_by_contextid($contextid, false)) {
|
||||
self::unset_context_instance($instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the supplied date interval as a retention period.
|
||||
*
|
||||
* @param \DateInterval $interval
|
||||
* @return string
|
||||
*/
|
||||
public static function format_retention_period(\DateInterval $interval) : string {
|
||||
// It is one or another.
|
||||
if ($interval->y) {
|
||||
$formattedtime = get_string('numyears', 'moodle', $interval->format('%y'));
|
||||
} else if ($interval->m) {
|
||||
$formattedtime = get_string('nummonths', 'moodle', $interval->format('%m'));
|
||||
} else if ($interval->d) {
|
||||
$formattedtime = get_string('numdays', 'moodle', $interval->format('%d'));
|
||||
} else {
|
||||
$formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
return $formattedtime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,7 @@
|
||||
namespace tool_dataprivacy;
|
||||
|
||||
use coding_exception;
|
||||
use tool_dataprivacy\purpose;
|
||||
use tool_dataprivacy\category;
|
||||
use tool_dataprivacy\contextlevel;
|
||||
use tool_dataprivacy\context_instance;
|
||||
use core\persistent;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -44,28 +41,25 @@ require_once($CFG->libdir . '/coursecatlib.php');
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class data_registry {
|
||||
|
||||
/**
|
||||
* @var array Inheritance between context levels.
|
||||
*/
|
||||
private static $contextlevelinheritance = [
|
||||
CONTEXT_USER => [CONTEXT_SYSTEM],
|
||||
CONTEXT_COURSECAT => [CONTEXT_SYSTEM],
|
||||
CONTEXT_COURSE => [CONTEXT_COURSECAT, CONTEXT_SYSTEM],
|
||||
CONTEXT_MODULE => [CONTEXT_COURSE, CONTEXT_COURSECAT, CONTEXT_SYSTEM],
|
||||
CONTEXT_BLOCK => [CONTEXT_COURSE, CONTEXT_COURSECAT, CONTEXT_SYSTEM],
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns purpose and category var names from a context class name
|
||||
*
|
||||
* @param string $classname
|
||||
* @param string $classname The context level's class.
|
||||
* @param string $pluginname The name of the plugin associated with the context level.
|
||||
* @return string[]
|
||||
*/
|
||||
public static function var_names_from_context($classname) {
|
||||
public static function var_names_from_context($classname, $pluginname = '') {
|
||||
$pluginname = trim($pluginname);
|
||||
if (!empty($pluginname)) {
|
||||
$categoryvar = $classname . '_' . $pluginname . '_category';
|
||||
$purposevar = $classname . '_' . $pluginname . '_purpose';
|
||||
} else {
|
||||
$categoryvar = $classname . '_category';
|
||||
$purposevar = $classname . '_purpose';
|
||||
}
|
||||
return [
|
||||
$classname . '_purpose',
|
||||
$classname . '_category',
|
||||
$purposevar,
|
||||
$categoryvar
|
||||
];
|
||||
}
|
||||
|
||||
@@ -74,22 +68,35 @@ class data_registry {
|
||||
*
|
||||
* The caller code is responsible of checking that $contextlevel is an integer.
|
||||
*
|
||||
* @param int $contextlevel
|
||||
* @return int|false[]
|
||||
* @param int $contextlevel The context level.
|
||||
* @param string $pluginname The name of the plugin associated with the context level.
|
||||
* @return int[]|false[]
|
||||
*/
|
||||
public static function get_defaults($contextlevel) {
|
||||
|
||||
public static function get_defaults($contextlevel, $pluginname = '') {
|
||||
$classname = \context_helper::get_class_for_level($contextlevel);
|
||||
list($purposevar, $categoryvar) = self::var_names_from_context($classname);
|
||||
list($purposevar, $categoryvar) = self::var_names_from_context($classname, $pluginname);
|
||||
|
||||
$purposeid = get_config('tool_dataprivacy', $purposevar);
|
||||
$categoryid = get_config('tool_dataprivacy', $categoryvar);
|
||||
|
||||
if (!empty($pluginname)) {
|
||||
list($purposevar, $categoryvar) = self::var_names_from_context($classname);
|
||||
// If the plugin-level doesn't have a default purpose set, try the context level.
|
||||
if ($purposeid == false) {
|
||||
$purposeid = get_config('tool_dataprivacy', $purposevar);
|
||||
}
|
||||
|
||||
// If the plugin-level doesn't have a default category set, try the context level.
|
||||
if ($categoryid == false) {
|
||||
$categoryid = get_config('tool_dataprivacy', $categoryvar);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($purposeid)) {
|
||||
$purposeid = false;
|
||||
$purposeid = context_instance::NOTSET;
|
||||
}
|
||||
if (empty($categoryid)) {
|
||||
$categoryid = false;
|
||||
$categoryid = context_instance::NOTSET;
|
||||
}
|
||||
|
||||
return [$purposeid, $categoryid];
|
||||
@@ -171,55 +178,93 @@ class data_registry {
|
||||
* @param int|false $forcedvalue Use this value as if this was this context instance value.
|
||||
* @return persistent|false It return a 'purpose' instance or a 'category' instance, depending on $element
|
||||
*/
|
||||
public static function get_effective_context_value(\context $context, $element, $forcedvalue=false) {
|
||||
public static function get_effective_context_value(\context $context, $element, $forcedvalue = false) {
|
||||
global $DB;
|
||||
|
||||
if ($element !== 'purpose' && $element !== 'category') {
|
||||
throw new coding_exception('Only \'purpose\' and \'category\' are supported.');
|
||||
}
|
||||
$fieldname = $element . 'id';
|
||||
|
||||
if ($forcedvalue === false) {
|
||||
$instance = context_instance::get_record_by_contextid($context->id, false);
|
||||
|
||||
if (!$instance) {
|
||||
// If the instance does not have a value defaults to not set, so we grab the context level default as its value.
|
||||
$instancevalue = context_instance::NOTSET;
|
||||
} else {
|
||||
$instancevalue = $instance->get($fieldname);
|
||||
}
|
||||
if (!empty($forcedvalue) && ($forcedvalue == context_instance::INHERIT)) {
|
||||
// Do not include the current context when calculating the value.
|
||||
// This has the effect that an inheritted value is calculated.
|
||||
$parentcontextids = $context->get_parent_context_ids(false);
|
||||
} else if (!empty($forcedvalue) && ($forcedvalue != context_instance::NOTSET)) {
|
||||
return self::get_element_instance($element, $forcedvalue);
|
||||
} else {
|
||||
$instancevalue = $forcedvalue;
|
||||
// Fetch all parent contexts, including self.
|
||||
$parentcontextids = $context->get_parent_context_ids(true);
|
||||
}
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($parentcontextids, SQL_PARAMS_NAMED);
|
||||
$inparams['contextmodule'] = CONTEXT_MODULE;
|
||||
|
||||
// Not set.
|
||||
if ($instancevalue == context_instance::NOTSET) {
|
||||
if ('purpose' === $element) {
|
||||
$elementjoin = 'LEFT JOIN {tool_dataprivacy_purpose} ele ON ctxins.purposeid = ele.id';
|
||||
$elementfields = purpose::get_sql_fields('ele', 'ele');
|
||||
} else {
|
||||
$elementjoin = 'LEFT JOIN {tool_dataprivacy_category} ele ON ctxins.categoryid = ele.id';
|
||||
$elementfields = category::get_sql_fields('ele', 'ele');
|
||||
}
|
||||
$contextfields = \context_helper::get_preload_record_columns_sql('ctx');
|
||||
$fields = implode(', ', ['ctx.id', 'm.name AS modname', $contextfields, $elementfields]);
|
||||
|
||||
// The effective value varies depending on the context level.
|
||||
if ($context->contextlevel == CONTEXT_USER) {
|
||||
// Use the context level value as we don't allow people to set specific instances values.
|
||||
return self::get_effective_contextlevel_value($context->contextlevel, $element);
|
||||
} else {
|
||||
// Use the default context level value.
|
||||
list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category(
|
||||
$context->contextlevel
|
||||
);
|
||||
return self::get_element_instance($element, $$fieldname);
|
||||
$sql = "SELECT $fields
|
||||
FROM {context} ctx
|
||||
LEFT JOIN {tool_dataprivacy_ctxinstance} ctxins ON ctx.id = ctxins.contextid
|
||||
LEFT JOIN {course_modules} cm ON ctx.contextlevel = :contextmodule AND ctx.instanceid = cm.id
|
||||
LEFT JOIN {modules} m ON m.id = cm.module
|
||||
{$elementjoin}
|
||||
WHERE ctx.id {$insql}
|
||||
ORDER BY ctx.path DESC";
|
||||
$contextinstances = $DB->get_records_sql($sql, $inparams);
|
||||
|
||||
// Check whether this context is a user context, or a child of a user context.
|
||||
// All children of a User context share the same context and cannot be set individually.
|
||||
foreach ($contextinstances as $record) {
|
||||
\context_helper::preload_from_record($record);
|
||||
$parent = \context::instance_by_id($record->id, false);
|
||||
|
||||
if ($parent->contextlevel == CONTEXT_USER) {
|
||||
// Use the context level value for the user.
|
||||
return self::get_effective_contextlevel_value(CONTEXT_USER, $element);
|
||||
}
|
||||
}
|
||||
|
||||
// Specific value for this context instance.
|
||||
if ($instancevalue != context_instance::INHERIT) {
|
||||
return self::get_element_instance($element, $instancevalue);
|
||||
foreach ($contextinstances as $record) {
|
||||
$parent = \context::instance_by_id($record->id, false);
|
||||
|
||||
$checkcontextlevel = false;
|
||||
if (empty($record->eleid)) {
|
||||
$checkcontextlevel = true;
|
||||
}
|
||||
|
||||
if (!empty($forcedvalue) && context_instance::NOTSET == $forcedvalue) {
|
||||
$checkcontextlevel = true;
|
||||
}
|
||||
|
||||
if ($checkcontextlevel) {
|
||||
// Check for a value at the contextlevel
|
||||
$forplugin = empty($record->modname) ? '' : $record->modname;
|
||||
list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category(
|
||||
$parent->contextlevel, false, false, $forplugin);
|
||||
|
||||
$instancevalue = $$fieldname;
|
||||
|
||||
if (context_instance::NOTSET != $instancevalue && context_instance::INHERIT != $instancevalue) {
|
||||
// There is an actual value. Return it.
|
||||
return self::get_element_instance($element, $instancevalue);
|
||||
}
|
||||
} else {
|
||||
$elementclass = "\\tool_dataprivacy\\{$element}";
|
||||
$instance = new $elementclass(null, $elementclass::extract_record($record, 'ele'));
|
||||
$instance->validate();
|
||||
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
|
||||
// This context is using inherited so let's return the parent effective value.
|
||||
$parentcontext = $context->get_parent_context();
|
||||
if (!$parentcontext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The forced value should not be transmitted to parent contexts.
|
||||
return self::get_effective_context_value($parentcontext, $element);
|
||||
throw new coding_exception('Something went wrong, system defaults should be set and we should already have a value.');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,11 +276,9 @@ class data_registry {
|
||||
*
|
||||
* @param int $contextlevel
|
||||
* @param string $element 'category' or 'purpose'
|
||||
* @param int $forcedvalue Use this value as if this was this context level purpose.
|
||||
* @return \tool_dataprivacy\purpose|false
|
||||
*/
|
||||
public static function get_effective_contextlevel_value($contextlevel, $element, $forcedvalue = false) {
|
||||
|
||||
public static function get_effective_contextlevel_value($contextlevel, $element) {
|
||||
if ($element !== 'purpose' && $element !== 'category') {
|
||||
throw new coding_exception('Only \'purpose\' and \'category\' are supported.');
|
||||
}
|
||||
@@ -246,53 +289,30 @@ class data_registry {
|
||||
'have a purpose or a category.');
|
||||
}
|
||||
|
||||
if ($forcedvalue === false) {
|
||||
$instance = contextlevel::get_record_by_contextlevel($contextlevel, false);
|
||||
if (!$instance) {
|
||||
// If the context level does not have a value defaults to not set, so we grab the context level default as
|
||||
// its value.
|
||||
$instancevalue = context_instance::NOTSET;
|
||||
} else {
|
||||
$instancevalue = $instance->get($fieldname);
|
||||
}
|
||||
} else {
|
||||
$instancevalue = $forcedvalue;
|
||||
}
|
||||
list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category($contextlevel);
|
||||
|
||||
// Not set -> Use the default context level value.
|
||||
if ($instancevalue == context_instance::NOTSET) {
|
||||
list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category($contextlevel);
|
||||
// Note: The $$fieldname points to either $purposeid, or $categoryid.
|
||||
if (context_instance::NOTSET != $$fieldname && context_instance::INHERIT != $$fieldname) {
|
||||
// There is a specific value set.
|
||||
return self::get_element_instance($element, $$fieldname);
|
||||
}
|
||||
|
||||
// Specific value for this context instance.
|
||||
if ($instancevalue != context_instance::INHERIT) {
|
||||
return self::get_element_instance($element, $instancevalue);
|
||||
}
|
||||
|
||||
if ($contextlevel == CONTEXT_SYSTEM) {
|
||||
throw new coding_exception('Something went wrong, system defaults should be set and we should already have a value.');
|
||||
}
|
||||
|
||||
// If we reach this point is that we are inheriting so get the parent context level and repeat.
|
||||
$parentcontextlevel = reset(self::$contextlevelinheritance[$contextlevel]);
|
||||
|
||||
// Forced value are intentionally not passed as the force value should only affect the immediate context level.
|
||||
return self::get_effective_contextlevel_value($parentcontextlevel, $element);
|
||||
throw new coding_exception('Something went wrong, system defaults should be set and we should already have a value.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the effective default purpose and category for a context level.
|
||||
*
|
||||
* @param int $contextlevel
|
||||
* @param int $forcedpurposevalue Use this value as if this was this context level purpose.
|
||||
* @param int $forcedcategoryvalue Use this value as if this was this context level category.
|
||||
* @param int|bool $forcedpurposevalue Use this value as if this was this context level purpose.
|
||||
* @param int|bool $forcedcategoryvalue Use this value as if this was this context level category.
|
||||
* @param string $component The name of the component to check.
|
||||
* @return int[]
|
||||
*/
|
||||
public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false,
|
||||
$forcedcategoryvalue = false) {
|
||||
|
||||
list($purposeid, $categoryid) = self::get_defaults($contextlevel);
|
||||
$forcedcategoryvalue = false, $component = '') {
|
||||
// Get the defaults for this context level.
|
||||
list($purposeid, $categoryid) = self::get_defaults($contextlevel, $component);
|
||||
|
||||
// Honour forced values.
|
||||
if ($forcedpurposevalue) {
|
||||
@@ -302,37 +322,19 @@ class data_registry {
|
||||
$categoryid = $forcedcategoryvalue;
|
||||
}
|
||||
|
||||
// Not set == INHERIT for defaults.
|
||||
if ($purposeid == context_instance::INHERIT || $purposeid == context_instance::NOTSET) {
|
||||
$purposeid = false;
|
||||
}
|
||||
if ($categoryid == context_instance::INHERIT || $categoryid == context_instance::NOTSET) {
|
||||
$categoryid = false;
|
||||
}
|
||||
if ($contextlevel == CONTEXT_USER) {
|
||||
// Only user context levels inherit from a parent context level.
|
||||
list($parentpurposeid, $parentcategoryid) = self::get_defaults(CONTEXT_SYSTEM);
|
||||
|
||||
if ($contextlevel != CONTEXT_SYSTEM && ($purposeid === false || $categoryid === false)) {
|
||||
foreach (self::$contextlevelinheritance[$contextlevel] as $parent) {
|
||||
if (context_instance::INHERIT == $purposeid || context_instance::NOTSET == $purposeid) {
|
||||
$purposeid = (int)$parentpurposeid;
|
||||
}
|
||||
|
||||
list($parentpurposeid, $parentcategoryid) = self::get_defaults($parent);
|
||||
// Not set == INHERIT for defaults.
|
||||
if ($parentpurposeid == context_instance::INHERIT || $parentpurposeid == context_instance::NOTSET) {
|
||||
$parentpurposeid = false;
|
||||
}
|
||||
if ($parentcategoryid == context_instance::INHERIT || $parentcategoryid == context_instance::NOTSET) {
|
||||
$parentcategoryid = false;
|
||||
}
|
||||
|
||||
if ($purposeid === false && $parentpurposeid) {
|
||||
$purposeid = $parentpurposeid;
|
||||
}
|
||||
|
||||
if ($categoryid === false && $parentcategoryid) {
|
||||
$categoryid = $parentcategoryid;
|
||||
}
|
||||
if (context_instance::INHERIT == $categoryid || context_instance::NOTSET == $categoryid) {
|
||||
$categoryid = $parentcategoryid;
|
||||
}
|
||||
}
|
||||
|
||||
// They may still be false, but we return anyway.
|
||||
return [$purposeid, $categoryid];
|
||||
}
|
||||
|
||||
@@ -345,7 +347,6 @@ class data_registry {
|
||||
* @return \core\persistent
|
||||
*/
|
||||
private static function get_element_instance($element, $id) {
|
||||
|
||||
if ($element !== 'purpose' && $element !== 'category') {
|
||||
throw new coding_exception('No other elements than purpose and category are allowed');
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_dataprivacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core\persistent;
|
||||
@@ -37,6 +39,12 @@ class data_request extends persistent {
|
||||
/** The table name this persistent object maps to. */
|
||||
const TABLE = 'tool_dataprivacy_request';
|
||||
|
||||
/** Data request created manually. */
|
||||
const DATAREQUEST_CREATION_MANUAL = 0;
|
||||
|
||||
/** Data request created automatically. */
|
||||
const DATAREQUEST_CREATION_AUTO = 1;
|
||||
|
||||
/**
|
||||
* Return the definition of the properties of this model.
|
||||
*
|
||||
@@ -111,6 +119,14 @@ class data_request extends persistent {
|
||||
'type' => PARAM_INT,
|
||||
'default' => FORMAT_PLAIN
|
||||
],
|
||||
'creationmethod' => [
|
||||
'default' => self::DATAREQUEST_CREATION_MANUAL,
|
||||
'choices' => [
|
||||
self::DATAREQUEST_CREATION_MANUAL,
|
||||
self::DATAREQUEST_CREATION_AUTO
|
||||
],
|
||||
'type' => PARAM_INT
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -144,8 +160,6 @@ class data_request extends persistent {
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Fetch completed data requests which are due to expire.
|
||||
*
|
||||
@@ -210,4 +224,68 @@ class data_request extends persistent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this request is in a state appropriate for reset/resubmission.
|
||||
*
|
||||
* Note: This does not check whether any other completed requests exist for this user.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_resettable() : bool {
|
||||
if (api::DATAREQUEST_TYPE_OTHERS == $this->get('type')) {
|
||||
// It is not possible to reset 'other' reqeusts.
|
||||
return false;
|
||||
}
|
||||
|
||||
$resettable = [
|
||||
api::DATAREQUEST_STATUS_AWAITING_APPROVAL => true,
|
||||
api::DATAREQUEST_STATUS_APPROVED => true,
|
||||
api::DATAREQUEST_STATUS_REJECTED => true,
|
||||
];
|
||||
|
||||
return isset($resettable[$this->get('status')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this request is 'active'.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_active() : bool {
|
||||
$active = [
|
||||
api::DATAREQUEST_STATUS_AWAITING_APPROVAL => true,
|
||||
api::DATAREQUEST_STATUS_APPROVED => true,
|
||||
];
|
||||
|
||||
return isset($active[$this->get('status')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject this request and resubmit it as a fresh request.
|
||||
*
|
||||
* Note: This does not check whether any other completed requests exist for this user.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function resubmit_request() : data_request {
|
||||
if ($this->is_active()) {
|
||||
$this->set('status', api::DATAREQUEST_STATUS_REJECTED)->save();
|
||||
}
|
||||
|
||||
if (!$this->is_resettable()) {
|
||||
throw new \moodle_exception('cannotreset', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
$currentdata = $this->to_record();
|
||||
unset($currentdata->id);
|
||||
|
||||
$clone = api::create_data_request($this->get('userid'), $this->get('type'));
|
||||
$clone->set('comments', $this->get('comments'));
|
||||
$clone->set('dpo', $this->get('dpo'));
|
||||
$clone->set('requestedby', $this->get('requestedby'));
|
||||
$clone->save();
|
||||
|
||||
return $clone;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Event observers supported by this module.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_dataprivacy\event;
|
||||
|
||||
use \tool_dataprivacy\api;
|
||||
use \tool_dataprivacy\data_request;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Event observers supported by this module.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class user_deleted_observer {
|
||||
|
||||
/**
|
||||
* Create user data deletion request when the user is deleted.
|
||||
*
|
||||
* @param \core\event\user_deleted $event
|
||||
*/
|
||||
public static function create_delete_data_request(\core\event\user_deleted $event) {
|
||||
// Automatic creation of deletion requests must be enabled.
|
||||
if (get_config('tool_dataprivacy', 'automaticdeletionrequests')) {
|
||||
$requesttypes = [api::DATAREQUEST_TYPE_DELETE];
|
||||
$requeststatuses = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_DELETED];
|
||||
|
||||
$hasongoingdeleterequests = api::has_ongoing_request($event->objectid, $requesttypes[0]);
|
||||
$hascompleteddeleterequest = (api::get_data_requests_count($event->objectid, $requeststatuses,
|
||||
$requesttypes) > 0) ? true : false;
|
||||
|
||||
if (!$hasongoingdeleterequests && !$hascompleteddeleterequest) {
|
||||
api::create_data_request($event->objectid, $requesttypes[0],
|
||||
get_string('datarequestcreatedupondelete', 'tool_dataprivacy'),
|
||||
data_request::DATAREQUEST_CREATION_AUTO);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,12 +60,27 @@ class expired_context extends \core\persistent {
|
||||
* @return array
|
||||
*/
|
||||
protected static function define_properties() {
|
||||
return array(
|
||||
'contextid' => array(
|
||||
return [
|
||||
'contextid' => [
|
||||
'type' => PARAM_INT,
|
||||
'description' => 'The context id.',
|
||||
),
|
||||
'status' => array(
|
||||
],
|
||||
'defaultexpired' => [
|
||||
'type' => PARAM_INT,
|
||||
'description' => 'Whether to default retention period for the purpose has been reached',
|
||||
'default' => 1,
|
||||
],
|
||||
'expiredroles' => [
|
||||
'type' => PARAM_TEXT,
|
||||
'description' => 'This list of roles to include during deletion',
|
||||
'default' => '',
|
||||
],
|
||||
'unexpiredroles' => [
|
||||
'type' => PARAM_TEXT,
|
||||
'description' => 'This list of roles to exclude during deletion',
|
||||
'default' => '',
|
||||
],
|
||||
'status' => [
|
||||
'choices' => [
|
||||
self::STATUS_EXPIRED,
|
||||
self::STATUS_APPROVED,
|
||||
@@ -73,8 +88,8 @@ class expired_context extends \core\persistent {
|
||||
],
|
||||
'type' => PARAM_INT,
|
||||
'description' => 'The deletion status of the context.',
|
||||
),
|
||||
);
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,4 +174,205 @@ class expired_context extends \core\persistent {
|
||||
|
||||
return $DB->count_records_sql($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of role IDs for either expiredroles, or unexpiredroles.
|
||||
*
|
||||
* @param string $field
|
||||
* @param int[] $roleids
|
||||
* @return expired_context
|
||||
*/
|
||||
protected function set_roleids_for(string $field, array $roleids) : expired_context {
|
||||
$roledata = json_encode($roleids);
|
||||
|
||||
$this->raw_set($field, $roledata);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of role IDs for either expiredroles, or unexpiredroles.
|
||||
*
|
||||
* @param string $field
|
||||
* @return int[]
|
||||
*/
|
||||
protected function get_roleids_for(string $field) {
|
||||
$value = $this->raw_get($field);
|
||||
if (empty($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return json_decode($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of unexpired role IDs.
|
||||
*
|
||||
* @param int[] $roleids
|
||||
* @return expired_context
|
||||
*/
|
||||
protected function set_unexpiredroles(array $roleids) : expired_context {
|
||||
$this->set_roleids_for('unexpiredroles', $roleids);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a set of role IDs to the list of expired role IDs.
|
||||
*
|
||||
* @param int[] $roleids
|
||||
* @return expired_context
|
||||
*/
|
||||
public function add_expiredroles(array $roleids) : expired_context {
|
||||
$existing = $this->get('expiredroles');
|
||||
$newvalue = array_merge($existing, $roleids);
|
||||
|
||||
$this->set('expiredroles', $newvalue);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a set of role IDs to the list of unexpired role IDs.
|
||||
*
|
||||
* @param int[] $roleids
|
||||
* @return unexpired_context
|
||||
*/
|
||||
public function add_unexpiredroles(array $roleids) : expired_context {
|
||||
$existing = $this->get('unexpiredroles');
|
||||
$newvalue = array_merge($existing, $roleids);
|
||||
|
||||
$this->set('unexpiredroles', $newvalue);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of expired role IDs.
|
||||
*
|
||||
* @param int[] $roleids
|
||||
* @return expired_context
|
||||
*/
|
||||
protected function set_expiredroles(array $roleids) : expired_context {
|
||||
$this->set_roleids_for('expiredroles', $roleids);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of expired role IDs.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
protected function get_expiredroles() {
|
||||
return $this->get_roleids_for('expiredroles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of unexpired role IDs.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
protected function get_unexpiredroles() {
|
||||
return $this->get_roleids_for('unexpiredroles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new expired_context based on the context, and expiry_info object.
|
||||
*
|
||||
* @param \context $context
|
||||
* @param expiry_info $info
|
||||
* @param boolean $save
|
||||
* @return expired_context
|
||||
*/
|
||||
public static function create_from_expiry_info(\context $context, expiry_info $info, bool $save = true) : expired_context {
|
||||
$record = (object) [
|
||||
'contextid' => $context->id,
|
||||
'status' => self::STATUS_EXPIRED,
|
||||
'defaultexpired' => (int) $info->is_default_expired(),
|
||||
];
|
||||
|
||||
$expiredcontext = new static(0, $record);
|
||||
$expiredcontext->set('expiredroles', $info->get_expired_roles());
|
||||
$expiredcontext->set('unexpiredroles', $info->get_unexpired_roles());
|
||||
|
||||
if ($save) {
|
||||
$expiredcontext->save();
|
||||
}
|
||||
|
||||
return $expiredcontext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the expired_context from an expiry_info object which relates to this context.
|
||||
*
|
||||
* @param expiry_info $info
|
||||
* @return $this
|
||||
*/
|
||||
public function update_from_expiry_info(expiry_info $info) : expired_context {
|
||||
$save = false;
|
||||
|
||||
// Compare the expiredroles.
|
||||
$thisexpired = $this->get('expiredroles');
|
||||
$infoexpired = $info->get_expired_roles();
|
||||
|
||||
sort($thisexpired);
|
||||
sort($infoexpired);
|
||||
if ($infoexpired != $thisexpired) {
|
||||
$this->set('expiredroles', $infoexpired);
|
||||
$save = true;
|
||||
}
|
||||
|
||||
// Compare the unexpiredroles.
|
||||
$thisunexpired = $this->get('unexpiredroles');
|
||||
$infounexpired = $info->get_unexpired_roles();
|
||||
|
||||
sort($thisunexpired);
|
||||
sort($infounexpired);
|
||||
if ($infounexpired != $thisunexpired) {
|
||||
$this->set('unexpiredroles', $infounexpired);
|
||||
$save = true;
|
||||
}
|
||||
|
||||
if (empty($this->get('defaultexpired')) == $info->is_default_expired()) {
|
||||
$this->set('defaultexpired', (int) $info->is_default_expired());
|
||||
$save = true;
|
||||
}
|
||||
|
||||
if ($save) {
|
||||
$this->set('status', self::STATUS_EXPIRED);
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this expired_context record is in a state ready for deletion to actually take place.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function can_process_deletion() : bool {
|
||||
return ($this->get('status') == self::STATUS_APPROVED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this expired_context record has already been cleaned.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_complete() : bool {
|
||||
return ($this->get('status') == self::STATUS_CLEANED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this context has 'fully' expired.
|
||||
* That is to say that the default retention period has been reached, and that there are no unexpired roles.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_fully_expired() : bool {
|
||||
return $this->get('defaultexpired') && empty($this->get('unexpiredroles'));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,135 +0,0 @@
|
||||
<?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 contexts manager for CONTEXT_COURSE, CONTEXT_MODULE and CONTEXT_BLOCK.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 David Monllao
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace tool_dataprivacy;
|
||||
|
||||
use tool_dataprivacy\purpose;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Expired contexts manager for CONTEXT_COURSE, CONTEXT_MODULE and CONTEXT_BLOCK.
|
||||
*
|
||||
* @copyright 2018 David Monllao
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class expired_course_related_contexts extends \tool_dataprivacy\expired_contexts_manager {
|
||||
|
||||
/**
|
||||
* Course-related context levels.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
protected function get_context_levels() {
|
||||
return [CONTEXT_MODULE, CONTEXT_BLOCK, CONTEXT_COURSE];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a recordset with user context instances that are possibly expired (to be confirmed by get_recordset_callback).
|
||||
*
|
||||
* @return \stdClass[]
|
||||
*/
|
||||
protected function get_expired_contexts() {
|
||||
global $DB;
|
||||
|
||||
// Including context info + course end date + purposeid (this last one only if defined).
|
||||
$fields = 'ctx.id AS id, ctxcourse.enddate AS courseenddate, dpctx.purposeid AS purposeid, ' .
|
||||
\context_helper::get_preload_record_columns_sql('ctx');
|
||||
|
||||
// We want all contexts at course-dependant levels.
|
||||
$parentpath = $DB->sql_concat('ctxcourse.path', "'/%'");
|
||||
|
||||
// This SQL query returns all course-dependant contexts (including the course context)
|
||||
// which course end date already passed.
|
||||
$sql = "SELECT $fields
|
||||
FROM {context} ctx
|
||||
JOIN (
|
||||
SELECT c.enddate, subctx.path
|
||||
FROM {context} subctx
|
||||
JOIN {course} c
|
||||
ON subctx.contextlevel = ? AND subctx.instanceid = c.id
|
||||
WHERE c.enddate < ? AND c.enddate > 0
|
||||
) ctxcourse
|
||||
ON ctx.path LIKE {$parentpath} OR ctx.path = ctxcourse.path
|
||||
LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx
|
||||
ON dpctx.contextid = ctx.id
|
||||
LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx
|
||||
ON ctx.id = expiredctx.contextid
|
||||
WHERE expiredctx.id IS NULL
|
||||
ORDER BY ctx.contextlevel DESC, ctx.path";
|
||||
$possiblyexpired = $DB->get_recordset_sql($sql, [CONTEXT_COURSE, time()]);
|
||||
|
||||
$expiredcontexts = [];
|
||||
$excludedcontextids = [];
|
||||
foreach ($possiblyexpired as $record) {
|
||||
|
||||
\context_helper::preload_from_record($record);
|
||||
|
||||
// No strict checking as the context may already be deleted (e.g. we just deleted a course,
|
||||
// module contexts below it will not exist).
|
||||
$context = \context::instance_by_id($record->id, false);
|
||||
if (!$context) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We pass the value we just got from SQL so get_effective_context_purpose don't need to query
|
||||
// the db again to retrieve it. If there is no tool_dataprovider_ctxinstance record
|
||||
// $record->purposeid will be null which is ok as it would force get_effective_context_purpose
|
||||
// to return the default purpose for the context context level (no db queries involved).
|
||||
$purposevalue = $record->purposeid !== null ? $record->purposeid : context_instance::NOTSET;
|
||||
|
||||
// It should be cheap as system purposes and context level purposes will be retrieved from a cache most of the time.
|
||||
$purpose = api::get_effective_context_purpose($context, $purposevalue);
|
||||
|
||||
$dt = new \DateTime();
|
||||
$dt->setTimestamp($record->courseenddate);
|
||||
$di = new \DateInterval($purpose->get('retentionperiod'));
|
||||
$dt->add($di);
|
||||
|
||||
if (time() < $dt->getTimestamp()) {
|
||||
// Exclude this context ID as it has not reached the retention period yet.
|
||||
$excludedcontextids[] = $context->id;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this context has children that have not yet expired.
|
||||
$hasunexpiredchildren = false;
|
||||
$children = $context->get_child_contexts();
|
||||
foreach ($children as $child) {
|
||||
if (in_array($child->id, $excludedcontextids)) {
|
||||
$hasunexpiredchildren = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($hasunexpiredchildren) {
|
||||
// Exclude this context ID as it has children that have not yet expired.
|
||||
$excludedcontextids[] = $context->id;
|
||||
continue;
|
||||
}
|
||||
|
||||
$expiredcontexts[$context->id] = $context;
|
||||
}
|
||||
|
||||
return $expiredcontexts;
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
<?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 contexts manager for CONTEXT_USER.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 David Monllao
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace tool_dataprivacy;
|
||||
|
||||
use core_privacy\manager;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Expired contexts manager for CONTEXT_USER.
|
||||
*
|
||||
* @copyright 2018 David Monllao
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class expired_user_contexts extends \tool_dataprivacy\expired_contexts_manager {
|
||||
|
||||
/**
|
||||
* Only user level.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
protected function get_context_levels() {
|
||||
return [CONTEXT_USER];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user context instances that are expired.
|
||||
*
|
||||
* @return \stdClass[]
|
||||
*/
|
||||
protected function get_expired_contexts() {
|
||||
global $DB;
|
||||
|
||||
// Including context info + last login timestamp.
|
||||
$fields = 'ctx.id AS id, ' . \context_helper::get_preload_record_columns_sql('ctx');
|
||||
|
||||
$purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER);
|
||||
|
||||
// Calculate what is considered expired according to the context level effective purpose (= now + retention period).
|
||||
$expiredtime = new \DateTime();
|
||||
$retention = new \DateInterval($purpose->get('retentionperiod'));
|
||||
$expiredtime->sub($retention);
|
||||
|
||||
$sql = "SELECT $fields FROM {context} ctx
|
||||
JOIN {user} u ON ctx.contextlevel = ? AND ctx.instanceid = u.id
|
||||
LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx ON ctx.id = expiredctx.contextid
|
||||
WHERE u.lastaccess <= ? AND u.lastaccess > 0 AND expiredctx.id IS NULL
|
||||
ORDER BY ctx.path, ctx.contextlevel ASC";
|
||||
$possiblyexpired = $DB->get_recordset_sql($sql, [CONTEXT_USER, $expiredtime->getTimestamp()]);
|
||||
|
||||
$expiredcontexts = [];
|
||||
foreach ($possiblyexpired as $record) {
|
||||
|
||||
\context_helper::preload_from_record($record);
|
||||
|
||||
// No strict checking as the context may already be deleted (e.g. we just deleted a course,
|
||||
// module contexts below it will not exist).
|
||||
$context = \context::instance_by_id($record->id, false);
|
||||
if (!$context) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_siteadmin($context->instanceid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$courses = enrol_get_users_courses($context->instanceid, false, ['enddate']);
|
||||
foreach ($courses as $course) {
|
||||
if (!$course->enddate) {
|
||||
// We can not know it what is going on here, so we prefer to be conservative.
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if ($course->enddate >= time()) {
|
||||
// Future or ongoing course.
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$expiredcontexts[$context->id] = $context;
|
||||
}
|
||||
|
||||
return $expiredcontexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes user data from the provided context.
|
||||
*
|
||||
* Overwritten to delete the user.
|
||||
*
|
||||
* @param manager $privacymanager
|
||||
* @param expired_context $expiredctx
|
||||
* @return \context|false
|
||||
*/
|
||||
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'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PHPUNIT_TEST) {
|
||||
mtrace('Deleting context ' . $context->id . ' - ' .
|
||||
shorten_text($context->get_context_name(true, true)));
|
||||
}
|
||||
|
||||
// To ensure that all user data is deleted, instead of deleting by context, we run through and collect any stray
|
||||
// contexts for the user that may still exist and call delete_data_for_user().
|
||||
$user = \core_user::get_user($context->instanceid, '*', MUST_EXIST);
|
||||
$approvedlistcollection = new \core_privacy\local\request\contextlist_collection($user->id);
|
||||
$contextlistcollection = $privacymanager->get_contexts_for_userid($user->id);
|
||||
|
||||
foreach ($contextlistcollection as $contextlist) {
|
||||
$approvedlistcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist(
|
||||
$user,
|
||||
$contextlist->get_component(),
|
||||
$contextlist->get_contextids()
|
||||
));
|
||||
}
|
||||
|
||||
$privacymanager->delete_data_for_user($approvedlistcollection);
|
||||
api::set_expired_context_status($expiredctx, expired_context::STATUS_CLEANED);
|
||||
|
||||
// Delete the user.
|
||||
delete_user($user);
|
||||
|
||||
return $context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Expiry Data.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace tool_dataprivacy;
|
||||
|
||||
use core_privacy\manager;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Expiry Data.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class expiry_info {
|
||||
|
||||
/** @var bool Whether this context is fully expired */
|
||||
protected $fullyexpired = false;
|
||||
|
||||
/** @var bool Whether the default expiry value of this purpose has been reached */
|
||||
protected $defaultexpiryreached = false;
|
||||
|
||||
/** @var bool Whether the default purpose is protected */
|
||||
protected $defaultprotected = false;
|
||||
|
||||
/** @var int[] List of expires roles */
|
||||
protected $expired = [];
|
||||
|
||||
/** @var int[] List of unexpires roles */
|
||||
protected $unexpired = [];
|
||||
|
||||
/** @var int[] List of unexpired roles which are also protected */
|
||||
protected $protectedroles = [];
|
||||
|
||||
/**
|
||||
* Constructor for the expiry_info class.
|
||||
*
|
||||
* @param bool $default Whether the default expiry period for this context has been reached.
|
||||
* @param bool $defaultprotected Whether the default expiry is protected.
|
||||
* @param int[] $expired A list of roles in this context which have explicitly expired.
|
||||
* @param int[] $unexpired A list of roles in this context which have not yet expired.
|
||||
* @param int[] $protectedroles A list of unexpired roles in this context which are protected.
|
||||
*/
|
||||
public function __construct(bool $default, bool $defaultprotected, array $expired, array $unexpired, array $protectedroles) {
|
||||
$this->defaultexpiryreached = $default;
|
||||
$this->defaultprotected = $defaultprotected;
|
||||
$this->expired = $expired;
|
||||
$this->unexpired = $unexpired;
|
||||
$this->protectedroles = $protectedroles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this context has 'fully' expired.
|
||||
* That is to say that the default retention period has been reached, and that there are no unexpired roles.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_fully_expired() : bool {
|
||||
return $this->defaultexpiryreached && empty($this->unexpired);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any part of this context has expired.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_any_expired() : bool {
|
||||
if ($this->is_fully_expired()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!empty($this->get_expired_roles())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->is_default_expired()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of explicitly expired role IDs.
|
||||
* Note: This does not list roles which have been expired via the default retention policy being reached.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function get_expired_roles() : array {
|
||||
if ($this->is_default_expired()) {
|
||||
return [];
|
||||
}
|
||||
return $this->expired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the specified role is explicitly expired.
|
||||
* Note: This does not list roles which have been expired via the default retention policy being reached.
|
||||
*
|
||||
* @param int $roleid
|
||||
* @return bool
|
||||
*/
|
||||
public function is_role_expired(int $roleid) : bool {
|
||||
return false !== array_search($roleid, $this->expired);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the default retention policy has been reached.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_default_expired() : bool {
|
||||
return $this->defaultexpiryreached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the default purpose is protected.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_default_protected() : bool {
|
||||
return $this->defaultprotected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of unexpired role IDs.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function get_unexpired_roles() : array {
|
||||
return $this->unexpired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of unexpired protected roles.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function get_unexpired_protected_roles() : array {
|
||||
return array_keys(array_filter($this->protectedroles));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all overridden roles which are unprotected.
|
||||
* @return int[]
|
||||
*/
|
||||
public function get_unprotected_overridden_roles() : array {
|
||||
$allroles = array_merge($this->expired, $this->unexpired);
|
||||
|
||||
return array_diff($allroles, $this->protectedroles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge this expiry_info object with another belonging to a child context in order to set the 'safest' heritage.
|
||||
*
|
||||
* It is not possible to delete any part of a context that is not deleted by a parent.
|
||||
* So if a course's retention policy has been reached, then only parts where the children have also expired can be
|
||||
* deleted.
|
||||
*
|
||||
* @param expiry_info $child The child record to merge with.
|
||||
* @return $this
|
||||
*/
|
||||
public function merge_with_child(expiry_info $child) : expiry_info {
|
||||
if ($child->is_fully_expired()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// If the child is not fully expired, then none of the parents can be either.
|
||||
$this->fullyexpired = false;
|
||||
|
||||
// Remove any role in this node which is not expired in the child.
|
||||
foreach ($this->expired as $key => $roleid) {
|
||||
if (!$child->is_role_expired($roleid)) {
|
||||
unset($this->expired[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
array_merge($this->unexpired, $child->get_unexpired_roles());
|
||||
|
||||
if (!$child->is_default_expired()) {
|
||||
$this->defaultexpiryreached = false;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -90,17 +90,30 @@ class external extends external_api {
|
||||
]);
|
||||
$requestid = $params['requestid'];
|
||||
|
||||
// Validate context.
|
||||
// Validate context and access to manage the registry.
|
||||
$context = context_user::instance($USER->id);
|
||||
self::validate_context($context);
|
||||
|
||||
// Ensure the request exists.
|
||||
$select = 'id = :id AND (userid = :userid OR requestedby = :requestedby)';
|
||||
$params = ['id' => $requestid, 'userid' => $USER->id, 'requestedby' => $USER->id];
|
||||
$requestexists = data_request::record_exists_select($select, $params);
|
||||
$requests = data_request::get_records_select($select, $params);
|
||||
$requestexists = count($requests) === 1;
|
||||
|
||||
$result = false;
|
||||
if ($requestexists) {
|
||||
$request = reset($requests);
|
||||
$datasubject = $request->get('userid');
|
||||
|
||||
if ($datasubject !== $USER->id) {
|
||||
// The user is not the subject. Check that they can cancel this request.
|
||||
if (!api::can_create_data_request_for_user($datasubject)) {
|
||||
$forusercontext = \context_user::instance($datasubject);
|
||||
throw new required_capability_exception($forusercontext,
|
||||
'tool/dataprivacy:makedatarequestsforchildren', 'nopermissions', '');
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Do we want a request to be non-cancellable past a certain point? E.g. When it's already approved/processing.
|
||||
$result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED);
|
||||
} else {
|
||||
@@ -249,9 +262,10 @@ class external extends external_api {
|
||||
]);
|
||||
$requestid = $params['requestid'];
|
||||
|
||||
// Validate context.
|
||||
// Validate context and access to manage the registry.
|
||||
$context = context_system::instance();
|
||||
self::validate_context($context);
|
||||
api::check_can_manage_data_registry();
|
||||
|
||||
$message = get_string('markedcomplete', 'tool_dataprivacy');
|
||||
// Update the data request record.
|
||||
@@ -406,6 +420,85 @@ class external extends external_api {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter description for bulk_approve_data_requests().
|
||||
*
|
||||
* @since Moodle 3.5
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function bulk_approve_data_requests_parameters() {
|
||||
return new external_function_parameters([
|
||||
'requestids' => new external_multiple_structure(
|
||||
new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk approve bulk data request.
|
||||
*
|
||||
* @since Moodle 3.5
|
||||
* @param array $requestids Array consisting the request ID's.
|
||||
* @return array
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
* @throws invalid_parameter_exception
|
||||
* @throws restricted_context_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function bulk_approve_data_requests($requestids) {
|
||||
$warnings = [];
|
||||
$result = false;
|
||||
$params = external_api::validate_parameters(self::bulk_approve_data_requests_parameters(), [
|
||||
'requestids' => $requestids
|
||||
]);
|
||||
$requestids = $params['requestids'];
|
||||
|
||||
// Validate context.
|
||||
$context = context_system::instance();
|
||||
self::validate_context($context);
|
||||
require_capability('tool/dataprivacy:managedatarequests', $context);
|
||||
|
||||
foreach ($requestids as $requestid) {
|
||||
// Ensure the request exists.
|
||||
$requestexists = data_request::record_exists($requestid);
|
||||
|
||||
if ($requestexists) {
|
||||
api::approve_data_request($requestid);
|
||||
} else {
|
||||
$warnings[] = [
|
||||
'item' => $requestid,
|
||||
'warningcode' => 'errorrequestnotfound',
|
||||
'message' => get_string('errorrequestnotfound', 'tool_dataprivacy')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($warnings)) {
|
||||
$result = true;
|
||||
// Add notification in the session to be shown when the page is reloaded on the JS side.
|
||||
notification::success(get_string('requestsapproved', 'tool_dataprivacy'));
|
||||
}
|
||||
|
||||
return [
|
||||
'result' => $result,
|
||||
'warnings' => $warnings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter description for bulk_approve_data_requests().
|
||||
*
|
||||
* @since Moodle 3.5
|
||||
* @return external_description
|
||||
*/
|
||||
public static function bulk_approve_data_requests_returns() {
|
||||
return new external_single_structure([
|
||||
'result' => new external_value(PARAM_BOOL, 'The processing result'),
|
||||
'warnings' => new external_warnings()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter description for deny_data_request().
|
||||
*
|
||||
@@ -475,6 +568,85 @@ class external extends external_api {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter description for bulk_deny_data_requests().
|
||||
*
|
||||
* @since Moodle 3.5
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function bulk_deny_data_requests_parameters() {
|
||||
return new external_function_parameters([
|
||||
'requestids' => new external_multiple_structure(
|
||||
new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk deny data requests.
|
||||
*
|
||||
* @since Moodle 3.5
|
||||
* @param array $requestids Array consisting of request ID's.
|
||||
* @return array
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
* @throws invalid_parameter_exception
|
||||
* @throws restricted_context_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function bulk_deny_data_requests($requestids) {
|
||||
$warnings = [];
|
||||
$result = false;
|
||||
$params = external_api::validate_parameters(self::bulk_deny_data_requests_parameters(), [
|
||||
'requestids' => $requestids
|
||||
]);
|
||||
$requestids = $params['requestids'];
|
||||
|
||||
// Validate context.
|
||||
$context = context_system::instance();
|
||||
self::validate_context($context);
|
||||
require_capability('tool/dataprivacy:managedatarequests', $context);
|
||||
|
||||
foreach ($requestids as $requestid) {
|
||||
// Ensure the request exists.
|
||||
$requestexists = data_request::record_exists($requestid);
|
||||
|
||||
if ($requestexists) {
|
||||
api::deny_data_request($requestid);
|
||||
} else {
|
||||
$warnings[] = [
|
||||
'item' => $requestid,
|
||||
'warningcode' => 'errorrequestnotfound',
|
||||
'message' => get_string('errorrequestnotfound', 'tool_dataprivacy')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($warnings)) {
|
||||
$result = true;
|
||||
// Add notification in the session to be shown when the page is reloaded on the JS side.
|
||||
notification::success(get_string('requestsdenied', 'tool_dataprivacy'));
|
||||
}
|
||||
|
||||
return [
|
||||
'result' => $result,
|
||||
'warnings' => $warnings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter description for bulk_deny_data_requests().
|
||||
*
|
||||
* @since Moodle 3.5
|
||||
* @return external_description
|
||||
*/
|
||||
public static function bulk_deny_data_requests_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().
|
||||
*
|
||||
@@ -567,7 +739,9 @@ class external extends external_api {
|
||||
'jsonformdata' => $jsonformdata
|
||||
]);
|
||||
|
||||
// Validate context and access to manage the registry.
|
||||
self::validate_context(\context_system::instance());
|
||||
api::check_can_manage_data_registry();
|
||||
|
||||
$serialiseddata = json_decode($params['jsonformdata']);
|
||||
$data = array();
|
||||
@@ -632,6 +806,10 @@ class external extends external_api {
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
// Validate context and access to manage the registry.
|
||||
self::validate_context(\context_system::instance());
|
||||
api::check_can_manage_data_registry();
|
||||
|
||||
$result = api::delete_purpose($params['id']);
|
||||
|
||||
return [
|
||||
@@ -678,7 +856,9 @@ class external extends external_api {
|
||||
'jsonformdata' => $jsonformdata
|
||||
]);
|
||||
|
||||
// Validate context and access to manage the registry.
|
||||
self::validate_context(\context_system::instance());
|
||||
api::check_can_manage_data_registry();
|
||||
|
||||
$serialiseddata = json_decode($params['jsonformdata']);
|
||||
$data = array();
|
||||
@@ -743,6 +923,10 @@ class external extends external_api {
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
// Validate context and access to manage the registry.
|
||||
self::validate_context(\context_system::instance());
|
||||
api::check_can_manage_data_registry();
|
||||
|
||||
$result = api::delete_category($params['id']);
|
||||
|
||||
return [
|
||||
@@ -789,8 +973,9 @@ class external extends external_api {
|
||||
'jsonformdata' => $jsonformdata
|
||||
]);
|
||||
|
||||
// Extra permission checkings are delegated to api::set_contextlevel.
|
||||
// Validate context and access to manage the registry.
|
||||
self::validate_context(\context_system::instance());
|
||||
api::check_can_manage_data_registry();
|
||||
|
||||
$serialiseddata = json_decode($params['jsonformdata']);
|
||||
$data = array();
|
||||
@@ -804,7 +989,6 @@ class external extends external_api {
|
||||
$contextlevel = api::set_contextlevel($validateddata);
|
||||
} else if ($errors = $mform->is_validated()) {
|
||||
$warnings[] = json_encode($errors);
|
||||
throw new moodle_exception('generalerror');
|
||||
}
|
||||
|
||||
if ($contextlevel) {
|
||||
@@ -856,8 +1040,9 @@ class external extends external_api {
|
||||
'jsonformdata' => $jsonformdata
|
||||
]);
|
||||
|
||||
// Extra permission checkings are delegated to api::set_context_instance.
|
||||
// Validate context and access to manage the registry.
|
||||
self::validate_context(\context_system::instance());
|
||||
api::check_can_manage_data_registry();
|
||||
|
||||
$serialiseddata = json_decode($params['jsonformdata']);
|
||||
$data = array();
|
||||
@@ -867,6 +1052,7 @@ class external extends external_api {
|
||||
$customdata = \tool_dataprivacy\form\context_instance::get_context_instance_customdata($context);
|
||||
$mform = new \tool_dataprivacy\form\context_instance(null, $customdata, 'post', '', null, true, $data);
|
||||
if ($validateddata = $mform->get_data()) {
|
||||
api::check_can_manage_data_registry($validateddata->contextid);
|
||||
$context = api::set_context_instance($validateddata);
|
||||
} else if ($errors = $mform->is_validated()) {
|
||||
$warnings[] = json_encode($errors);
|
||||
@@ -990,9 +1176,9 @@ class external extends external_api {
|
||||
]);
|
||||
$ids = $params['ids'];
|
||||
|
||||
// Validate context.
|
||||
$context = context_system::instance();
|
||||
self::validate_context($context);
|
||||
// Validate context and access to manage the registry.
|
||||
self::validate_context(\context_system::instance());
|
||||
api::check_can_manage_data_registry();
|
||||
|
||||
$result = true;
|
||||
if (!empty($ids)) {
|
||||
@@ -1002,24 +1188,27 @@ class external extends external_api {
|
||||
$expiredcontext = new expired_context($id);
|
||||
$targetcontext = context_helper::instance_by_id($expiredcontext->get('contextid'));
|
||||
|
||||
// Fetch this context's child contexts. Make sure that all of the child contexts are flagged for deletion.
|
||||
$childcontexts = $targetcontext->get_child_contexts();
|
||||
foreach ($childcontexts as $child) {
|
||||
if ($expiredchildcontext = expired_context::get_record(['contextid' => $child->id])) {
|
||||
// Add this child context to the list for approval.
|
||||
$expiredcontextstoapprove[] = $expiredchildcontext;
|
||||
} else {
|
||||
// This context has not yet been flagged for deletion.
|
||||
$result = false;
|
||||
$message = get_string('errorcontexthasunexpiredchildren', 'tool_dataprivacy',
|
||||
$targetcontext->get_context_name(false));
|
||||
$warnings[] = [
|
||||
'item' => 'tool_dataprivacy_ctxexpired',
|
||||
'warningcode' => 'errorcontexthasunexpiredchildren',
|
||||
'message' => $message
|
||||
];
|
||||
// Exit the process.
|
||||
break 2;
|
||||
if (!$targetcontext instanceof \context_user) {
|
||||
// Fetch this context's child contexts. Make sure that all of the child contexts are flagged for deletion.
|
||||
// User context children do not need to be considered.
|
||||
$childcontexts = $targetcontext->get_child_contexts();
|
||||
foreach ($childcontexts as $child) {
|
||||
if ($expiredchildcontext = expired_context::get_record(['contextid' => $child->id])) {
|
||||
// Add this child context to the list for approval.
|
||||
$expiredcontextstoapprove[] = $expiredchildcontext;
|
||||
} else {
|
||||
// This context has not yet been flagged for deletion.
|
||||
$result = false;
|
||||
$message = get_string('errorcontexthasunexpiredchildren', 'tool_dataprivacy',
|
||||
$targetcontext->get_context_name(false));
|
||||
$warnings[] = [
|
||||
'item' => 'tool_dataprivacy_ctxexpired',
|
||||
'warningcode' => 'errorcontexthasunexpiredchildren',
|
||||
'message' => $message
|
||||
];
|
||||
// Exit the process.
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1065,6 +1254,289 @@ class external extends external_api {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for set_context_defaults().
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function set_context_defaults_parameters() {
|
||||
return new external_function_parameters([
|
||||
'contextlevel' => new external_value(PARAM_INT, 'The context level', VALUE_REQUIRED),
|
||||
'category' => new external_value(PARAM_INT, 'The default category for the given context level', VALUE_REQUIRED),
|
||||
'purpose' => new external_value(PARAM_INT, 'The default purpose for the given context level', VALUE_REQUIRED),
|
||||
'activity' => new external_value(PARAM_PLUGIN, 'The plugin name of the activity', VALUE_DEFAULT, null),
|
||||
'override' => new external_value(PARAM_BOOL, 'Whether to override existing instances with the defaults', VALUE_DEFAULT,
|
||||
false),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the default category and purpose for a given context level (and optionally, a plugin).
|
||||
*
|
||||
* @param int $contextlevel The context level.
|
||||
* @param int $category The ID matching the category.
|
||||
* @param int $purpose The ID matching the purpose record.
|
||||
* @param int $activity The name of the activity that we're making a defaults configuration for.
|
||||
* @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
|
||||
* @return array
|
||||
*/
|
||||
public static function set_context_defaults($contextlevel, $category, $purpose, $activity, $override) {
|
||||
$warnings = [];
|
||||
|
||||
$params = external_api::validate_parameters(self::set_context_defaults_parameters(), [
|
||||
'contextlevel' => $contextlevel,
|
||||
'category' => $category,
|
||||
'purpose' => $purpose,
|
||||
'activity' => $activity,
|
||||
'override' => $override,
|
||||
]);
|
||||
$contextlevel = $params['contextlevel'];
|
||||
$category = $params['category'];
|
||||
$purpose = $params['purpose'];
|
||||
$activity = $params['activity'];
|
||||
$override = $params['override'];
|
||||
|
||||
// Validate context.
|
||||
$context = context_system::instance();
|
||||
self::validate_context($context);
|
||||
api::check_can_manage_data_registry();
|
||||
|
||||
// Set the context defaults.
|
||||
$result = api::set_context_defaults($contextlevel, $category, $purpose, $activity, $override);
|
||||
|
||||
return [
|
||||
'result' => $result,
|
||||
'warnings' => $warnings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns for set_context_defaults().
|
||||
*
|
||||
* @return external_single_structure
|
||||
*/
|
||||
public static function set_context_defaults_returns() {
|
||||
return new external_single_structure([
|
||||
'result' => new external_value(PARAM_BOOL, 'Whether the context defaults were successfully set or not'),
|
||||
'warnings' => new external_warnings()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for get_category_options().
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function get_category_options_parameters() {
|
||||
return new external_function_parameters([
|
||||
'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
|
||||
'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a list of data category options containing category IDs as keys and the category name for the value.
|
||||
*
|
||||
* @param bool $includeinherit Whether to include the "Inherit" option.
|
||||
* @param bool $includenotset Whether to include the "Not set" option.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_category_options($includeinherit, $includenotset) {
|
||||
$warnings = [];
|
||||
|
||||
$params = self::validate_parameters(self::get_category_options_parameters(), [
|
||||
'includeinherit' => $includeinherit,
|
||||
'includenotset' => $includenotset
|
||||
]);
|
||||
$includeinherit = $params['includeinherit'];
|
||||
$includenotset = $params['includenotset'];
|
||||
|
||||
$context = context_system::instance();
|
||||
self::validate_context($context);
|
||||
api::check_can_manage_data_registry();
|
||||
|
||||
$categories = api::get_categories();
|
||||
$options = data_registry_page::category_options($categories, $includenotset, $includeinherit);
|
||||
$categoryoptions = [];
|
||||
foreach ($options as $id => $name) {
|
||||
$categoryoptions[] = [
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'options' => $categoryoptions,
|
||||
'warnings' => $warnings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns for get_category_options().
|
||||
*
|
||||
* @return external_single_structure
|
||||
*/
|
||||
public static function get_category_options_returns() {
|
||||
$optiondefinition = new external_single_structure(
|
||||
[
|
||||
'id' => new external_value(PARAM_INT, 'The category ID'),
|
||||
'name' => new external_value(PARAM_TEXT, 'The category name'),
|
||||
]
|
||||
);
|
||||
|
||||
return new external_single_structure([
|
||||
'options' => new external_multiple_structure($optiondefinition),
|
||||
'warnings' => new external_warnings()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for get_purpose_options().
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function get_purpose_options_parameters() {
|
||||
return new external_function_parameters([
|
||||
'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
|
||||
'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a list of data storage purposes containing purpose IDs as keys and the purpose name for the value.
|
||||
*
|
||||
* @param bool $includeinherit Whether to include the "Inherit" option.
|
||||
* @param bool $includenotset Whether to include the "Not set" option.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_purpose_options($includeinherit, $includenotset) {
|
||||
$warnings = [];
|
||||
|
||||
$params = self::validate_parameters(self::get_category_options_parameters(), [
|
||||
'includeinherit' => $includeinherit,
|
||||
'includenotset' => $includenotset
|
||||
]);
|
||||
$includeinherit = $params['includeinherit'];
|
||||
$includenotset = $params['includenotset'];
|
||||
|
||||
$context = context_system::instance();
|
||||
self::validate_context($context);
|
||||
|
||||
$purposes = api::get_purposes();
|
||||
$options = data_registry_page::purpose_options($purposes, $includenotset, $includeinherit);
|
||||
$purposeoptions = [];
|
||||
foreach ($options as $id => $name) {
|
||||
$purposeoptions[] = [
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'options' => $purposeoptions,
|
||||
'warnings' => $warnings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns for get_purpose_options().
|
||||
*
|
||||
* @return external_single_structure
|
||||
*/
|
||||
public static function get_purpose_options_returns() {
|
||||
$optiondefinition = new external_single_structure(
|
||||
[
|
||||
'id' => new external_value(PARAM_INT, 'The purpose ID'),
|
||||
'name' => new external_value(PARAM_TEXT, 'The purpose name'),
|
||||
]
|
||||
);
|
||||
|
||||
return new external_single_structure([
|
||||
'options' => new external_multiple_structure($optiondefinition),
|
||||
'warnings' => new external_warnings()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for get_activity_options().
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function get_activity_options_parameters() {
|
||||
return new external_function_parameters([
|
||||
'nodefaults' => new external_value(PARAM_BOOL, 'Whether to fetch all activities or only those without defaults',
|
||||
VALUE_DEFAULT, false),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a list of activity options for setting data registry defaults.
|
||||
*
|
||||
* @param boolean $nodefaults If false, it will fetch all of the activities. Otherwise, it will only fetch the activities
|
||||
* that don't have defaults yet (e.g. when adding a new activity module defaults).
|
||||
* @return array
|
||||
*/
|
||||
public static function get_activity_options($nodefaults) {
|
||||
$warnings = [];
|
||||
|
||||
$params = self::validate_parameters(self::get_activity_options_parameters(), [
|
||||
'nodefaults' => $nodefaults,
|
||||
]);
|
||||
$nodefaults = $params['nodefaults'];
|
||||
|
||||
$context = context_system::instance();
|
||||
self::validate_context($context);
|
||||
|
||||
// Get activity module plugin info.
|
||||
$pluginmanager = \core_plugin_manager::instance();
|
||||
$modplugins = $pluginmanager->get_enabled_plugins('mod');
|
||||
$modoptions = [];
|
||||
|
||||
// Get the module-level defaults. data_registry::get_defaults falls back to this when there are no activity defaults.
|
||||
list($levelpurpose, $levelcategory) = data_registry::get_defaults(CONTEXT_MODULE);
|
||||
foreach ($modplugins as $name) {
|
||||
// Check if we have default purpose and category for this module if we want don't want to fetch everything.
|
||||
if ($nodefaults) {
|
||||
list($purpose, $category) = data_registry::get_defaults(CONTEXT_MODULE, $name);
|
||||
// Compare this with the module-level defaults.
|
||||
if ($purpose !== $levelpurpose || $category !== $levelcategory) {
|
||||
// If the defaults for this activity has been already set, there's no need to add this in the list of options.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$displayname = $pluginmanager->plugin_name('mod_' . $name);
|
||||
$modoptions[] = (object)[
|
||||
'name' => $name,
|
||||
'displayname' => $displayname
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'options' => $modoptions,
|
||||
'warnings' => $warnings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns for get_category_options().
|
||||
*
|
||||
* @return external_single_structure
|
||||
*/
|
||||
public static function get_activity_options_returns() {
|
||||
$optionsdefinition = new external_single_structure(
|
||||
[
|
||||
'name' => new external_value(PARAM_TEXT, 'The plugin name of the activity'),
|
||||
'displayname' => new external_value(PARAM_TEXT, 'The display name of the activity'),
|
||||
]
|
||||
);
|
||||
|
||||
return new external_single_structure([
|
||||
'options' => new external_multiple_structure($optionsdefinition),
|
||||
'warnings' => new external_warnings()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the structure of a tree node (link + child branches).
|
||||
*
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace tool_dataprivacy\external;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core\external\persistent_exporter;
|
||||
use tool_dataprivacy\category;
|
||||
use tool_dataprivacy\context_instance;
|
||||
|
||||
/**
|
||||
* Class for exporting field data.
|
||||
@@ -53,4 +55,25 @@ class category_exporter extends persistent_exporter {
|
||||
'context' => 'context',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function that fetches a category name from the given ID.
|
||||
*
|
||||
* @param int $categoryid The category ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID.
|
||||
* @return string The purpose name.
|
||||
*/
|
||||
public static function get_name($categoryid) {
|
||||
global $PAGE;
|
||||
if ($categoryid === false || $categoryid == context_instance::INHERIT) {
|
||||
return get_string('inherit', 'tool_dataprivacy');
|
||||
} else if ($categoryid == context_instance::NOTSET) {
|
||||
return get_string('notset', 'tool_dataprivacy');
|
||||
} else {
|
||||
$purpose = new category($categoryid);
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
$exporter = new self($purpose, ['context' => \context_system::instance()]);
|
||||
$data = $exporter->export($output);
|
||||
return $data->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-13
@@ -26,9 +26,9 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use coding_exception;
|
||||
use core\external\persistent_exporter;
|
||||
use DateInterval;
|
||||
use Exception;
|
||||
use renderer_base;
|
||||
use tool_dataprivacy\context_instance;
|
||||
use tool_dataprivacy\purpose;
|
||||
|
||||
/**
|
||||
@@ -78,6 +78,9 @@ class purpose_exporter extends persistent_exporter {
|
||||
'multiple' => true,
|
||||
'optional' => true
|
||||
],
|
||||
'roleoverrides' => [
|
||||
'type' => PARAM_TEXT
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -124,23 +127,35 @@ class purpose_exporter extends persistent_exporter {
|
||||
|
||||
$retentionperiod = $this->persistent->get('retentionperiod');
|
||||
if ($retentionperiod) {
|
||||
$interval = new DateInterval($retentionperiod);
|
||||
|
||||
// It is one or another.
|
||||
if ($interval->y) {
|
||||
$formattedtime = get_string('numyears', 'moodle', $interval->format('%y'));
|
||||
} else if ($interval->m) {
|
||||
$formattedtime = get_string('nummonths', 'moodle', $interval->format('%m'));
|
||||
} else if ($interval->d) {
|
||||
$formattedtime = get_string('numdays', 'moodle', $interval->format('%d'));
|
||||
} else {
|
||||
$formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy');
|
||||
}
|
||||
$formattedtime = \tool_dataprivacy\api::format_retention_period(new \DateInterval($retentionperiod));
|
||||
} else {
|
||||
$formattedtime = get_string('retentionperiodnotdefined', 'tool_dataprivacy');
|
||||
}
|
||||
$values['formattedretentionperiod'] = $formattedtime;
|
||||
|
||||
$values['roleoverrides'] = !empty($this->persistent->get_purpose_overrides());
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function that fetches a purpose name from the given ID.
|
||||
*
|
||||
* @param int $purposeid The purpose ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID.
|
||||
* @return string The purpose name.
|
||||
*/
|
||||
public static function get_name($purposeid) {
|
||||
global $PAGE;
|
||||
if ($purposeid === false || $purposeid == context_instance::INHERIT) {
|
||||
return get_string('inherit', 'tool_dataprivacy');
|
||||
} else if ($purposeid == context_instance::NOTSET) {
|
||||
return get_string('notset', 'tool_dataprivacy');
|
||||
} else {
|
||||
$purpose = new purpose($purposeid);
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
$exporter = new self($purpose, ['context' => \context_system::instance()]);
|
||||
$data = $exporter->export($output);
|
||||
return $data->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* An implementation of a userlist which has been filtered and approved.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_dataprivacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* An implementation of a userlist which can be filtered by role.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class filtered_userlist extends \core_privacy\local\request\approved_userlist {
|
||||
|
||||
/**
|
||||
* Apply filters to only remove users in the expireduserids list, and to remove any who are in the unexpired list.
|
||||
* The unexpired list wins where a user is in both lists.
|
||||
*
|
||||
* @param int[] $expireduserids The list of userids for users who should be expired.
|
||||
* @param int[] $unexpireduserids The list of userids for those users who should not be expired.
|
||||
* @return $this
|
||||
*/
|
||||
public function apply_expired_context_filters(array $expireduserids, array $unexpireduserids) : filtered_userlist {
|
||||
// The current userlist content.
|
||||
$userids = $this->get_userids();
|
||||
|
||||
if (!empty($expireduserids)) {
|
||||
// Now remove any not on the list of expired users.
|
||||
$userids = array_intersect($userids, $expireduserids);
|
||||
}
|
||||
|
||||
if (!empty($unexpireduserids)) {
|
||||
// Remove any on the list of unexpiredusers users.
|
||||
$userids = array_diff($userids, $unexpireduserids);
|
||||
}
|
||||
|
||||
$this->set_userids($userids);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -145,12 +145,12 @@ class context_instance extends \core\form\persistent {
|
||||
$persistent->set('contextid', $context->id);
|
||||
}
|
||||
|
||||
$purposeoptions = \tool_dataprivacy\output\data_registry_page::purpose_options(
|
||||
api::get_purposes()
|
||||
);
|
||||
$categoryoptions = \tool_dataprivacy\output\data_registry_page::category_options(
|
||||
api::get_categories()
|
||||
);
|
||||
$purposes = [];
|
||||
foreach (api::get_purposes() as $purpose) {
|
||||
$purposes[$purpose->get('id')] = $purpose;
|
||||
}
|
||||
$purposeoptions = \tool_dataprivacy\output\data_registry_page::purpose_options($purposes);
|
||||
$categoryoptions = \tool_dataprivacy\output\data_registry_page::category_options(api::get_categories());
|
||||
|
||||
$customdata = [
|
||||
'context' => $context,
|
||||
@@ -168,9 +168,14 @@ class context_instance extends \core\form\persistent {
|
||||
$context);
|
||||
|
||||
$customdata['purposeretentionperiods'] = [];
|
||||
foreach ($purposeoptions as $optionvalue => $unused) {
|
||||
// Get the effective purpose if $optionvalue would be the selected value.
|
||||
$purpose = api::get_effective_context_purpose($context, $optionvalue);
|
||||
foreach (array_keys($purposeoptions) as $optionvalue) {
|
||||
|
||||
if (isset($purposes[$optionvalue])) {
|
||||
$purpose = $purposes[$optionvalue];
|
||||
} else {
|
||||
// Get the effective purpose if $optionvalue would be the selected value.
|
||||
$purpose = api::get_effective_context_purpose($context, $optionvalue);
|
||||
}
|
||||
|
||||
$retentionperiod = self::get_retention_display_text(
|
||||
$purpose,
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* This file contains the defaults form.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 David Monllao
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_dataprivacy\form;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \tool_dataprivacy\output\data_registry_page;
|
||||
|
||||
require_once($CFG->libdir . '/formslib.php');
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
/**
|
||||
* Context levels defaults form.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 David Monllao
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class defaults extends \moodleform {
|
||||
|
||||
/**
|
||||
* Define the form.
|
||||
*/
|
||||
public function definition() {
|
||||
global $OUTPUT;
|
||||
|
||||
$mform = $this->_form;
|
||||
$mform->setDisableShortforms();
|
||||
|
||||
$notification = $OUTPUT->notification(get_string('defaultsinfo', 'tool_dataprivacy'),
|
||||
\core\output\notification::NOTIFY_INFO);
|
||||
$mform->addElement('html', $notification);
|
||||
|
||||
foreach ($this->_customdata['levels'] as $level => $classname) {
|
||||
|
||||
$mform->addElement('header', $classname . '-header',
|
||||
get_string('contextlevelname' . $level, 'tool_dataprivacy'));
|
||||
|
||||
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
|
||||
|
||||
$includeinherit = true;
|
||||
if ($level == CONTEXT_SYSTEM) {
|
||||
$includeinherit = false;
|
||||
}
|
||||
|
||||
$categoryoptions = data_registry_page::category_options($this->_customdata['categories'], false, $includeinherit);
|
||||
$purposeoptions = data_registry_page::category_options($this->_customdata['purposes'], false, $includeinherit);
|
||||
|
||||
$mform->addElement('select', $categoryvar, get_string('category', 'tool_dataprivacy'), $categoryoptions);
|
||||
$mform->addHelpButton($categoryvar, 'categorydefault', 'tool_dataprivacy');
|
||||
$mform->setType($categoryvar, PARAM_INT);
|
||||
|
||||
$mform->addElement('select', $purposevar, get_string('purpose', 'tool_dataprivacy'), $purposeoptions);
|
||||
$mform->addHelpButton($purposevar, 'purposedefault', 'tool_dataprivacy');
|
||||
$mform->setType($purposevar, PARAM_INT);
|
||||
}
|
||||
|
||||
$this->add_action_buttons(true, get_string('savechanges'));
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,11 @@ class purpose extends persistent {
|
||||
*/
|
||||
protected static $persistentclass = 'tool_dataprivacy\\purpose';
|
||||
|
||||
/**
|
||||
* @var array The list of current overrides.
|
||||
*/
|
||||
protected $existingoverrides = [];
|
||||
|
||||
/**
|
||||
* Define the form - called by parent constructor
|
||||
*/
|
||||
@@ -56,41 +61,16 @@ class purpose extends persistent {
|
||||
$mform->setType('description', PARAM_CLEANHTML);
|
||||
|
||||
// Field for selecting lawful bases (from GDPR Article 6.1).
|
||||
$lawfulbases = [];
|
||||
foreach (\tool_dataprivacy\purpose::GDPR_ART_6_1_ITEMS as $article) {
|
||||
$key = 'gdpr_art_6_1_' . $article;
|
||||
$lawfulbases[$key] = get_string($key . '_name', 'tool_dataprivacy');
|
||||
}
|
||||
$options = array(
|
||||
'multiple' => true,
|
||||
);
|
||||
$mform->addElement('autocomplete', 'lawfulbases', get_string('lawfulbases', 'tool_dataprivacy'), $lawfulbases, $options);
|
||||
$this->add_field($this->get_lawful_base_field());
|
||||
$mform->addRule('lawfulbases', get_string('required'), 'required', null, 'server');
|
||||
$mform->addHelpButton('lawfulbases', 'lawfulbases', 'tool_dataprivacy');
|
||||
|
||||
// Optional field for selecting reasons for collecting sensitive personal data (from GDPR Article 9.2).
|
||||
$sensitivereasons = [];
|
||||
foreach (\tool_dataprivacy\purpose::GDPR_ART_9_2_ITEMS as $article) {
|
||||
$key = 'gdpr_art_9_2_' . $article;
|
||||
$sensitivereasons[$key] = get_string($key . '_name', 'tool_dataprivacy');
|
||||
}
|
||||
$mform->addElement('autocomplete', 'sensitivedatareasons', get_string('sensitivedatareasons', 'tool_dataprivacy'),
|
||||
$sensitivereasons, $options);
|
||||
$mform->addHelpButton('sensitivedatareasons', 'sensitivedatareasons', 'tool_dataprivacy');
|
||||
$this->add_field($this->get_sensitive_base_field());
|
||||
|
||||
$number = $mform->createElement('text', 'retentionperiodnumber', null, ['size' => 8]);
|
||||
$unitoptions = [
|
||||
'Y' => get_string('years'),
|
||||
'M' => strtolower(get_string('months')),
|
||||
'D' => strtolower(get_string('days'))
|
||||
];
|
||||
$unit = $mform->createElement('select', 'retentionperiodunit', '', $unitoptions);
|
||||
$mform->addGroup(['number' => $number, 'unit' => $unit], 'retentionperiod',
|
||||
get_string('retentionperiod', 'tool_dataprivacy'), null, false);
|
||||
$mform->setType('retentionperiodnumber', PARAM_INT);
|
||||
$this->add_field($this->get_retention_period_fields());
|
||||
$this->add_field($this->get_protected_field());
|
||||
|
||||
$this->_form->addElement('advcheckbox', 'protected', get_string('protected', 'tool_dataprivacy'),
|
||||
get_string('protectedlabel', 'tool_dataprivacy'));
|
||||
$this->add_override_fields();
|
||||
|
||||
if (!empty($this->_customdata['showbuttons'])) {
|
||||
if (!$this->get_persistent()->get('id')) {
|
||||
@@ -102,16 +82,360 @@ class purpose extends persistent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a fieldset to the current form.
|
||||
*
|
||||
* @param \stdClass $data
|
||||
*/
|
||||
protected function add_field(\stdClass $data) {
|
||||
foreach ($data->fields as $field) {
|
||||
$this->_form->addElement($field);
|
||||
}
|
||||
|
||||
if (!empty($data->helps)) {
|
||||
foreach ($data->helps as $fieldname => $helpdata) {
|
||||
$help = array_merge([$fieldname], $helpdata);
|
||||
call_user_func_array([$this->_form, 'addHelpButton'], $help);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data->types)) {
|
||||
foreach ($data->types as $fieldname => $type) {
|
||||
$this->_form->setType($fieldname, $type);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data->rules)) {
|
||||
foreach ($data->rules as $fieldname => $ruledata) {
|
||||
$rule = array_merge([$fieldname], $ruledata);
|
||||
call_user_func_array([$this->_form, 'addRule'], $rule);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data->defaults)) {
|
||||
foreach ($data->defaults as $fieldname => $default) {
|
||||
$this->_form($fieldname, $default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle addition of relevant repeated element fields for role overrides.
|
||||
*/
|
||||
protected function add_override_fields() {
|
||||
$purpose = $this->get_persistent();
|
||||
|
||||
if (empty($purpose->get('id'))) {
|
||||
// It is not possible to use repeated elements in a modal form yet.
|
||||
return;
|
||||
}
|
||||
|
||||
$fields = [
|
||||
$this->get_role_override_id('roleoverride_'),
|
||||
$this->get_role_field('roleoverride_'),
|
||||
$this->get_retention_period_fields('roleoverride_'),
|
||||
$this->get_protected_field('roleoverride_'),
|
||||
$this->get_lawful_base_field('roleoverride_'),
|
||||
$this->get_sensitive_base_field('roleoverride_'),
|
||||
];
|
||||
|
||||
$options = [
|
||||
'type' => [],
|
||||
'helpbutton' => [],
|
||||
];
|
||||
|
||||
// Start by adding the title.
|
||||
$overrideelements = [
|
||||
$this->_form->createElement('header', 'roleoverride', get_string('roleoverride', 'tool_dataprivacy')),
|
||||
$this->_form->createElement(
|
||||
'static',
|
||||
'roleoverrideoverview',
|
||||
'',
|
||||
get_string('roleoverrideoverview', 'tool_dataprivacy')
|
||||
),
|
||||
];
|
||||
|
||||
foreach ($fields as $fielddata) {
|
||||
foreach ($fielddata->fields as $field) {
|
||||
$overrideelements[] = $field;
|
||||
}
|
||||
|
||||
if (!empty($fielddata->helps)) {
|
||||
foreach ($fielddata->helps as $name => $help) {
|
||||
if (!isset($options[$name])) {
|
||||
$options[$name] = [];
|
||||
}
|
||||
$options[$name]['helpbutton'] = $help;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fielddata->types)) {
|
||||
foreach ($fielddata->types as $name => $type) {
|
||||
if (!isset($options[$name])) {
|
||||
$options[$name] = [];
|
||||
}
|
||||
$options[$name]['type'] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fielddata->rules)) {
|
||||
foreach ($fielddata->rules as $name => $rule) {
|
||||
if (!isset($options[$name])) {
|
||||
$options[$name] = [];
|
||||
}
|
||||
$options[$name]['rule'] = $rule;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fielddata->defaults)) {
|
||||
foreach ($fielddata->defaults as $name => $default) {
|
||||
if (!isset($options[$name])) {
|
||||
$options[$name] = [];
|
||||
}
|
||||
$options[$name]['default'] = $default;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fielddata->advanceds)) {
|
||||
foreach ($fielddata->advanceds as $name => $advanced) {
|
||||
if (!isset($options[$name])) {
|
||||
$options[$name] = [];
|
||||
}
|
||||
$options[$name]['advanced'] = $advanced;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->existingoverrides = $purpose->get_purpose_overrides();
|
||||
$existingoverridecount = count($this->existingoverrides);
|
||||
|
||||
$this->repeat_elements(
|
||||
$overrideelements,
|
||||
$existingoverridecount,
|
||||
$options,
|
||||
'overrides',
|
||||
'addoverride',
|
||||
1,
|
||||
get_string('addroleoverride', 'tool_dataprivacy')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts fields.
|
||||
*
|
||||
* @param \stdClass $data
|
||||
* @return \stdClass
|
||||
*/
|
||||
public function filter_data_for_persistent($data) {
|
||||
$data = parent::filter_data_for_persistent($data);
|
||||
|
||||
$classname = static::$persistentclass;
|
||||
$properties = $classname::properties_definition();
|
||||
|
||||
$data = (object) array_filter((array) $data, function($value, $key) use ($properties) {
|
||||
return isset($properties[$key]);
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the field for the role name.
|
||||
*
|
||||
* @param string $prefix The prefix to apply to the field
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function get_role_override_id(string $prefix = '') : \stdClass {
|
||||
$fieldname = "{$prefix}id";
|
||||
|
||||
$fielddata = (object) [
|
||||
'fields' => [],
|
||||
];
|
||||
|
||||
$fielddata->fields[] = $this->_form->createElement('hidden', $fieldname);
|
||||
$fielddata->types[$fieldname] = PARAM_INT;
|
||||
|
||||
return $fielddata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the field for the role name.
|
||||
*
|
||||
* @param string $prefix The prefix to apply to the field
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function get_role_field(string $prefix = '') : \stdClass {
|
||||
$fieldname = "{$prefix}roleid";
|
||||
|
||||
$fielddata = (object) [
|
||||
'fields' => [],
|
||||
'helps' => [],
|
||||
];
|
||||
|
||||
$roles = [
|
||||
'' => get_string('none'),
|
||||
];
|
||||
foreach (role_get_names() as $roleid => $role) {
|
||||
$roles[$roleid] = $role->localname;
|
||||
}
|
||||
|
||||
$fielddata->fields[] = $this->_form->createElement('select', $fieldname, get_string('role'),
|
||||
$roles,
|
||||
[
|
||||
'multiple' => false,
|
||||
]
|
||||
);
|
||||
$fielddata->helps[$fieldname] = ['role', 'tool_dataprivacy'];
|
||||
$fielddata->defaults[$fieldname] = null;
|
||||
|
||||
return $fielddata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mform field for lawful bases.
|
||||
*
|
||||
* @param string $prefix The prefix to apply to the field
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function get_lawful_base_field(string $prefix = '') : \stdClass {
|
||||
$fieldname = "{$prefix}lawfulbases";
|
||||
|
||||
$data = (object) [
|
||||
'fields' => [],
|
||||
];
|
||||
|
||||
$bases = [];
|
||||
foreach (\tool_dataprivacy\purpose::GDPR_ART_6_1_ITEMS as $article) {
|
||||
$key = 'gdpr_art_6_1_' . $article;
|
||||
$bases[$key] = get_string("{$key}_name", 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
$data->fields[] = $this->_form->createElement('autocomplete', $fieldname, get_string('lawfulbases', 'tool_dataprivacy'),
|
||||
$bases,
|
||||
[
|
||||
'multiple' => true,
|
||||
]
|
||||
);
|
||||
|
||||
$data->helps = [
|
||||
$fieldname => ['lawfulbases', 'tool_dataprivacy'],
|
||||
];
|
||||
|
||||
$data->advanceds = [
|
||||
$fieldname => true,
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mform field for sensitive bases.
|
||||
*
|
||||
* @param string $prefix The prefix to apply to the field
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function get_sensitive_base_field(string $prefix = '') : \stdClass {
|
||||
$fieldname = "{$prefix}sensitivedatareasons";
|
||||
|
||||
$data = (object) [
|
||||
'fields' => [],
|
||||
];
|
||||
|
||||
$bases = [];
|
||||
foreach (\tool_dataprivacy\purpose::GDPR_ART_9_2_ITEMS as $article) {
|
||||
$key = 'gdpr_art_9_2_' . $article;
|
||||
$bases[$key] = get_string("{$key}_name", 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
$data->fields[] = $this->_form->createElement(
|
||||
'autocomplete',
|
||||
$fieldname,
|
||||
get_string('sensitivedatareasons', 'tool_dataprivacy'),
|
||||
$bases,
|
||||
[
|
||||
'multiple' => true,
|
||||
]
|
||||
);
|
||||
$data->helps = [
|
||||
$fieldname => ['sensitivedatareasons', 'tool_dataprivacy'],
|
||||
];
|
||||
|
||||
$data->advanceds = [
|
||||
$fieldname => true,
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the retention period fields.
|
||||
*
|
||||
* @param string $prefix The name of the main field, and prefix for the subfields.
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function get_retention_period_fields(string $prefix = '') : \stdClass {
|
||||
$prefix = "{$prefix}retentionperiod";
|
||||
$data = (object) [
|
||||
'fields' => [],
|
||||
'types' => [],
|
||||
];
|
||||
|
||||
$number = $this->_form->createElement('text', "{$prefix}number", null, ['size' => 8]);
|
||||
$data->types["{$prefix}number"] = PARAM_INT;
|
||||
|
||||
$unitoptions = [
|
||||
'Y' => get_string('years'),
|
||||
'M' => strtolower(get_string('months')),
|
||||
'D' => strtolower(get_string('days'))
|
||||
];
|
||||
$unit = $this->_form->createElement('select', "{$prefix}unit", '', $unitoptions);
|
||||
|
||||
$data->fields[] = $this->_form->createElement(
|
||||
'group',
|
||||
$prefix,
|
||||
get_string('retentionperiod', 'tool_dataprivacy'),
|
||||
[
|
||||
'number' => $number,
|
||||
'unit' => $unit,
|
||||
],
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mform field for the protected flag.
|
||||
*
|
||||
* @param string $prefix The prefix to apply to the field
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function get_protected_field(string $prefix = '') : \stdClass {
|
||||
$fieldname = "{$prefix}protected";
|
||||
|
||||
return (object) [
|
||||
'fields' => [
|
||||
$this->_form->createElement(
|
||||
'advcheckbox',
|
||||
$fieldname,
|
||||
get_string('protected', 'tool_dataprivacy'),
|
||||
get_string('protectedlabel', 'tool_dataprivacy')
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts data to data suitable for storage.
|
||||
*
|
||||
* @param \stdClass $data
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected static function convert_fields(\stdClass $data) {
|
||||
$data = parent::convert_fields($data);
|
||||
|
||||
if (is_array($data->lawfulbases)) {
|
||||
if (!empty($data->lawfulbases) && is_array($data->lawfulbases)) {
|
||||
$data->lawfulbases = implode(',', $data->lawfulbases);
|
||||
}
|
||||
if (!empty($data->sensitivedatareasons) && is_array($data->sensitivedatareasons)) {
|
||||
@@ -122,6 +446,7 @@ class purpose extends persistent {
|
||||
$data->retentionperiod = 'P' . $data->retentionperiodnumber . $data->retentionperiodunit;
|
||||
unset($data->retentionperiodnumber);
|
||||
unset($data->retentionperiodunit);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -133,6 +458,16 @@ class purpose extends persistent {
|
||||
protected function get_default_data() {
|
||||
$data = parent::get_default_data();
|
||||
|
||||
return $this->convert_existing_data_to_values($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise any values stored in existing data.
|
||||
*
|
||||
* @param \stdClass $data
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function convert_existing_data_to_values(\stdClass $data) : \stdClass {
|
||||
$data->lawfulbases = explode(',', $data->lawfulbases);
|
||||
if (!empty($data->sensitivedatareasons)) {
|
||||
$data->sensitivedatareasons = explode(',', $data->sensitivedatareasons);
|
||||
@@ -146,4 +481,94 @@ class purpose extends persistent {
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the role override data from the list of submitted data.
|
||||
*
|
||||
* @param \stdClass $data The complete set of processed data
|
||||
* @return \stdClass[] The list of overrides
|
||||
*/
|
||||
public function get_role_overrides_from_data(\stdClass $data) {
|
||||
$overrides = [];
|
||||
if (!empty($data->overrides)) {
|
||||
$searchkey = 'roleoverride_';
|
||||
|
||||
for ($i = 0; $i < $data->overrides; $i++) {
|
||||
$overridedata = (object) [];
|
||||
foreach ((array) $data as $fieldname => $value) {
|
||||
if (strpos($fieldname, $searchkey) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$overridefieldname = substr($fieldname, strlen($searchkey));
|
||||
$overridedata->$overridefieldname = $value[$i];
|
||||
}
|
||||
|
||||
if (empty($overridedata->roleid) || empty($overridedata->retentionperiodnumber)) {
|
||||
// Skip this one.
|
||||
// There is no value and it will be delete.
|
||||
continue;
|
||||
}
|
||||
|
||||
$override = static::convert_fields($overridedata);
|
||||
|
||||
$overrides[$i] = $override;
|
||||
}
|
||||
}
|
||||
|
||||
return $overrides;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define extra validation mechanims.
|
||||
*
|
||||
* @param stdClass $data Data to validate.
|
||||
* @param array $files Array of files.
|
||||
* @param array $errors Currently reported errors.
|
||||
* @return array of additional errors, or overridden errors.
|
||||
*/
|
||||
protected function extra_validation($data, $files, array &$errors) {
|
||||
$overrides = $this->get_role_overrides_from_data($data);
|
||||
|
||||
// Check role overrides to ensure that:
|
||||
// - roles are unique; and
|
||||
// - specifeid retention periods are numeric.
|
||||
$seenroleids = [];
|
||||
foreach ($overrides as $id => $override) {
|
||||
$override->purposeid = 0;
|
||||
$persistent = new \tool_dataprivacy\purpose_override($override->id, $override);
|
||||
|
||||
if (isset($seenroleids[$persistent->get('roleid')])) {
|
||||
$errors["roleoverride_roleid[{$id}]"] = get_string('duplicaterole');
|
||||
}
|
||||
$seenroleids[$persistent->get('roleid')] = true;
|
||||
|
||||
$errors = array_merge($errors, $persistent->get_errors());
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load in existing data as form defaults. Usually new entry defaults are stored directly in
|
||||
* form definition (new entry form); this function is used to load in data where values
|
||||
* already exist and data is being edited (edit entry form).
|
||||
*
|
||||
* @param stdClass $data
|
||||
*/
|
||||
public function set_data($data) {
|
||||
$purpose = $this->get_persistent();
|
||||
|
||||
$count = 0;
|
||||
foreach ($this->existingoverrides as $override) {
|
||||
$overridedata = $this->convert_existing_data_to_values($override->to_record());
|
||||
foreach ($overridedata as $key => $value) {
|
||||
$keyname = "roleoverride_{$key}[{$count}]";
|
||||
$data->$keyname = $value;
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
|
||||
parent::set_data($data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
use coding_exception;
|
||||
use moodle_exception;
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\data_request;
|
||||
|
||||
/**
|
||||
* Class containing helper functions for the data privacy tool.
|
||||
@@ -44,9 +45,15 @@ class helper {
|
||||
/** Filter constant associated with the request status filter. */
|
||||
const FILTER_STATUS = 2;
|
||||
|
||||
/** Filter constant associated with the request creation filter. */
|
||||
const FILTER_CREATION = 3;
|
||||
|
||||
/** The request filters preference key. */
|
||||
const PREF_REQUEST_FILTERS = 'tool_dataprivacy_request-filters';
|
||||
|
||||
/** The number of data request records per page preference key. */
|
||||
const PREF_REQUEST_PERPAGE = 'tool_dataprivacy_request-perpage';
|
||||
|
||||
/**
|
||||
* Retrieves the human-readable text value of a data request type.
|
||||
*
|
||||
@@ -142,6 +149,34 @@ class helper {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the human-readable value of a data request creation method.
|
||||
*
|
||||
* @param int $creation The request creation method.
|
||||
* @return string
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function get_request_creation_method_string($creation) {
|
||||
$creationmethods = self::get_request_creation_methods();
|
||||
if (!isset($creationmethods[$creation])) {
|
||||
throw new moodle_exception('errorinvalidrequestcreationmethod', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
return $creationmethods[$creation];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key value-pairs of request creation method code and string value.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_request_creation_methods() {
|
||||
return [
|
||||
data_request::DATAREQUEST_CREATION_MANUAL => get_string('creationmanual', 'tool_dataprivacy'),
|
||||
data_request::DATAREQUEST_CREATION_AUTO => get_string('creationauto', 'tool_dataprivacy'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the users that a user can make data request for.
|
||||
*
|
||||
@@ -196,6 +231,10 @@ class helper {
|
||||
'name' => get_string('requeststatus', 'tool_dataprivacy'),
|
||||
'options' => self::get_request_statuses()
|
||||
],
|
||||
self::FILTER_CREATION => (object)[
|
||||
'name' => get_string('requestcreation', 'tool_dataprivacy'),
|
||||
'options' => self::get_request_creation_methods()
|
||||
],
|
||||
];
|
||||
$options = [];
|
||||
foreach ($filters as $category => $filtercategory) {
|
||||
|
||||
@@ -76,6 +76,31 @@ class metadata_registry {
|
||||
if (isset($contributedplugins[$plugintype][$shortname])) {
|
||||
$internaldata['external'] = true;
|
||||
}
|
||||
|
||||
// Additional interface checks.
|
||||
if (!$manager->is_empty_subsystem($component)) {
|
||||
$classname = $manager->get_provider_classname_for_component($component);
|
||||
if (class_exists($classname)) {
|
||||
$componentclass = new $classname();
|
||||
// Check if the interface is deprecated.
|
||||
if ($componentclass instanceof \core_privacy\local\deprecated) {
|
||||
$internaldata['deprecated'] = true;
|
||||
}
|
||||
|
||||
// Check that the core_userlist_provider is implemented for all user data providers.
|
||||
if ($componentclass instanceof \core_privacy\local\request\core_user_data_provider
|
||||
&& !$componentclass instanceof \core_privacy\local\request\core_userlist_provider) {
|
||||
$internaldata['userlistnoncompliance'] = true;
|
||||
}
|
||||
|
||||
// Check that any type of userlist_provider is implemented for all shared data providers.
|
||||
if ($componentclass instanceof \core_privacy\local\request\shared_data_provider
|
||||
&& !$componentclass instanceof \core_privacy\local\request\userlist_provider) {
|
||||
$internaldata['userlistnoncompliance'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $internaldata;
|
||||
}, $leaves['plugins']);
|
||||
$fullyrichtree[$branch]['plugin_type_raw'] = $plugintype;
|
||||
|
||||
@@ -426,7 +426,7 @@ class data_registry_page implements renderable, templatable {
|
||||
/**
|
||||
* From a list of purpose persistents to a list of id => name purposes.
|
||||
*
|
||||
* @param \tool_dataprivacy\purpose $purposes
|
||||
* @param \tool_dataprivacy\purpose[] $purposes
|
||||
* @param bool $includenotset
|
||||
* @param bool $includeinherit
|
||||
* @return string[]
|
||||
@@ -443,7 +443,7 @@ class data_registry_page implements renderable, templatable {
|
||||
/**
|
||||
* From a list of category persistents to a list of id => name categories.
|
||||
*
|
||||
* @param \tool_dataprivacy\category $categories
|
||||
* @param \tool_dataprivacy\category[] $categories
|
||||
* @param bool $includenotset
|
||||
* @param bool $includeinherit
|
||||
* @return string[]
|
||||
|
||||
@@ -86,7 +86,7 @@ class data_requests_page implements renderable, templatable {
|
||||
$data->filter = $filter->export_for_template($output);
|
||||
|
||||
ob_start();
|
||||
$this->table->out(helper::DEFAULT_PAGE_SIZE, true);
|
||||
$this->table->out($this->table->get_requests_per_page(), true);
|
||||
$requests = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
|
||||
@@ -62,24 +62,41 @@ class data_requests_table extends table_sql {
|
||||
/** @var \tool_dataprivacy\data_request[] Array of data request persistents. */
|
||||
protected $datarequests = [];
|
||||
|
||||
/** @var \stdClass[] List of userids and whether they have any ongoing active requests. */
|
||||
protected $ongoingrequests = [];
|
||||
|
||||
/** @var int The number of data request to be displayed per page. */
|
||||
protected $perpage;
|
||||
|
||||
/** @var int[] The available options for the number of data request to be displayed per page. */
|
||||
protected $perpageoptions = [25, 50, 100, 250];
|
||||
|
||||
/**
|
||||
* data_requests_table constructor.
|
||||
*
|
||||
* @param int $userid The user ID
|
||||
* @param int[] $statuses
|
||||
* @param int[] $types
|
||||
* @param int[] $creationmethods
|
||||
* @param bool $manage
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function __construct($userid = 0, $statuses = [], $types = [], $manage = false) {
|
||||
public function __construct($userid = 0, $statuses = [], $types = [], $creationmethods = [], $manage = false) {
|
||||
parent::__construct('data-requests-table');
|
||||
|
||||
$this->userid = $userid;
|
||||
$this->statuses = $statuses;
|
||||
$this->types = $types;
|
||||
$this->creationmethods = $creationmethods;
|
||||
$this->manage = $manage;
|
||||
|
||||
$checkboxattrs = [
|
||||
'title' => get_string('selectall'),
|
||||
'data-action' => 'selectall'
|
||||
];
|
||||
|
||||
$columnheaders = [
|
||||
'select' => html_writer::checkbox('selectall', 1, false, null, $checkboxattrs),
|
||||
'type' => get_string('requesttype', 'tool_dataprivacy'),
|
||||
'userid' => get_string('user', 'tool_dataprivacy'),
|
||||
'timecreated' => get_string('daterequested', 'tool_dataprivacy'),
|
||||
@@ -91,7 +108,26 @@ class data_requests_table extends table_sql {
|
||||
|
||||
$this->define_columns(array_keys($columnheaders));
|
||||
$this->define_headers(array_values($columnheaders));
|
||||
$this->no_sorting('actions');
|
||||
$this->no_sorting('select', 'actions');
|
||||
}
|
||||
|
||||
/**
|
||||
* The select column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_select($data) {
|
||||
if ($data->status == \tool_dataprivacy\api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
|
||||
$stringdata = [
|
||||
'username' => $data->foruser->fullname,
|
||||
'requesttype' => \core_text::strtolower($data->typenameshort)
|
||||
];
|
||||
|
||||
return \html_writer::checkbox('requestids[]', $data->id, false, '',
|
||||
['class' => 'selectrequests', 'title' => get_string('selectuserdatarequest',
|
||||
'tool_dataprivacy', $stringdata)]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,6 +252,20 @@ class data_requests_table extends table_sql {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($this->manage) {
|
||||
$persistent = $this->datarequests[$requestid];
|
||||
$canreset = $persistent->is_active() || empty($this->ongoingrequests[$data->foruser->id]->{$data->type});
|
||||
$canreset = $canreset && $persistent->is_resettable();
|
||||
if ($canreset) {
|
||||
$reseturl = new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', [
|
||||
'requestid' => $requestid,
|
||||
]);
|
||||
$actiondata = ['data-action' => 'reset', 'data-requestid' => $requestid];
|
||||
$actiontext = get_string('resubmitrequestasnew', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($reseturl, null, $actiontext, $actiondata);
|
||||
}
|
||||
}
|
||||
|
||||
$actionsmenu = new action_menu($actions);
|
||||
$actionsmenu->set_menu_trigger(get_string('actions'));
|
||||
$actionsmenu->set_owner_selector('request-actions-' . $requestid);
|
||||
@@ -242,23 +292,31 @@ class data_requests_table extends table_sql {
|
||||
$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());
|
||||
$datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types,
|
||||
$this->creationmethods, $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);
|
||||
$total = api::get_data_requests_count($this->userid, $this->statuses, $this->types,
|
||||
$this->creationmethods);
|
||||
$this->pagesize($pagesize, $total);
|
||||
|
||||
$this->rawdata = [];
|
||||
$context = \context_system::instance();
|
||||
$renderer = $PAGE->get_renderer('tool_dataprivacy');
|
||||
|
||||
$forusers = [];
|
||||
foreach ($datarequests as $persistent) {
|
||||
$this->datarequests[$persistent->get('id')] = $persistent;
|
||||
$exporter = new data_request_exporter($persistent, ['context' => $context]);
|
||||
$this->rawdata[] = $exporter->export($renderer);
|
||||
$forusers[] = $persistent->get('userid');
|
||||
}
|
||||
|
||||
// Fetch the list of all ongoing requests for the users currently shown.
|
||||
// This is used to determine whether any non-active request can be resubmitted.
|
||||
// There can only be one ongoing request of a type for each user.
|
||||
$this->ongoingrequests = api::find_ongoing_request_types_for_users($forusers);
|
||||
|
||||
// Set initial bars.
|
||||
if ($useinitialsbar) {
|
||||
$this->initialbars($total > $pagesize);
|
||||
@@ -290,4 +348,72 @@ class data_requests_table extends table_sql {
|
||||
protected function show_hide_link($column, $index) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the table's wrap_html_finish method in order to render the bulk actions and
|
||||
* records per page options.
|
||||
*/
|
||||
public function wrap_html_finish() {
|
||||
global $OUTPUT;
|
||||
|
||||
$data = new stdClass();
|
||||
$data->options = [
|
||||
[
|
||||
'value' => 0,
|
||||
'name' => ''
|
||||
],
|
||||
[
|
||||
'value' => \tool_dataprivacy\api::DATAREQUEST_ACTION_APPROVE,
|
||||
'name' => get_string('approve', 'tool_dataprivacy')
|
||||
],
|
||||
[
|
||||
'value' => \tool_dataprivacy\api::DATAREQUEST_ACTION_REJECT,
|
||||
'name' => get_string('deny', 'tool_dataprivacy')
|
||||
]
|
||||
];
|
||||
|
||||
$perpageoptions = array_combine($this->perpageoptions, $this->perpageoptions);
|
||||
$perpageselect = new \single_select(new moodle_url(''), 'perpage',
|
||||
$perpageoptions, get_user_preferences('tool_dataprivacy_request-perpage'), null, 'selectgroup');
|
||||
$perpageselect->label = get_string('perpage', 'moodle');
|
||||
$data->perpage = $OUTPUT->render($perpageselect);
|
||||
|
||||
echo $OUTPUT->render_from_template('tool_dataprivacy/data_requests_bulk_actions', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the number of data request records to be displayed per page.
|
||||
*
|
||||
* @param int $perpage The number of data request records.
|
||||
*/
|
||||
public function set_requests_per_page(int $perpage) {
|
||||
$this->perpage = $perpage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of data request records to be displayed per page.
|
||||
*
|
||||
* @return int The number of data request records.
|
||||
*/
|
||||
public function get_requests_per_page() : int {
|
||||
return $this->perpage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the available options for the number of data request to be displayed per page.
|
||||
*
|
||||
* @param array $perpageoptions The available options for the number of data request to be displayed per page.
|
||||
*/
|
||||
public function set_requests_per_page_options(array $perpageoptions) {
|
||||
$this->$perpageoptions = $perpageoptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available options for the number of data request to be displayed per page.
|
||||
*
|
||||
* @return array The available options for the number of data request to be displayed per page.
|
||||
*/
|
||||
public function get_requests_per_page_options() : array {
|
||||
return $this->perpageoptions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<?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 data for the data registry defaults.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace tool_dataprivacy\output;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use action_menu_link_primary;
|
||||
use coding_exception;
|
||||
use moodle_exception;
|
||||
use moodle_url;
|
||||
use renderable;
|
||||
use renderer_base;
|
||||
use stdClass;
|
||||
use templatable;
|
||||
use tool_dataprivacy\data_registry;
|
||||
use tool_dataprivacy\external\category_exporter;
|
||||
use tool_dataprivacy\external\purpose_exporter;
|
||||
|
||||
/**
|
||||
* Class containing data for the data registry defaults.
|
||||
*
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class defaults_page implements renderable, templatable {
|
||||
|
||||
/** @var int $mode The display mode. */
|
||||
protected $mode = null;
|
||||
|
||||
/** @var int $category The default category for the given mode. */
|
||||
protected $category = null;
|
||||
|
||||
/** @var int $purpose The default purpose for the given mode. */
|
||||
protected $purpose = null;
|
||||
|
||||
/** @var stdClass[] $otherdefaults Other defaults for the given mode. */
|
||||
protected $otherdefaults = [];
|
||||
|
||||
/** @var bool $canedit Whether editing is allowed. */
|
||||
protected $canedit = false;
|
||||
|
||||
/**
|
||||
* Construct this renderable.
|
||||
*
|
||||
* @param int $mode The display mode.
|
||||
* @param int $category The default category for the given mode.
|
||||
* @param int $purpose The default purpose for the given mode.
|
||||
* @param stdClass[] $otherdefaults Other defaults for the given mode.
|
||||
* @param bool $canedit Whether editing is allowed.
|
||||
*/
|
||||
public function __construct($mode, $category, $purpose, $otherdefaults = [], $canedit = false) {
|
||||
$this->mode = $mode;
|
||||
$this->category = $category;
|
||||
$this->purpose = $purpose;
|
||||
$this->otherdefaults = $otherdefaults;
|
||||
$this->canedit = $canedit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export this data so it can be used as the context for a mustache template.
|
||||
*
|
||||
* @param renderer_base $output
|
||||
* @return stdClass
|
||||
* @throws coding_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public function export_for_template(renderer_base $output) {
|
||||
$data = new stdClass();
|
||||
|
||||
// Set tab URLs.
|
||||
$coursecaturl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSECAT]);
|
||||
$courseurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSE]);
|
||||
$moduleurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_MODULE]);
|
||||
$blockurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_BLOCK]);
|
||||
$data->coursecaturl = $coursecaturl;
|
||||
$data->courseurl = $courseurl;
|
||||
$data->moduleurl = $moduleurl;
|
||||
$data->blockurl = $blockurl;
|
||||
|
||||
// Set display mode.
|
||||
switch ($this->mode) {
|
||||
case CONTEXT_COURSECAT:
|
||||
$data->modecoursecat = true;
|
||||
break;
|
||||
case CONTEXT_COURSE:
|
||||
$data->modecourse = true;
|
||||
break;
|
||||
case CONTEXT_MODULE:
|
||||
$data->modemodule = true;
|
||||
break;
|
||||
case CONTEXT_BLOCK:
|
||||
$data->modeblock = true;
|
||||
break;
|
||||
default:
|
||||
$data->modecoursecat = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Set config variables.
|
||||
$configname = \context_helper::get_class_for_level($this->mode);
|
||||
list($purposevar, $categoryvar) = data_registry::var_names_from_context($configname);
|
||||
$data->categoryvar = $categoryvar;
|
||||
$data->purposevar = $purposevar;
|
||||
|
||||
// Set default category.
|
||||
$data->categoryid = $this->category;
|
||||
$data->category = category_exporter::get_name($this->category);
|
||||
|
||||
// Set default purpose.
|
||||
$data->purposeid = $this->purpose;
|
||||
$data->purpose = purpose_exporter::get_name($this->purpose);
|
||||
|
||||
// Set other defaults.
|
||||
$otherdefaults = [];
|
||||
$url = new moodle_url('#');
|
||||
foreach ($this->otherdefaults as $pluginname => $values) {
|
||||
$defaults = [
|
||||
'name' => $values->name,
|
||||
'category' => category_exporter::get_name($values->category),
|
||||
'purpose' => purpose_exporter::get_name($values->purpose),
|
||||
];
|
||||
if ($this->canedit) {
|
||||
$actions = [];
|
||||
// Edit link.
|
||||
$editattrs = [
|
||||
'data-action' => 'edit-activity-defaults',
|
||||
'data-contextlevel' => $this->mode,
|
||||
'data-activityname' => $pluginname,
|
||||
'data-category' => $values->category,
|
||||
'data-purpose' => $values->purpose,
|
||||
];
|
||||
$editlink = new action_menu_link_primary($url, new \pix_icon('t/edit', get_string('edit')),
|
||||
get_string('edit'), $editattrs);
|
||||
$actions[] = $editlink->export_for_template($output);
|
||||
|
||||
// Delete link.
|
||||
$deleteattrs = [
|
||||
'data-action' => 'delete-activity-defaults',
|
||||
'data-contextlevel' => $this->mode,
|
||||
'data-activityname' => $pluginname,
|
||||
'data-activitydisplayname' => $values->name,
|
||||
];
|
||||
$deletelink = new action_menu_link_primary($url, new \pix_icon('t/delete', get_string('delete')),
|
||||
get_string('delete'), $deleteattrs);
|
||||
$actions[] = $deletelink->export_for_template($output);
|
||||
|
||||
$defaults['actions'] = $actions;
|
||||
}
|
||||
$otherdefaults[] = (object)$defaults;
|
||||
}
|
||||
$data->otherdefaults = $otherdefaults;
|
||||
|
||||
$data->canedit = $this->canedit;
|
||||
$data->contextlevel = $this->mode;
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -57,9 +57,15 @@ class expired_contexts_table extends table_sql {
|
||||
*/
|
||||
protected $selectall = true;
|
||||
|
||||
/** @var purpose[] Array of purposes mapped to the contexts. */
|
||||
/** @var purpose[] Array of purposes by their id. */
|
||||
protected $purposes = [];
|
||||
|
||||
/** @var purpose[] Map of context => purpose. */
|
||||
protected $purposemap = [];
|
||||
|
||||
/** @var array List of roles. */
|
||||
protected $roles = [];
|
||||
|
||||
/**
|
||||
* expired_contexts_table constructor.
|
||||
*
|
||||
@@ -77,6 +83,7 @@ class expired_contexts_table extends table_sql {
|
||||
'purpose' => get_string('purpose', 'tool_dataprivacy'),
|
||||
'category' => get_string('category', 'tool_dataprivacy'),
|
||||
'retentionperiod' => get_string('retentionperiod', 'tool_dataprivacy'),
|
||||
'tobedeleted' => get_string('tobedeleted', 'tool_dataprivacy'),
|
||||
'timecreated' => get_string('expiry', 'tool_dataprivacy'),
|
||||
];
|
||||
$checkboxattrs = [
|
||||
@@ -93,21 +100,25 @@ class expired_contexts_table extends table_sql {
|
||||
$this->no_sorting('purpose');
|
||||
$this->no_sorting('category');
|
||||
$this->no_sorting('retentionperiod');
|
||||
$this->no_sorting('tobedeleted');
|
||||
|
||||
// Make this table sorted by first name by default.
|
||||
$this->sortable(true, 'timecreated');
|
||||
|
||||
// We use roles in several places.
|
||||
$this->roles = role_get_names();
|
||||
}
|
||||
|
||||
/**
|
||||
* The context name column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function col_name($data) {
|
||||
public function col_name($expiredctx) {
|
||||
global $OUTPUT;
|
||||
$context = context_helper::instance_by_id($data->contextid);
|
||||
$context = context_helper::instance_by_id($expiredctx->get('contextid'));
|
||||
$parent = $context->get_parent_context();
|
||||
$contextdata = (object)[
|
||||
'name' => $context->get_context_name(false, true),
|
||||
@@ -128,14 +139,14 @@ class expired_contexts_table extends table_sql {
|
||||
/**
|
||||
* The context information column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function col_info($data) {
|
||||
public function col_info($expiredctx) {
|
||||
global $OUTPUT;
|
||||
|
||||
$context = context_helper::instance_by_id($data->contextid);
|
||||
$context = context_helper::instance_by_id($expiredctx->get('contextid'));
|
||||
|
||||
$children = $context->get_child_contexts();
|
||||
if (empty($children)) {
|
||||
@@ -156,13 +167,13 @@ class expired_contexts_table extends table_sql {
|
||||
/**
|
||||
* The category name column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return mixed
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public function col_category($data) {
|
||||
$context = context_helper::instance_by_id($data->contextid);
|
||||
public function col_category($expiredctx) {
|
||||
$context = context_helper::instance_by_id($expiredctx->get('contextid'));
|
||||
$category = api::get_effective_context_category($context);
|
||||
|
||||
return s($category->get('name'));
|
||||
@@ -171,12 +182,12 @@ class expired_contexts_table extends table_sql {
|
||||
/**
|
||||
* The purpose column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function col_purpose($data) {
|
||||
$purpose = $this->purposes[$data->contextid];
|
||||
public function col_purpose($expiredctx) {
|
||||
$purpose = $this->get_purpose_for_expiry($expiredctx);
|
||||
|
||||
return s($purpose->get('name'));
|
||||
}
|
||||
@@ -184,42 +195,114 @@ class expired_contexts_table extends table_sql {
|
||||
/**
|
||||
* The retention period column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function col_retentionperiod($data) {
|
||||
global $PAGE;
|
||||
public function col_retentionperiod($expiredctx) {
|
||||
$purpose = $this->get_purpose_for_expiry($expiredctx);
|
||||
|
||||
$purpose = $this->purposes[$data->contextid];
|
||||
$expiries = [];
|
||||
|
||||
$exporter = new purpose_exporter($purpose, ['context' => \context_system::instance()]);
|
||||
$exportedpurpose = $exporter->export($PAGE->get_renderer('core'));
|
||||
$expiry = html_writer::tag('dt', get_string('default'), ['class' => 'col-sm-3']);
|
||||
if ($expiredctx->get('defaultexpired')) {
|
||||
$expiries[get_string('default')] = get_string('expiredrolewithretention', 'tool_dataprivacy', (object) [
|
||||
'retention' => api::format_retention_period(new \DateInterval($purpose->get('retentionperiod'))),
|
||||
]);
|
||||
} else {
|
||||
$expiries[get_string('default')] = get_string('unexpiredrolewithretention', 'tool_dataprivacy', (object) [
|
||||
'retention' => api::format_retention_period(new \DateInterval($purpose->get('retentionperiod'))),
|
||||
]);
|
||||
}
|
||||
|
||||
return $exportedpurpose->formattedretentionperiod;
|
||||
if (!$expiredctx->is_fully_expired()) {
|
||||
$purposeoverrides = $purpose->get_purpose_overrides();
|
||||
|
||||
foreach ($expiredctx->get('unexpiredroles') as $roleid) {
|
||||
$role = $this->roles[$roleid];
|
||||
$override = $purposeoverrides[$roleid];
|
||||
|
||||
$expiries[$role->localname] = get_string('unexpiredrolewithretention', 'tool_dataprivacy', (object) [
|
||||
'retention' => api::format_retention_period(new \DateInterval($override->get('retentionperiod'))),
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($expiredctx->get('expiredroles') as $roleid) {
|
||||
$role = $this->roles[$roleid];
|
||||
$override = $purposeoverrides[$roleid];
|
||||
|
||||
$expiries[$role->localname] = get_string('expiredrolewithretention', 'tool_dataprivacy', (object) [
|
||||
'retention' => api::format_retention_period(new \DateInterval($override->get('retentionperiod'))),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$output = array_map(function($rolename, $expiry) {
|
||||
$return = html_writer::tag('dt', $rolename, ['class' => 'col-sm-3']);
|
||||
$return .= html_writer::tag('dd', $expiry, ['class' => 'col-sm-9']);
|
||||
|
||||
return $return;
|
||||
}, array_keys($expiries), $expiries);
|
||||
|
||||
return html_writer::tag('dl', implode($output), ['class' => 'row']);
|
||||
}
|
||||
|
||||
/**
|
||||
* The timecreated a.k.a. the context expiry date column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_timecreated($data) {
|
||||
return userdate($data->timecreated);
|
||||
public function col_timecreated($expiredctx) {
|
||||
return userdate($expiredctx->get('timecreated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the select column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_select($data) {
|
||||
$id = $data->id;
|
||||
public function col_select($expiredctx) {
|
||||
$id = $expiredctx->get('id');
|
||||
return html_writer::checkbox('expiredcontext_' . $id, $id, $this->selectall, '', ['class' => 'selectcontext']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatting for the 'tobedeleted' column which indicates in a friendlier fashion whose data will be removed.
|
||||
*
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_tobedeleted($expiredctx) {
|
||||
if ($expiredctx->is_fully_expired()) {
|
||||
return get_string('defaultexpired', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
$purpose = $this->get_purpose_for_expiry($expiredctx);
|
||||
|
||||
$a = (object) [];
|
||||
|
||||
$expiredroles = [];
|
||||
foreach ($expiredctx->get('expiredroles') as $roleid) {
|
||||
$expiredroles[] = html_writer::tag('li', $this->roles[$roleid]->localname);
|
||||
}
|
||||
$a->expired = html_writer::tag('ul', implode($expiredroles));
|
||||
|
||||
$unexpiredroles = [];
|
||||
foreach ($expiredctx->get('unexpiredroles') as $roleid) {
|
||||
$unexpiredroles[] = html_writer::tag('li', $this->roles[$roleid]->localname);
|
||||
}
|
||||
$a->unexpired = html_writer::tag('ul', implode($unexpiredroles));
|
||||
|
||||
if ($expiredctx->get('defaultexpired')) {
|
||||
return get_string('defaultexpiredexcept', 'tool_dataprivacy', $a);
|
||||
} else if (empty($unexpiredroles)) {
|
||||
return get_string('defaultunexpired', 'tool_dataprivacy', $a);
|
||||
} else {
|
||||
return get_string('defaultunexpiredwithexceptions', 'tool_dataprivacy', $a);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the database for results to display in the table.
|
||||
*
|
||||
@@ -241,17 +324,16 @@ class expired_contexts_table extends table_sql {
|
||||
// Only load expired contexts that are awaiting confirmation.
|
||||
$expiredcontexts = expired_context::get_records_by_contextlevel($this->contextlevel, expired_context::STATUS_EXPIRED,
|
||||
$sort, $this->get_page_start(), $this->get_page_size());
|
||||
|
||||
$this->rawdata = [];
|
||||
$contextids = [];
|
||||
foreach ($expiredcontexts as $persistent) {
|
||||
$data = $persistent->to_record();
|
||||
|
||||
$context = context_helper::instance_by_id($data->contextid);
|
||||
|
||||
$purpose = api::get_effective_context_purpose($context);
|
||||
$this->purposes[$data->contextid] = $purpose;
|
||||
$this->rawdata[] = $data;
|
||||
$this->rawdata[] = $persistent;
|
||||
$contextids[] = $persistent->get('contextid');
|
||||
}
|
||||
|
||||
$this->preload_contexts($contextids);
|
||||
|
||||
// Set initial bars.
|
||||
if ($useinitialsbar) {
|
||||
$this->initialbars($total > $pagesize);
|
||||
@@ -281,4 +363,48 @@ class expired_contexts_table extends table_sql {
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the purpose for the specified expired context.
|
||||
*
|
||||
* @param expired_context $expiredcontext
|
||||
* @return purpose
|
||||
*/
|
||||
protected function get_purpose_for_expiry(expired_context $expiredcontext) : purpose {
|
||||
$context = context_helper::instance_by_id($expiredcontext->get('contextid'));
|
||||
|
||||
if (empty($this->purposemap[$context->id])) {
|
||||
$purpose = api::get_effective_context_purpose($context);
|
||||
$this->purposemap[$context->id] = $purpose->get('id');
|
||||
|
||||
if (empty($this->purposes[$purpose->get('id')])) {
|
||||
$this->purposes[$purpose->get('id')] = $purpose;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->purposes[$this->purposemap[$context->id]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload context records given a set of contextids.
|
||||
*
|
||||
* @param array $contextids
|
||||
*/
|
||||
protected function preload_contexts(array $contextids) {
|
||||
global $DB;
|
||||
|
||||
if (empty($contextids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ctxfields = \context_helper::get_preload_record_columns_sql('ctx');
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
|
||||
$sql = "SELECT {$ctxfields} FROM {context} ctx WHERE ctx.id {$insql}";
|
||||
$contextlist = $DB->get_recordset_sql($sql, $inparams);
|
||||
foreach ($contextlist as $contextdata) {
|
||||
\context_helper::preload_from_record($contextdata);
|
||||
}
|
||||
$contextlist->close();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,4 +139,15 @@ class renderer extends plugin_renderer_base {
|
||||
$data = $page->export_for_template($this);
|
||||
return parent::render_from_template('tool_dataprivacy/data_deletion', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the user data retention summary page.
|
||||
*
|
||||
* @param summary_page $page
|
||||
* @return string html for the page.
|
||||
*/
|
||||
public function render_summary_page(summary_page $page) {
|
||||
$data = $page->export_for_template($this);
|
||||
return parent::render_from_template('tool_dataprivacy/summary', $data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Summary page renderable.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Adrian Greeve
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace tool_dataprivacy\output;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use renderable;
|
||||
use renderer_base;
|
||||
use templatable;
|
||||
|
||||
|
||||
/**
|
||||
* Class containing the summary page renderable.
|
||||
*
|
||||
* @copyright 2018 Adrian Greeve
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class summary_page implements renderable, templatable {
|
||||
|
||||
/**
|
||||
* Export this data so it can be used as the context for a mustache template.
|
||||
*
|
||||
* @param renderer_base $output
|
||||
* @return array
|
||||
*/
|
||||
public function export_for_template(renderer_base $output) {
|
||||
$contextlevels = [
|
||||
'contextlevelname10' => CONTEXT_SYSTEM,
|
||||
'contextlevelname30' => CONTEXT_USER,
|
||||
'contextlevelname40' => CONTEXT_COURSECAT,
|
||||
'contextlevelname50' => CONTEXT_COURSE,
|
||||
'contextlevelname70' => CONTEXT_MODULE,
|
||||
'contextlevelname80' => CONTEXT_BLOCK
|
||||
];
|
||||
|
||||
$data = [];
|
||||
$context = \context_system::instance();
|
||||
|
||||
foreach ($contextlevels as $levelname => $level) {
|
||||
$classname = \context_helper::get_class_for_level($level);
|
||||
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
|
||||
$purposeid = get_config('tool_dataprivacy', $purposevar);
|
||||
$categoryid = get_config('tool_dataprivacy', $categoryvar);
|
||||
|
||||
$section = [];
|
||||
$section['contextname'] = get_string($levelname, 'tool_dataprivacy');
|
||||
|
||||
if (empty($purposeid)) {
|
||||
list($purposeid, $categoryid) =
|
||||
\tool_dataprivacy\data_registry::get_effective_default_contextlevel_purpose_and_category($level);
|
||||
}
|
||||
if ($purposeid == -1) {
|
||||
$purposeid = 0;
|
||||
}
|
||||
$purpose = new \tool_dataprivacy\purpose($purposeid);
|
||||
$export = new \tool_dataprivacy\external\purpose_exporter($purpose, ['context' => $context]);
|
||||
$purposedata = $export->export($output);
|
||||
$section['purpose'] = $purposedata;
|
||||
|
||||
if (empty($categoryid)) {
|
||||
list($purposeid, $categoryid) =
|
||||
\tool_dataprivacy\data_registry::get_effective_default_contextlevel_purpose_and_category($level);
|
||||
}
|
||||
if ($categoryid == -1) {
|
||||
$categoryid = 0;
|
||||
}
|
||||
$category = new \tool_dataprivacy\category($categoryid);
|
||||
$export = new \tool_dataprivacy\external\category_exporter($category, ['context' => $context]);
|
||||
$categorydata = $export->export($output);
|
||||
$section['category'] = $categorydata;
|
||||
$data['contexts'][] = $section;
|
||||
}
|
||||
|
||||
// Get activity module plugin info.
|
||||
$pluginmanager = \core_plugin_manager::instance();
|
||||
$modplugins = $pluginmanager->get_enabled_plugins('mod');
|
||||
|
||||
foreach ($modplugins as $name) {
|
||||
$classname = \context_helper::get_class_for_level($contextlevels['contextlevelname70']);
|
||||
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname, $name);
|
||||
$categoryid = get_config('tool_dataprivacy', $categoryvar);
|
||||
$purposeid = get_config('tool_dataprivacy', $purposevar);
|
||||
if ($categoryid === false && $purposeid === false) {
|
||||
// If no purpose and category has been set for this plugin, then there's no need to show this on the list.
|
||||
continue;
|
||||
}
|
||||
|
||||
$section = [];
|
||||
$section['contextname'] = $pluginmanager->plugin_name('mod_' . $name);
|
||||
|
||||
if ($purposeid == -1) {
|
||||
$purposeid = 0;
|
||||
}
|
||||
$purpose = new \tool_dataprivacy\purpose($purposeid);
|
||||
$export = new \tool_dataprivacy\external\purpose_exporter($purpose, ['context' => $context]);
|
||||
$purposedata = $export->export($output);
|
||||
$section['purpose'] = $purposedata;
|
||||
|
||||
if ($categoryid == -1) {
|
||||
$categoryid = 0;
|
||||
}
|
||||
$category = new \tool_dataprivacy\category($categoryid);
|
||||
$export = new \tool_dataprivacy\external\category_exporter($category, ['context' => $context]);
|
||||
$categorydata = $export->export($output);
|
||||
$section['category'] = $categorydata;
|
||||
|
||||
$data['contexts'][] = $section;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ class page_helper {
|
||||
*/
|
||||
public static function setup(moodle_url $url, $title, $attachtoparentnode = '',
|
||||
$requiredcapability = 'tool/dataprivacy:managedataregistry') {
|
||||
global $PAGE;
|
||||
global $PAGE, $SITE;
|
||||
|
||||
$context = context_system::instance();
|
||||
|
||||
@@ -65,7 +65,7 @@ class page_helper {
|
||||
$PAGE->set_context($context);
|
||||
$PAGE->set_pagelayout('admin');
|
||||
$PAGE->set_title($title);
|
||||
$PAGE->set_heading($title);
|
||||
$PAGE->set_heading($SITE->fullname);
|
||||
|
||||
// If necessary, override the settings navigation to add this page into the breadcrumb navigation.
|
||||
if ($attachtoparentnode) {
|
||||
|
||||
@@ -30,9 +30,11 @@ use context;
|
||||
use context_user;
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\helper;
|
||||
use core_privacy\local\request\transform;
|
||||
use \core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
use dml_exception;
|
||||
use stdClass;
|
||||
@@ -50,6 +52,9 @@ class provider implements
|
||||
// This tool stores user data.
|
||||
\core_privacy\local\metadata\provider,
|
||||
|
||||
// This plugin is capable of determining which users have data within it.
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
|
||||
// This tool may provide access to and deletion of user data.
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
|
||||
@@ -76,6 +81,8 @@ class provider implements
|
||||
|
||||
$collection->add_user_preference(tool_helper::PREF_REQUEST_FILTERS,
|
||||
'privacy:metadata:preference:tool_dataprivacy_request-filters');
|
||||
$collection->add_user_preference(tool_helper::PREF_REQUEST_PERPAGE,
|
||||
'privacy:metadata:preference:tool_dataprivacy_request-perpage');
|
||||
|
||||
return $collection;
|
||||
}
|
||||
@@ -98,6 +105,32 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_user::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'contextlevel' => CONTEXT_USER,
|
||||
'contextid' => $context->id,
|
||||
];
|
||||
|
||||
$sql = "SELECT instanceid AS userid
|
||||
FROM {context}
|
||||
WHERE id = :contextid
|
||||
AND contextlevel = :contextlevel";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -134,6 +167,8 @@ class provider implements
|
||||
$data->type = tool_helper::get_shortened_request_type_string($record->type);
|
||||
// Status.
|
||||
$data->status = tool_helper::get_request_status_string($record->status);
|
||||
// Creation method.
|
||||
$data->creationmethod = tool_helper::get_request_creation_method_string($record->creationmethod);
|
||||
// Comments.
|
||||
$data->comments = $record->comments;
|
||||
// The DPO's comment about this request.
|
||||
@@ -170,6 +205,15 @@ class provider implements
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user preferences for the plugin.
|
||||
*
|
||||
@@ -192,6 +236,10 @@ class provider implements
|
||||
$option->category = get_string('requeststatus', 'tool_dataprivacy');
|
||||
$option->name = tool_helper::get_request_status_string($value);
|
||||
break;
|
||||
case tool_helper::FILTER_CREATION:
|
||||
$option->category = get_string('requestcreation', 'tool_dataprivacy');
|
||||
$option->name = tool_helper::get_request_creation_method_string($value);
|
||||
break;
|
||||
}
|
||||
$descriptions[] = get_string('filteroption', 'tool_dataprivacy', $option);
|
||||
}
|
||||
@@ -200,5 +248,11 @@ class provider implements
|
||||
$descriptionstext = implode(', ', $descriptions);
|
||||
writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_FILTERS, $values, $descriptionstext);
|
||||
}
|
||||
|
||||
$prefperpage = get_user_preferences(tool_helper::PREF_REQUEST_PERPAGE, null, $userid);
|
||||
if ($prefperpage !== null) {
|
||||
writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_PERPAGE, $prefperpage,
|
||||
get_string('privacy:metadata:preference:tool_dataprivacy_request-perpage', 'tool_dataprivacy'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,6 @@ class purpose extends \core\persistent {
|
||||
* @return null
|
||||
*/
|
||||
public function is_used() {
|
||||
|
||||
if (\tool_dataprivacy\contextlevel::is_purpose_used($this->get('id')) ||
|
||||
\tool_dataprivacy\context_instance::is_purpose_used($this->get('id'))) {
|
||||
return true;
|
||||
@@ -180,4 +179,13 @@ class purpose extends \core\persistent {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of the role purpose overrides for this purpose.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_purpose_overrides() : array {
|
||||
return purpose_override::get_overrides_for_purpose($this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<?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 for loading/storing data purpose overrides from the DB.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace tool_dataprivacy;
|
||||
|
||||
use stdClass;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
/**
|
||||
* Class for loading/storing data purpose overrides from the DB.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class purpose_override extends \core\persistent {
|
||||
|
||||
/**
|
||||
* Database table.
|
||||
*/
|
||||
const TABLE = 'tool_dataprivacy_purposerole';
|
||||
|
||||
/**
|
||||
* Return the definition of the properties of this model.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function define_properties() {
|
||||
return array(
|
||||
'purposeid' => array(
|
||||
'type' => PARAM_INT,
|
||||
'description' => 'The purpose that that this override relates to',
|
||||
),
|
||||
'roleid' => array(
|
||||
'type' => PARAM_INT,
|
||||
'description' => 'The role that that this override relates to',
|
||||
),
|
||||
'lawfulbases' => array(
|
||||
'type' => PARAM_TEXT,
|
||||
'description' => 'Comma-separated IDs matching records in tool_dataprivacy_lawfulbasis.',
|
||||
'null' => NULL_ALLOWED,
|
||||
'default' => null,
|
||||
),
|
||||
'sensitivedatareasons' => array(
|
||||
'type' => PARAM_TEXT,
|
||||
'description' => 'Comma-separated IDs matching records in tool_dataprivacy_sensitive',
|
||||
'null' => NULL_ALLOWED,
|
||||
'default' => null,
|
||||
),
|
||||
'retentionperiod' => array(
|
||||
'type' => PARAM_ALPHANUM,
|
||||
'description' => 'Retention period. ISO_8601 durations format (as in DateInterval format).',
|
||||
'default' => '',
|
||||
),
|
||||
'protected' => array(
|
||||
'type' => PARAM_INT,
|
||||
'description' => 'Data retention with higher precedent over user\'s request to be forgotten.',
|
||||
'default' => '0',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all role overrides for the purpose.
|
||||
*
|
||||
* @param purpose $purpose
|
||||
* @return array
|
||||
*/
|
||||
public static function get_overrides_for_purpose(purpose $purpose) : array {
|
||||
$cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
|
||||
|
||||
$overrides = [];
|
||||
$alldata = $cache->get($purpose->get('id'));
|
||||
if (false === $alldata) {
|
||||
$tocache = [];
|
||||
foreach (self::get_records(['purposeid' => $purpose->get('id')]) as $override) {
|
||||
$tocache[] = $override->to_record();
|
||||
$overrides[$override->get('roleid')] = $override;
|
||||
}
|
||||
$cache->set($purpose->get('id'), $tocache);
|
||||
} else {
|
||||
foreach ($alldata as $data) {
|
||||
$override = new self(0, $data);
|
||||
$overrides[$override->get('roleid')] = $override;
|
||||
}
|
||||
}
|
||||
|
||||
return $overrides;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the new record to the cache.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
protected function after_create() {
|
||||
$cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
|
||||
$cache->delete($this->get('purposeid'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cache record.
|
||||
*
|
||||
* @param bool $result
|
||||
* @return null
|
||||
*/
|
||||
protected function after_update($result) {
|
||||
$cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
|
||||
$cache->delete($this->get('purposeid'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes unnecessary stuff from db.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
protected function before_delete() {
|
||||
$cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
|
||||
$cache->delete($this->get('purposeid'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?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 create delete data request for pre-existing deleted users.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Mihail Geshoski
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_dataprivacy\task;
|
||||
|
||||
use core\task\scheduled_task;
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\data_request;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
/**
|
||||
* Scheduled task to create delete data request for pre-existing deleted users.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Mihail Geshoski
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class delete_existing_deleted_users extends scheduled_task {
|
||||
|
||||
/**
|
||||
* Returns the task name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name() {
|
||||
return get_string('deleteexistingdeleteduserstask', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the task to delete expired data request files and update request statuses.
|
||||
*
|
||||
*/
|
||||
public function execute() {
|
||||
global $DB;
|
||||
|
||||
// Automatic creation of deletion requests must be enabled.
|
||||
if (get_config('tool_dataprivacy', 'automaticdeletionrequests')) {
|
||||
// Select all deleted users that do not have any delete data requests created for them.
|
||||
$sql = "SELECT DISTINCT(u.id)
|
||||
FROM {user} u
|
||||
LEFT JOIN {tool_dataprivacy_request} r
|
||||
ON u.id = r.userid
|
||||
WHERE u.deleted = ?
|
||||
AND (r.id IS NULL
|
||||
OR r.type != ?)";
|
||||
|
||||
$params = [
|
||||
1,
|
||||
api::DATAREQUEST_TYPE_DELETE
|
||||
];
|
||||
|
||||
$deletedusers = $DB->get_records_sql($sql, $params);
|
||||
$createdrequests = 0;
|
||||
|
||||
foreach ($deletedusers as $user) {
|
||||
api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE,
|
||||
get_string('datarequestcreatedfromscheduledtask', 'tool_dataprivacy'),
|
||||
data_request::DATAREQUEST_CREATION_AUTO);
|
||||
$createdrequests++;
|
||||
}
|
||||
|
||||
if ($createdrequests > 0) {
|
||||
mtrace($createdrequests . ' delete data request(s) created for existing deleted users');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,18 +52,10 @@ class delete_expired_contexts extends scheduled_task {
|
||||
|
||||
/**
|
||||
* Run the task to delete context instances based on their retention periods.
|
||||
*
|
||||
*/
|
||||
public function execute() {
|
||||
$manager = new \tool_dataprivacy\expired_course_related_contexts();
|
||||
$deleted = $manager->delete();
|
||||
if ($deleted > 0) {
|
||||
mtrace($deleted . ' course-related contexts have been deleted');
|
||||
}
|
||||
$manager = new \tool_dataprivacy\expired_user_contexts();
|
||||
$deleted = $manager->delete();
|
||||
if ($deleted > 0) {
|
||||
mtrace($deleted . ' user contexts have been deleted');
|
||||
}
|
||||
$manager = new \tool_dataprivacy\expired_contexts_manager(new \text_progress_trace());
|
||||
list($courses, $users) = $manager->process_approved_deletions();
|
||||
mtrace("Processed deletions for {$courses} course contexts, and {$users} user contexts as expired");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,15 +54,8 @@ class expired_retention_period extends scheduled_task {
|
||||
* Run the task to flag context instances as expired.
|
||||
*/
|
||||
public function execute() {
|
||||
$manager = new \tool_dataprivacy\expired_course_related_contexts();
|
||||
$flagged = $manager->flag_expired();
|
||||
if ($flagged > 0) {
|
||||
mtrace($flagged . ' course-related contexts have been flagged as expired');
|
||||
}
|
||||
$manager = new \tool_dataprivacy\expired_user_contexts();
|
||||
$flagged = $manager->flag_expired();
|
||||
if ($flagged > 0) {
|
||||
mtrace($flagged . ' user contexts have been flagged as expired');
|
||||
}
|
||||
$manager = new \tool_dataprivacy\expired_contexts_manager(new \text_progress_trace());
|
||||
list($courses, $users) = $manager->flag_expired_contexts();
|
||||
mtrace("Flagged {$courses} course contexts, and {$users} user contexts as expired");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,27 +70,6 @@ class initiate_data_request_task extends adhoc_task {
|
||||
return;
|
||||
}
|
||||
|
||||
$requestedby = $datarequest->get('requestedby');
|
||||
$valid = true;
|
||||
$comment = '';
|
||||
$foruser = $datarequest->get('userid');
|
||||
if ($foruser != $requestedby) {
|
||||
if (!$valid = api::can_create_data_request_for_user($foruser, $requestedby)) {
|
||||
$params = (object)[
|
||||
'requestedby' => $requestedby,
|
||||
'userid' => $foruser
|
||||
];
|
||||
$comment = get_string('errornocapabilitytorequestforothers', 'tool_dataprivacy', $params);
|
||||
mtrace($comment);
|
||||
}
|
||||
}
|
||||
// Reject the request outright if it's invalid.
|
||||
if (!$valid) {
|
||||
$dpo = $datarequest->get('dpo');
|
||||
api::update_request_status($requestid, api::DATAREQUEST_STATUS_REJECTED, $dpo, $comment);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the status of this request as pre-processing.
|
||||
mtrace('Generating the contexts containing personal data for the user...');
|
||||
api::update_request_status($requestid, api::DATAREQUEST_STATUS_PREPROCESSING);
|
||||
@@ -109,13 +88,19 @@ class initiate_data_request_task extends adhoc_task {
|
||||
// Get the list of the site Data Protection Officers.
|
||||
$dpos = api::get_site_dpos();
|
||||
|
||||
// Email the data request to the Data Protection Officer(s)/Admin(s).
|
||||
foreach ($dpos as $dpo) {
|
||||
$dponame = fullname($dpo);
|
||||
if (api::notify_dpo($dpo, $datarequest)) {
|
||||
mtrace('Message sent to DPO: ' . $dponame);
|
||||
} else {
|
||||
mtrace('A problem was encountered while sending the message to the DPO: ' . $dponame);
|
||||
// We should prevent DPO(s)/Admin(s) being flooded with notifications for each request when bulk delete
|
||||
// data requests are being created.
|
||||
// NOTE: This should be improved, we should not totally disable the notifications for automatically
|
||||
// created requests. Possibly, we should create one notification for these such cases.
|
||||
if ($datarequest->get('creationmethod') != data_request::DATAREQUEST_CREATION_AUTO) {
|
||||
// Email the data request to the Data Protection Officer(s)/Admin(s).
|
||||
foreach ($dpos as $dpo) {
|
||||
$dponame = fullname($dpo);
|
||||
if (api::notify_dpo($dpo, $datarequest)) {
|
||||
mtrace('Message sent to DPO: ' . $dponame);
|
||||
} else {
|
||||
mtrace('A problem was encountered while sending the message to the DPO: ' . $dponame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,16 +74,29 @@ class process_data_request_task extends adhoc_task {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\tool_dataprivacy\data_registry::defaults_set()) {
|
||||
// Warn if no site purpose is defined.
|
||||
mtrace('Warning: No purpose is defined at the system level. Deletion will delete all.');
|
||||
}
|
||||
|
||||
// Get the user details now. We might not be able to retrieve it later if it's a deletion processing.
|
||||
$foruser = core_user::get_user($request->userid);
|
||||
$usercontext = \context_user::instance($foruser->id);
|
||||
|
||||
// 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;
|
||||
$deleteuser = false;
|
||||
|
||||
if ($request->type == api::DATAREQUEST_TYPE_EXPORT) {
|
||||
// Get the user context.
|
||||
$usercontext = \context_user::instance($foruser->id, IGNORE_MISSING);
|
||||
if (!$usercontext) {
|
||||
mtrace("Request {$requestid} cannot be processed due to a missing user context instance for the user
|
||||
with ID {$foruser->id}. Skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the collection of approved_contextlist objects needed for core_privacy data export.
|
||||
$approvedclcollection = api::get_approved_contextlist_collection_for_request($requestpersistent);
|
||||
|
||||
@@ -117,6 +130,7 @@ class process_data_request_task extends adhoc_task {
|
||||
|
||||
$manager->delete_data_for_user($approvedclcollection);
|
||||
$completestatus = api::DATAREQUEST_STATUS_DELETED;
|
||||
$deleteuser = !$foruser->deleted;
|
||||
}
|
||||
|
||||
// When the preparation of the metadata finishes, update the request status to awaiting approval.
|
||||
@@ -191,12 +205,19 @@ class process_data_request_task extends adhoc_task {
|
||||
|
||||
// Send message to the user involved.
|
||||
if ($notifyuser) {
|
||||
$messagesent = false;
|
||||
if ($emailonly) {
|
||||
email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
|
||||
// Do not sent an email if the user has been deleted. The user email has been previously deleted.
|
||||
if (!$foruser->deleted) {
|
||||
$messagesent = email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
|
||||
}
|
||||
} else {
|
||||
message_send($message);
|
||||
$messagesent = message_send($message);
|
||||
}
|
||||
|
||||
if ($messagesent) {
|
||||
mtrace('Message sent to user: ' . $messagetextdata['username']);
|
||||
}
|
||||
mtrace('Message sent to user: ' . $messagetextdata['username']);
|
||||
}
|
||||
|
||||
// Send to requester as well in some circumstances.
|
||||
@@ -242,7 +263,7 @@ class process_data_request_task extends adhoc_task {
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->type == api::DATAREQUEST_TYPE_DELETE) {
|
||||
if ($deleteuser) {
|
||||
// Delete the user.
|
||||
delete_user($foruser);
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@
|
||||
|
||||
require_once('../../../config.php');
|
||||
require_once('lib.php');
|
||||
require_once('classes/api.php');
|
||||
require_once('createdatarequest_form.php');
|
||||
|
||||
$manage = optional_param('manage', 0, PARAM_INT);
|
||||
$requesttype = optional_param('type', \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT, PARAM_INT);
|
||||
|
||||
$url = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', ['manage' => $manage]);
|
||||
$url = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', ['manage' => $manage, 'type' => $requesttype]);
|
||||
|
||||
$PAGE->set_url($url);
|
||||
|
||||
@@ -59,6 +59,7 @@ if (!$manage && !\tool_dataprivacy\api::can_contact_dpo()) {
|
||||
}
|
||||
|
||||
$mform = new tool_dataprivacy_data_request_form($url->out(false), ['manage' => !empty($manage)]);
|
||||
$mform->set_data(['type' => $requesttype]);
|
||||
|
||||
// Data request cancelled.
|
||||
if ($mform->is_cancelled()) {
|
||||
@@ -67,6 +68,14 @@ if ($mform->is_cancelled()) {
|
||||
|
||||
// Data request submitted.
|
||||
if ($data = $mform->get_data()) {
|
||||
if ($data->userid != $USER->id) {
|
||||
if (!\tool_dataprivacy\api::can_manage_data_requests($USER->id)) {
|
||||
// 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).
|
||||
\tool_dataprivacy\api::require_can_create_data_request_for_user($data->userid);
|
||||
}
|
||||
}
|
||||
|
||||
\tool_dataprivacy\api::create_data_request($data->userid, $data->type, $data->comments);
|
||||
|
||||
if ($manage) {
|
||||
@@ -78,8 +87,8 @@ if ($data = $mform->get_data()) {
|
||||
redirect($returnurl, $redirectmessage);
|
||||
}
|
||||
|
||||
$title = get_string('contactdataprotectionofficer', 'tool_dataprivacy');
|
||||
$PAGE->set_heading($title);
|
||||
$title = get_string('createnewdatarequest', 'tool_dataprivacy');
|
||||
$PAGE->set_heading($SITE->fullname);
|
||||
$PAGE->set_title($title);
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading($title);
|
||||
|
||||
@@ -36,6 +36,7 @@ $title = get_string('datadeletion', 'tool_dataprivacy');
|
||||
\tool_dataprivacy\page_helper::setup($url, $title);
|
||||
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading($title);
|
||||
|
||||
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
|
||||
$table = new \tool_dataprivacy\output\expired_contexts_table($filter);
|
||||
|
||||
@@ -37,6 +37,7 @@ $title = get_string('dataregistry', 'tool_dataprivacy');
|
||||
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->header();
|
||||
echo $OUTPUT->heading($title);
|
||||
|
||||
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
|
||||
$dataregistry = new tool_dataprivacy\output\data_registry_page($contextlevel, $contextid);
|
||||
|
||||
@@ -27,6 +27,8 @@ require_once('lib.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$perpage = optional_param('perpage', 0, PARAM_INT);
|
||||
|
||||
$url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
|
||||
|
||||
$title = get_string('datarequests', 'tool_dataprivacy');
|
||||
@@ -36,6 +38,10 @@ $title = get_string('datarequests', 'tool_dataprivacy');
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading($title);
|
||||
|
||||
if (!\tool_dataprivacy\data_registry::defaults_set()) {
|
||||
\core\notification::error(get_string('systemconfignotsetwarning', 'tool_dataprivacy'));
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -53,6 +59,7 @@ if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
|
||||
|
||||
$types = [];
|
||||
$statuses = [];
|
||||
$creationmethods = [];
|
||||
foreach ($filtersapplied as $filter) {
|
||||
list($category, $value) = explode(':', $filter);
|
||||
switch($category) {
|
||||
@@ -62,10 +69,20 @@ if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
|
||||
case \tool_dataprivacy\local\helper::FILTER_STATUS:
|
||||
$statuses[] = $value;
|
||||
break;
|
||||
case \tool_dataprivacy\local\helper::FILTER_CREATION:
|
||||
$creationmethods[] = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, true);
|
||||
$table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, $creationmethods, true);
|
||||
if (!empty($perpage)) {
|
||||
set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_PERPAGE, $perpage);
|
||||
} else {
|
||||
$prefperpage = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_PERPAGE);
|
||||
$perpage = ($prefperpage) ? $prefperpage : $table->get_requests_per_page_options()[0];
|
||||
}
|
||||
$table->set_requests_per_page($perpage);
|
||||
$table->baseurl = $url;
|
||||
|
||||
$requestlist = new tool_dataprivacy\output\data_requests_page($table, $filtersapplied);
|
||||
|
||||
@@ -33,6 +33,13 @@ $definitions = array(
|
||||
'staticacceleration' => true,
|
||||
'staticaccelerationsize' => 30,
|
||||
),
|
||||
'purpose_overrides' => array(
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'simplekeys' => true,
|
||||
'simpledata' => false,
|
||||
'staticacceleration' => true,
|
||||
'staticaccelerationsize' => 50,
|
||||
),
|
||||
'contextlevel' => array(
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'simplekeys' => true,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* This file defines observers needed by the plugin.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$observers = [
|
||||
[
|
||||
'eventname' => '\core\event\user_deleted',
|
||||
'callback' => '\tool_dataprivacy\event\user_deleted_observer::create_delete_data_request',
|
||||
],
|
||||
];
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180821" COMMENT="XMLDB file for Moodle tool/dataprivacy"
|
||||
<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180904" COMMENT="XMLDB file for Moodle tool/dataprivacy"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
|
||||
>
|
||||
@@ -19,6 +19,7 @@
|
||||
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user who created/modified this request object"/>
|
||||
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time this data request was created"/>
|
||||
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The last time this data request was updated"/>
|
||||
<FIELD NAME="creationmethod" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The type of the creation method of the data request"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
@@ -98,6 +99,9 @@
|
||||
<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="unexpiredroles" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Roles which have explicitly not expired yet."/>
|
||||
<FIELD NAME="expiredroles" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Explicitly expires roles"/>
|
||||
<FIELD NAME="defaultexpired" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="The default retention period has passed."/>
|
||||
<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"/>
|
||||
@@ -146,5 +150,27 @@
|
||||
<KEY NAME="request_contextlist" TYPE="unique" FIELDS="requestid, contextlistid" COMMENT="Uniqueness constraint on request and contextlist"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="tool_dataprivacy_purposerole" COMMENT="Data purpose overrides for a specific role">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="purposeid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="roleid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="lawfulbases" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="sensitivedatareasons" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="retentionperiod" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="protected" TYPE="int" LENGTH="1" NOTNULL="false" 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"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="purposepurposeid" TYPE="foreign" FIELDS="purposeid" REFTABLE="tool_dataprivacy_purpose" REFFIELDS="id"/>
|
||||
<KEY NAME="puproseroleid" TYPE="foreign" FIELDS="roleid" REFTABLE="role" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
<INDEXES>
|
||||
<INDEX NAME="purposerole" UNIQUE="true" FIELDS="purposeid, roleid"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
@@ -73,6 +73,16 @@ $functions = [
|
||||
'ajax' => true,
|
||||
'loginrequired' => true,
|
||||
],
|
||||
'tool_dataprivacy_bulk_approve_data_requests' => [
|
||||
'classname' => 'tool_dataprivacy\external',
|
||||
'methodname' => 'bulk_approve_data_requests',
|
||||
'classpath' => '',
|
||||
'description' => 'Bulk approve data requests',
|
||||
'type' => 'write',
|
||||
'capabilities' => 'tool/dataprivacy:managedatarequests',
|
||||
'ajax' => true,
|
||||
'loginrequired' => true,
|
||||
],
|
||||
'tool_dataprivacy_deny_data_request' => [
|
||||
'classname' => 'tool_dataprivacy\external',
|
||||
'methodname' => 'deny_data_request',
|
||||
@@ -83,6 +93,16 @@ $functions = [
|
||||
'ajax' => true,
|
||||
'loginrequired' => true,
|
||||
],
|
||||
'tool_dataprivacy_bulk_deny_data_requests' => [
|
||||
'classname' => 'tool_dataprivacy\external',
|
||||
'methodname' => 'bulk_deny_data_requests',
|
||||
'classpath' => '',
|
||||
'description' => 'Bulk deny data requests',
|
||||
'type' => 'write',
|
||||
'capabilities' => 'tool/dataprivacy:managedatarequests',
|
||||
'ajax' => true,
|
||||
'loginrequired' => true,
|
||||
],
|
||||
'tool_dataprivacy_get_users' => [
|
||||
'classname' => 'tool_dataprivacy\external',
|
||||
'methodname' => 'get_users',
|
||||
@@ -173,4 +193,44 @@ $functions = [
|
||||
'ajax' => true,
|
||||
'loginrequired' => true,
|
||||
],
|
||||
'tool_dataprivacy_set_context_defaults' => [
|
||||
'classname' => 'tool_dataprivacy\external',
|
||||
'methodname' => 'set_context_defaults',
|
||||
'classpath' => '',
|
||||
'description' => 'Updates the default category and purpose for a given context level (and optionally, a plugin)',
|
||||
'type' => 'write',
|
||||
'capabilities' => 'tool/dataprivacy:managedataregistry',
|
||||
'ajax' => true,
|
||||
'loginrequired' => true,
|
||||
],
|
||||
'tool_dataprivacy_get_category_options' => [
|
||||
'classname' => 'tool_dataprivacy\external',
|
||||
'methodname' => 'get_category_options',
|
||||
'classpath' => '',
|
||||
'description' => 'Fetches a list of data category options',
|
||||
'type' => 'read',
|
||||
'capabilities' => 'tool/dataprivacy:managedataregistry',
|
||||
'ajax' => true,
|
||||
'loginrequired' => true,
|
||||
],
|
||||
'tool_dataprivacy_get_purpose_options' => [
|
||||
'classname' => 'tool_dataprivacy\external',
|
||||
'methodname' => 'get_purpose_options',
|
||||
'classpath' => '',
|
||||
'description' => 'Fetches a list of data storage purpose options',
|
||||
'type' => 'read',
|
||||
'capabilities' => 'tool/dataprivacy:managedataregistry',
|
||||
'ajax' => true,
|
||||
'loginrequired' => true,
|
||||
],
|
||||
'tool_dataprivacy_get_activity_options' => [
|
||||
'classname' => 'tool_dataprivacy\external',
|
||||
'methodname' => 'get_activity_options',
|
||||
'classpath' => '',
|
||||
'description' => 'Fetches a list of activity options',
|
||||
'type' => 'read',
|
||||
'capabilities' => 'tool/dataprivacy:managedataregistry',
|
||||
'ajax' => true,
|
||||
'loginrequired' => true,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -50,5 +50,14 @@ $tasks = array(
|
||||
'day' => '*',
|
||||
'dayofweek' => '*',
|
||||
'month' => '*'
|
||||
), array(
|
||||
'classname' => 'tool_dataprivacy\task\delete_existing_deleted_users',
|
||||
'blocking' => 0,
|
||||
'minute' => 'R',
|
||||
'hour' => 'R',
|
||||
'day' => '*',
|
||||
'dayofweek' => '*',
|
||||
'month' => '*',
|
||||
'disabled' => true,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -201,5 +201,97 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) {
|
||||
upgrade_plugin_savepoint(true, 2017111316, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
if ($oldversion < 2017111354) {
|
||||
// Define table tool_dataprivacy_purposerole to be created.
|
||||
$table = new xmldb_table('tool_dataprivacy_purposerole');
|
||||
|
||||
// Adding fields to table tool_dataprivacy_purposerole.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('purposeid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('lawfulbases', XMLDB_TYPE_TEXT, null, null, null, null, null);
|
||||
$table->add_field('sensitivedatareasons', XMLDB_TYPE_TEXT, null, null, null, null, null);
|
||||
$table->add_field('retentionperiod', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('protected', XMLDB_TYPE_INTEGER, '1', null, null, null, null);
|
||||
$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_purposerole.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
|
||||
$table->add_key('purposepurposeid', XMLDB_KEY_FOREIGN, ['purposeid'], 'tool_dataprivacy_purpose', ['id']);
|
||||
$table->add_key('puproseroleid', XMLDB_KEY_FOREIGN, ['roleid'], 'role', ['id']);
|
||||
|
||||
// Adding indexes to table tool_dataprivacy_purposerole.
|
||||
$table->add_index('purposerole', XMLDB_INDEX_UNIQUE, ['purposeid', 'roleid']);
|
||||
|
||||
// Conditionally launch create table for tool_dataprivacy_purposerole.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Update the ctxexpired table.
|
||||
$table = new xmldb_table('tool_dataprivacy_ctxexpired');
|
||||
|
||||
// Add the unexpiredroles field.
|
||||
$field = new xmldb_field('unexpiredroles', XMLDB_TYPE_TEXT, null, null, null, null, null, 'contextid');
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
$DB->set_field('tool_dataprivacy_ctxexpired', 'unexpiredroles', '');
|
||||
|
||||
// Add the expiredroles field.
|
||||
$field = new xmldb_field('expiredroles', XMLDB_TYPE_TEXT, null, null, null, null, null, 'unexpiredroles');
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
$DB->set_field('tool_dataprivacy_ctxexpired', 'expiredroles', '');
|
||||
|
||||
// Add the defaultexpired field.
|
||||
$field = new xmldb_field('defaultexpired', XMLDB_TYPE_INTEGER, '1', null, null, null, '1', 'expiredroles');
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Change the default for the expired field to be empty.
|
||||
$field = new xmldb_field('defaultexpired', XMLDB_TYPE_INTEGER, '1', null, null, null, null, 'expiredroles');
|
||||
$dbman->change_field_default($table, $field);
|
||||
|
||||
// Prevent hte field from being nullable.
|
||||
$field = new xmldb_field('defaultexpired', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null, 'expiredroles');
|
||||
$dbman->change_field_notnull($table, $field);
|
||||
|
||||
// Dataprivacy savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2017111354, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
if ($oldversion < 2017111356) {
|
||||
// Define field sensitivedatareasons to be added to tool_dataprivacy_purpose.
|
||||
$table = new xmldb_table('tool_dataprivacy_request');
|
||||
$field = new xmldb_field('creationmethod', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, 0, 'timemodified');
|
||||
|
||||
// Conditionally launch add field sensitivedatareasons.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Dataprivacy savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2017111356, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
if ($oldversion < 2017111358) {
|
||||
// Delete orphaned data privacy requests.
|
||||
$sql = "SELECT r.id
|
||||
FROM {tool_dataprivacy_request} r LEFT JOIN {user} u ON r.userid = u.id
|
||||
WHERE u.id IS NULL";
|
||||
$orphaned = $DB->get_fieldset_sql($sql);
|
||||
|
||||
if ($orphaned) {
|
||||
$DB->delete_records_list('tool_dataprivacy_request', 'id', $orphaned);
|
||||
}
|
||||
|
||||
upgrade_plugin_savepoint(true, 2017111358, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -32,47 +32,40 @@ $title = get_string('setdefaults', 'tool_dataprivacy');
|
||||
|
||||
\tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry');
|
||||
|
||||
$levels = \context_helper::get_all_levels();
|
||||
// They are set through the context level site and user.
|
||||
unset($levels[CONTEXT_SYSTEM]);
|
||||
unset($levels[CONTEXT_USER]);
|
||||
$mode = optional_param('mode', CONTEXT_COURSECAT, PARAM_INT);
|
||||
$classname = context_helper::get_class_for_level($mode);
|
||||
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
|
||||
$purpose = get_config('tool_dataprivacy', $purposevar);
|
||||
$category = get_config('tool_dataprivacy', $categoryvar);
|
||||
|
||||
$customdata = [
|
||||
'levels' => $levels,
|
||||
'purposes' => \tool_dataprivacy\api::get_purposes(),
|
||||
'categories' => \tool_dataprivacy\api::get_categories(),
|
||||
];
|
||||
$form = new \tool_dataprivacy\form\defaults($PAGE->url->out(false), $customdata);
|
||||
$otherdefaults = [];
|
||||
if ($mode == CONTEXT_MODULE) {
|
||||
// Get activity module plugin info.
|
||||
$pluginmanager = core_plugin_manager::instance();
|
||||
$modplugins = $pluginmanager->get_enabled_plugins('mod');
|
||||
|
||||
$toform = new stdClass();
|
||||
foreach ($levels as $level => $classname) {
|
||||
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
|
||||
$toform->{$purposevar} = get_config('tool_dataprivacy', $purposevar);
|
||||
$toform->{$categoryvar} = get_config('tool_dataprivacy', $categoryvar);
|
||||
}
|
||||
$form->set_data($toform);
|
||||
|
||||
$returnurl = new \moodle_url('/admin/tool/dataprivacy/dataregistry.php');
|
||||
if ($form->is_cancelled()) {
|
||||
redirect($returnurl);
|
||||
} else if ($data = $form->get_data()) {
|
||||
|
||||
foreach ($levels as $level => $classname) {
|
||||
|
||||
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
|
||||
|
||||
if (isset($data->{$purposevar})) {
|
||||
set_config($purposevar, $data->{$purposevar}, 'tool_dataprivacy');
|
||||
}
|
||||
if (isset($data->{$categoryvar})) {
|
||||
set_config($categoryvar, $data->{$categoryvar}, 'tool_dataprivacy');
|
||||
foreach ($modplugins as $name) {
|
||||
list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname, $name);
|
||||
$plugincategory = get_config('tool_dataprivacy', $categoryvar);
|
||||
$pluginpurpose = get_config('tool_dataprivacy', $purposevar);
|
||||
if ($plugincategory === false && $pluginpurpose === false) {
|
||||
// If no purpose and category has been set for this plugin, then there's no need to show this on the list.
|
||||
continue;
|
||||
}
|
||||
|
||||
$displayname = $pluginmanager->plugin_name('mod_' . $name);
|
||||
$otherdefaults[$name] = (object)[
|
||||
'name' => $displayname,
|
||||
'category' => $plugincategory,
|
||||
'purpose' => $pluginpurpose,
|
||||
];
|
||||
}
|
||||
redirect($returnurl, get_string('defaultssaved', 'tool_dataprivacy'),
|
||||
0, \core\output\notification::NOTIFY_SUCCESS);
|
||||
}
|
||||
|
||||
$defaultspage = new \tool_dataprivacy\output\defaults_page($mode, $category, $purpose, $otherdefaults, true);
|
||||
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->header();
|
||||
$form->display();
|
||||
echo $output->heading($title);
|
||||
echo $output->render_from_template('tool_dataprivacy/defaults_page', $defaultspage->export_for_template($output));
|
||||
echo $output->footer();
|
||||
|
||||
@@ -44,14 +44,47 @@ $form = new \tool_dataprivacy\form\purpose($PAGE->url->out(false),
|
||||
$returnurl = new \moodle_url('/admin/tool/dataprivacy/purposes.php');
|
||||
if ($form->is_cancelled()) {
|
||||
redirect($returnurl);
|
||||
} else if ($data = $form->get_data()) {
|
||||
} else if ($alldata = $form->get_data()) {
|
||||
$data = $form->filter_data_for_persistent($alldata);
|
||||
|
||||
if (empty($data->id)) {
|
||||
\tool_dataprivacy\api::create_purpose($data);
|
||||
$purpose = \tool_dataprivacy\api::create_purpose($data);
|
||||
$messagesuccess = get_string('purposecreated', 'tool_dataprivacy');
|
||||
} else {
|
||||
\tool_dataprivacy\api::update_purpose($data);
|
||||
$purpose = \tool_dataprivacy\api::update_purpose($data);
|
||||
$messagesuccess = get_string('purposeupdated', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
$currentoverrides = [];
|
||||
foreach ($purpose->get_purpose_overrides() as $override) {
|
||||
$currentoverrides[$override->get('id')] = $override;
|
||||
}
|
||||
|
||||
$overrides = $form->get_role_overrides_from_data($alldata);
|
||||
$submittedoverrides = [];
|
||||
$tosave = [];
|
||||
|
||||
foreach ($overrides as $overridedata) {
|
||||
$overridedata->purposeid = $purpose->get('id');
|
||||
$override = new \tool_dataprivacy\purpose_override($overridedata->id, $overridedata);
|
||||
|
||||
$tosave[] = $override;
|
||||
|
||||
if (!empty($overridedata->id)) {
|
||||
$submittedoverrides[$overridedata->id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($currentoverrides as $id => $override) {
|
||||
if (!isset($submittedoverrides[$id])) {
|
||||
$override->delete();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tosave as $override) {
|
||||
$override->save();
|
||||
}
|
||||
|
||||
redirect($returnurl, $messagesuccess, 0, \core\output\notification::NOTIFY_SUCCESS);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,13 +28,20 @@ $string['pluginname'] = 'Data privacy';
|
||||
$string['pluginname_help'] = 'Data privacy plugin';
|
||||
$string['activitiesandresources'] = 'Activities and resources';
|
||||
$string['addcategory'] = 'Add category';
|
||||
$string['addnewdefaults'] = 'Add a new module default';
|
||||
$string['addpurpose'] = 'Add purpose';
|
||||
$string['approve'] = 'Approve';
|
||||
$string['approverequest'] = 'Approve request';
|
||||
$string['automaticdeletionrequests'] = 'Create automatic data deletion requests';
|
||||
$string['automaticdeletionrequests_desc'] = 'If enabled, automatic delete data request will be created upon user deletion or for each existing deleted user which data was not fully deleted.';
|
||||
$string['bulkapproverequests'] = 'Approve requests';
|
||||
$string['bulkdenyrequests'] = 'Deny requests';
|
||||
$string['cachedef_purpose'] = 'Data purposes';
|
||||
$string['cachedef_purpose_overrides'] = 'Purpose overrides in the Data privacy tool';
|
||||
$string['cachedef_contextlevel'] = 'Context levels purpose and category';
|
||||
$string['cancelrequest'] = 'Cancel request';
|
||||
$string['cancelrequestconfirmation'] = 'Do you really want cancel this data request?';
|
||||
$string['cannotreset'] = 'Unable to reset this request. Only rejected requests can be reset.';
|
||||
$string['categories'] = 'Categories';
|
||||
$string['category'] = 'Category';
|
||||
$string['category_help'] = 'A category in the data registry describes a type of data. A new category may be added, or if Inherit is selected, the data category from a higher context is applied. Contexts are (from low to high): Blocks > Activity modules > Courses > Course categories > Site.';
|
||||
@@ -46,9 +53,12 @@ $string['categoryupdated'] = 'Category updated';
|
||||
$string['close'] = 'Close';
|
||||
$string['compliant'] = 'Compliant';
|
||||
$string['confirmapproval'] = 'Do you really want to approve this data request?';
|
||||
$string['confirmbulkapproval'] = 'Do you really want to bulk approve the selected data requests?';
|
||||
$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['confirmbulkdenial'] = 'Do you really want to bulk deny the selected data requests?';
|
||||
$string['confirmrequestresubmit'] = 'Are you sure you wish to cancel the current {$a->type} request for {$a->username} and resubmit it?';
|
||||
$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';
|
||||
@@ -60,7 +70,10 @@ $string['contextlevelname80'] = 'Blocks';
|
||||
$string['contextpurposecategorysaved'] = 'Purpose and category saved.';
|
||||
$string['contactdpoviaprivacypolicy'] = 'Please contact the privacy officer as described in the privacy policy.';
|
||||
$string['createcategory'] = 'Create data category';
|
||||
$string['createnewdatarequest'] = 'Create a new data request';
|
||||
$string['createpurpose'] = 'Create data purpose';
|
||||
$string['creationauto'] = 'Automatically';
|
||||
$string['creationmanual'] = 'Manually';
|
||||
$string['datadeletion'] = 'Data deletion';
|
||||
$string['datadeletionpagehelp'] = 'Data for which the retention period has expired are listed here. Please review and confirm data deletion, which will then be executed by the "Delete expired contexts" scheduled task.';
|
||||
$string['dataprivacy:makedatarequestsforchildren'] = 'Make data requests for minors';
|
||||
@@ -70,22 +83,33 @@ $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['dataretentionexplanation'] = 'This summary shows the default categories and purposes for retaining user data. Certain areas may have more specific categories and purposes than those listed here.';
|
||||
$string['dataretentionsummary'] = 'Data retention summary';
|
||||
$string['datarequestcreatedforuser'] = 'Data request created for {$a}';
|
||||
$string['datarequestcreatedfromscheduledtask'] = 'Automatically created from a scheduled task (pre-existing deleted user).';
|
||||
$string['datarequestemailsubject'] = 'Data request: {$a}';
|
||||
$string['datarequestcreatedupondelete'] = 'Automatically created upon user deletion.';
|
||||
$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['defaultsinfo'] = 'Default categories and purposes are applied to all new and existing instances where a value is not set.';
|
||||
$string['defaultswarninginfo'] = 'Warning: Changing these defaults may affect the retention period of existing instances.';
|
||||
$string['deletecategory'] = 'Delete category';
|
||||
$string['deletecategorytext'] = 'Are you sure you want to delete the category \'{$a}\'?';
|
||||
$string['deletedefaults'] = 'Delete defaults: {$a}';
|
||||
$string['deletedefaultsconfirmation'] = 'Are you sure you want to delete the default category and purpose for {$a} modules?';
|
||||
$string['deleteexpiredcontextstask'] = 'Delete expired contexts';
|
||||
$string['deleteexpireddatarequeststask'] = 'Delete files from completed data requests that have expired';
|
||||
$string['deleteexpireddatarequeststask'] = 'Delete expired data request export files';
|
||||
$string['deleteexistingdeleteduserstask'] = 'Create delete data request for pre-existing deleted users';
|
||||
$string['deletemyaccount'] = 'Delete my account';
|
||||
$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['deprecated'] = 'Deprecated';
|
||||
$string['deprecatedexplanation'] = 'This plugin is using an old version of one of the privacy interfaces and should be updated.';
|
||||
$string['download'] = 'Download';
|
||||
$string['downloadexpireduser'] = 'Download has expired. Submit a new request if you wish to export your personal data.';
|
||||
$string['dporolemapping'] = 'Privacy officer role mapping';
|
||||
@@ -93,11 +117,14 @@ $string['dporolemapping_desc'] = 'The privacy officer can manage data requests.
|
||||
$string['editcategories'] = 'Edit categories';
|
||||
$string['editcategory'] = 'Edit category';
|
||||
$string['editcategories'] = 'Edit categories';
|
||||
$string['editdefaults'] = 'Edit defaults: {$a}';
|
||||
$string['editmoduledefaults'] = 'Edit module defaults';
|
||||
$string['editpurpose'] = 'Edit purpose';
|
||||
$string['editpurposes'] = 'Edit purposes';
|
||||
$string['effectiveretentionperiodcourse'] = '{$a} (after the course end date)';
|
||||
$string['effectiveretentionperioduser'] = '{$a} (since the last time the user accessed the site)';
|
||||
$string['emailsalutation'] = 'Dear {$a},';
|
||||
$string['errorinvalidrequestcreationmethod'] = 'Invalid request creation method!';
|
||||
$string['errorinvalidrequeststatus'] = 'Invalid request status!';
|
||||
$string['errorinvalidrequesttype'] = 'Invalid request type!';
|
||||
$string['errornocapabilitytorequestforothers'] = 'User {$a->requestedby} doesn\'t have the capability to make a data request on behalf of user {$a->userid}';
|
||||
@@ -183,11 +210,14 @@ $string['nopurposes'] = 'There are no purposes yet';
|
||||
$string['nosubjectaccessrequests'] = 'There are no data requests that you need to act on';
|
||||
$string['nosystemdefaults'] = 'Site purpose and category have not yet been defined.';
|
||||
$string['notset'] = 'Not set (use the default value)';
|
||||
$string['notyetexpired'] = '{$a} (not yet expired)';
|
||||
$string['overrideinstances'] = 'Reset instances with custom values';
|
||||
$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:preference:tool_dataprivacy_request-perpage'] = 'The number of data requests the user prefers to see on one 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';
|
||||
@@ -213,10 +243,14 @@ $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['requestcreation'] = 'Creation';
|
||||
$string['requestdenied'] = 'The request has been denied';
|
||||
$string['requestemailintro'] = 'You have received a data request:';
|
||||
$string['requestfor'] = 'Requesting for';
|
||||
$string['requestfor'] = 'User';
|
||||
$string['requestmarkedcomplete'] = 'The request has been marked as complete';
|
||||
$string['requestorigin'] = 'Site';
|
||||
$string['requestsapproved'] = 'The requests have been approved';
|
||||
$string['requestsdenied'] = 'The requests have been denied';
|
||||
$string['requeststatus'] = 'Status';
|
||||
$string['requestsubmitted'] = 'Your request has been submitted to the privacy officer';
|
||||
$string['requesttype'] = 'Type';
|
||||
@@ -228,8 +262,20 @@ $string['requesttypeexport'] = 'Export all of my personal data';
|
||||
$string['requesttypeexportshort'] = 'Export';
|
||||
$string['requesttypeothers'] = 'General inquiry';
|
||||
$string['requesttypeothersshort'] = 'Message';
|
||||
$string['requireallenddatesforuserdeletion'] = 'Consider courses without end date as active';
|
||||
$string['requireallenddatesforuserdeletion_desc'] = 'When calculating user expiry, several factors are considered:
|
||||
|
||||
* the user\'s last login time is compared against the retention period for users; and
|
||||
* whether the user is actively enrolled in any courses.
|
||||
|
||||
When checking the active enrolment in a course, if the course has no end date then this setting is used to determine whether that course is considered active or not.
|
||||
|
||||
If the course has no end date, and this setting is enabled, then the user cannot be deleted.';
|
||||
$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['resubmitrequestasnew'] = 'Resubmit as new request';
|
||||
$string['resubmitrequest'] = 'Resubmit {$a->type} request for {$a->username}';
|
||||
$string['resubmittedrequest'] = 'The existing {$a->type} request for {$a->username} was cancelled and resubmitted';
|
||||
$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.';
|
||||
$string['resultdownloadready'] = 'Your copy of your personal data in {$a} that you recently requested is now available for download. Please click on the link below to go to the download page.';
|
||||
$string['reviewdata'] = 'Review data';
|
||||
@@ -237,10 +283,16 @@ $string['retentionperiod'] = 'Retention period';
|
||||
$string['retentionperiod_help'] = 'The retention period specifies the length of time that data should be kept for. When the retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
|
||||
$string['retentionperiodnotdefined'] = 'No retention period was defined';
|
||||
$string['retentionperiodzero'] = 'No retention period';
|
||||
$string['roleoverrides'] = 'Role overrides';
|
||||
$string['selectbulkaction'] = 'Please select a bulk action.';
|
||||
$string['selectdatarequests'] = 'Please select data requests.';
|
||||
$string['selectuserdatarequest'] = 'Select {$a->username}\'s {$a->requesttype} data request.';
|
||||
$string['send'] = 'Send';
|
||||
$string['sensitivedatareasons'] = 'Sensitive personal data processing reasons';
|
||||
$string['sensitivedatareasons_help'] = 'Select one or more applicable reasons that exempts the prohibition of processing sensitive personal data tied to this purpose. For more information, please see <a href="https://gdpr-info.eu/art-9-gdpr/" target="_blank">GDPR Art. 9.2</a>';
|
||||
$string['setdefaults'] = 'Set defaults';
|
||||
$string['showdataretentionsummary'] = 'Show data retention summary';
|
||||
$string['showdataretentionsummary_desc'] = 'If enabled, a link to the data retention summary is shown in the page footer and in the user profile page.';
|
||||
$string['statusapproved'] = 'Approved';
|
||||
$string['statusawaitingapproval'] = 'Awaiting approval';
|
||||
$string['statuscancelled'] = 'Cancelled';
|
||||
@@ -255,6 +307,29 @@ $string['statuspending'] = 'Pending';
|
||||
$string['statusrejected'] = 'Rejected';
|
||||
$string['subjectscope'] = 'Subject scope';
|
||||
$string['subjectscope_help'] = 'The subject scope lists the roles which may be assigned in this context.';
|
||||
$string['summary'] = 'Registry configuration summary';
|
||||
$string['systemconfignotsetwarning'] = 'A site purpose and category have not been defined. When these are not defined, all data will be removed when processing deletion requests.';
|
||||
$string['user'] = 'User';
|
||||
$string['userlistnoncompliant'] = 'Userlist provider missing';
|
||||
$string['userlistexplanation'] = 'This plugin has the base provider but should also implement the userlist provider for full support of privacy functionality.';
|
||||
$string['viewrequest'] = 'View the request';
|
||||
$string['visible'] = 'Expand all';
|
||||
$string['unexpiredrolewithretention'] = '{$a->retention} (Unexpired)';
|
||||
$string['expiredrolewithretention'] = '{$a->retention} (Expired)';
|
||||
$string['defaultexpired'] = 'Data for all users';
|
||||
$string['defaultexpiredexcept'] = 'Data for all users, except those who hold any of the following roles:<br>
|
||||
{$a->unexpired}';
|
||||
$string['defaultunexpiredwithexceptions'] = 'Only data for users who hold any of the following roles:<br>
|
||||
{$a->expired}
|
||||
Unless they also hold any of the following roles:<br>
|
||||
{$a->unexpired}';
|
||||
$string['defaultunexpired'] = 'Only data for users holding any of the following roles:<br>
|
||||
{$a->expired}';
|
||||
$string['tobedeleted'] = 'Data to be deleted';
|
||||
$string['addroleoverride'] = 'Add role override';
|
||||
$string['roleoverride'] = 'Role override';
|
||||
$string['role'] = 'Role';
|
||||
$string['role_help'] = 'The role which the override should apply to.';
|
||||
$string['duplicaterole'] = 'Role already specified';
|
||||
$string['purposeoverview'] = 'A purpose describes the intended use and retention policy for stored data. The basis for storing and retaining that data is also described in the purpose.';
|
||||
$string['roleoverrideoverview'] = 'The default retention policy can be overridden for specific user roles, allowing you to specify a longer, or a shorter, retention policy. A user is only expired when all of their roles have expired.';
|
||||
|
||||
@@ -63,6 +63,42 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser
|
||||
$node = new core_user\output\myprofile\node('privacyandpolicies', 'datarequests',
|
||||
get_string('datarequests', 'tool_dataprivacy'), null, $url);
|
||||
$category->add_node($node);
|
||||
|
||||
// Check if the user has an ongoing data export request.
|
||||
$hasexportrequest = \tool_dataprivacy\api::has_ongoing_request($user->id, \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT);
|
||||
// Show data export link only if the user doesn't have an ongoing data export request.
|
||||
if (!$hasexportrequest) {
|
||||
$exportparams = ['type' => \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT];
|
||||
$exporturl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', $exportparams);
|
||||
$exportnode = new core_user\output\myprofile\node('privacyandpolicies', 'requestdataexport',
|
||||
get_string('requesttypeexport', 'tool_dataprivacy'), null, $exporturl);
|
||||
$category->add_node($exportnode);
|
||||
}
|
||||
|
||||
// Check if the user has an ongoing data deletion request.
|
||||
$hasdeleterequest = \tool_dataprivacy\api::has_ongoing_request($user->id, \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE);
|
||||
// Show data deletion link only if the user doesn't have an ongoing data deletion request.
|
||||
if (!$hasdeleterequest) {
|
||||
$deleteparams = ['type' => \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE];
|
||||
$deleteurl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', $deleteparams);
|
||||
$deletenode = new core_user\output\myprofile\node('privacyandpolicies', 'requestdatadeletion',
|
||||
get_string('deletemyaccount', 'tool_dataprivacy'), null, $deleteurl);
|
||||
$category->add_node($deletenode);
|
||||
}
|
||||
}
|
||||
|
||||
// A returned 0 means that the setting was set and disabled, false means that there is no value for the provided setting.
|
||||
$showsummary = get_config('tool_dataprivacy', 'showdataretentionsummary');
|
||||
if ($showsummary === false) {
|
||||
// This means that no value is stored in db. We use the default value in this case.
|
||||
$showsummary = true;
|
||||
}
|
||||
|
||||
if ($showsummary) {
|
||||
$summaryurl = new moodle_url('/admin/tool/dataprivacy/summary.php');
|
||||
$summarynode = new core_user\output\myprofile\node('privacyandpolicies', 'retentionsummary',
|
||||
get_string('dataretentionsummary', 'tool_dataprivacy'), null, $summaryurl);
|
||||
$category->add_node($summarynode);
|
||||
}
|
||||
|
||||
// Add the Privacy category to the tree if it's not empty and it doesn't exist.
|
||||
@@ -77,6 +113,29 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to add footer elements.
|
||||
*
|
||||
* @return string HTML footer content
|
||||
*/
|
||||
function tool_dataprivacy_standard_footer_html() {
|
||||
$output = '';
|
||||
|
||||
// A returned 0 means that the setting was set and disabled, false means that there is no value for the provided setting.
|
||||
$showsummary = get_config('tool_dataprivacy', 'showdataretentionsummary');
|
||||
if ($showsummary === false) {
|
||||
// This means that no value is stored in db. We use the default value in this case.
|
||||
$showsummary = true;
|
||||
}
|
||||
|
||||
if ($showsummary) {
|
||||
$url = new moodle_url('/admin/tool/dataprivacy/summary.php');
|
||||
$output = html_writer::link($url, get_string('dataretentionsummary', 'tool_dataprivacy'));
|
||||
$output = html_writer::div($output, 'tool_dataprivacy');
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fragment to add a new purpose.
|
||||
*
|
||||
|
||||
@@ -55,7 +55,7 @@ $PAGE->set_title($title);
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading($title);
|
||||
|
||||
$requests = tool_dataprivacy\api::get_data_requests($USER->id, [], [], 'timecreated DESC');
|
||||
$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);
|
||||
|
||||
@@ -33,6 +33,7 @@ $title = get_string('editpurposes', 'tool_dataprivacy');
|
||||
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->header();
|
||||
echo $output->heading($title);
|
||||
|
||||
$purposes = \tool_dataprivacy\api::get_purposes();
|
||||
$renderable = new \tool_dataprivacy\output\purposes($purposes);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Display the request reject + resubmit confirmation page.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
|
||||
* @package tool_dataprivacy
|
||||
*/
|
||||
|
||||
require_once('../../../config.php');
|
||||
|
||||
$requestid = required_param('requestid', PARAM_INT);
|
||||
$confirm = optional_param('confirm', null, PARAM_INT);
|
||||
|
||||
$PAGE->set_url(new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', ['requestid' => $requestid]));
|
||||
|
||||
require_login();
|
||||
|
||||
$PAGE->set_context(\context_system::instance());
|
||||
require_capability('tool/dataprivacy:managedatarequests', $PAGE->context);
|
||||
|
||||
$manageurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
|
||||
|
||||
$originalrequest = \tool_dataprivacy\api::get_request($requestid);
|
||||
$user = \core_user::get_user($originalrequest->get('userid'));
|
||||
$stringparams = (object) [
|
||||
'username' => fullname($user),
|
||||
'type' => \tool_dataprivacy\local\helper::get_shortened_request_type_string($originalrequest->get('type')),
|
||||
];
|
||||
|
||||
if (null !== $confirm && confirm_sesskey()) {
|
||||
$originalrequest->resubmit_request();
|
||||
redirect($manageurl, get_string('resubmittedrequest', 'tool_dataprivacy', $stringparams));
|
||||
}
|
||||
|
||||
$heading = get_string('resubmitrequest', 'tool_dataprivacy', $stringparams);
|
||||
$PAGE->set_title($heading);
|
||||
$PAGE->set_heading($heading);
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
$confirmstring = get_string('confirmrequestresubmit', 'tool_dataprivacy', $stringparams);
|
||||
$confirmurl = new \moodle_url($PAGE->url, ['confirm' => 1]);
|
||||
echo $OUTPUT->confirm($confirmstring, $confirmurl, $manageurl);
|
||||
echo $OUTPUT->footer();
|
||||
@@ -34,6 +34,14 @@ if ($hassiteconfig) {
|
||||
new lang_string('contactdataprotectionofficer_desc', 'tool_dataprivacy'), 0)
|
||||
);
|
||||
|
||||
// Automatically create delete data request for users upon user deletion.
|
||||
// Automatically create delete data request for pre-existing deleted users.
|
||||
// Disabled by default.
|
||||
$privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/automaticdeletionrequests',
|
||||
new lang_string('automaticdeletionrequests', 'tool_dataprivacy'),
|
||||
new lang_string('automaticdeletionrequests_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'),
|
||||
@@ -60,6 +68,18 @@ if ($hassiteconfig) {
|
||||
new lang_string('dporolemapping_desc', 'tool_dataprivacy'), null, $roles)
|
||||
);
|
||||
}
|
||||
|
||||
// When calculating user expiry, should courses which have no end date be considered.
|
||||
$privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/requireallenddatesforuserdeletion',
|
||||
new lang_string('requireallenddatesforuserdeletion', 'tool_dataprivacy'),
|
||||
new lang_string('requireallenddatesforuserdeletion_desc', 'tool_dataprivacy'),
|
||||
1));
|
||||
|
||||
// Whether the data retention summary should be shown in the page footer and in the user profile page.
|
||||
$privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/showdataretentionsummary',
|
||||
new lang_string('showdataretentionsummary', 'tool_dataprivacy'),
|
||||
new lang_string('showdataretentionsummary_desc', 'tool_dataprivacy'),
|
||||
1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,3 +32,15 @@ dd a.contactdpo {
|
||||
#page-admin-tool-dataprivacy-datarequests [data-region="data-requests-table"] .moodle-actionmenu {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.context-level-view {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
[data-region="purposes"] .col-name-description {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
[data-region="categories"] .col-description {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Prints the compliance data registry main page.
|
||||
*
|
||||
* @copyright 2018 onwards Adrian Greeve <adriangreeve.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
|
||||
* @package tool_dataprivacy
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
$url = new moodle_url('/' . $CFG->admin . '/tool/dataprivacy/summary.php');
|
||||
$title = get_string('summary', 'tool_dataprivacy');
|
||||
|
||||
$context = \context_system::instance();
|
||||
$PAGE->set_url($url);
|
||||
$PAGE->set_context($context);
|
||||
$PAGE->set_title($title);
|
||||
$PAGE->set_heading($SITE->fullname);
|
||||
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->header();
|
||||
$summarypage = new \tool_dataprivacy\output\summary_page();
|
||||
echo $output->render($summarypage);
|
||||
echo $output->footer();
|
||||
@@ -51,18 +51,17 @@
|
||||
{{/navigation}}
|
||||
|
||||
<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" title="{{#str}}addcategory, tool_dataprivacy{{/str}}">
|
||||
{{#pix}}t/add, moodle, {{#str}}addcategory, tool_dataprivacy{{/str}}{{/pix}}
|
||||
</button>
|
||||
</div>
|
||||
<table class="generaltable fullwidth">
|
||||
<table class="table table-striped table-hover">
|
||||
<caption class="accesshide">{{#str}}categorieslist, tool_dataprivacy{{/str}}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{#str}}name{{/str}}</th>
|
||||
<th scope="col">{{#str}}description{{/str}}</th>
|
||||
<th scope="col" class="col-description">{{#str}}description{{/str}}</th>
|
||||
<th scope="col">{{#str}}actions{{/str}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
{{!
|
||||
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/category_purpose_form
|
||||
|
||||
Manage data registry defaults.
|
||||
|
||||
Classes required for JS:
|
||||
|
||||
Data attributes required for JS:
|
||||
|
||||
Context variables required for this template:
|
||||
* actionurl String The action URL.
|
||||
* contextlevel Number - The context level.
|
||||
* modemodule Boolean - Whether to display the activity select element.
|
||||
* activityoptions Array - An array of objects for the activity select element.
|
||||
* categoryoptions Array - An array of objects for the category select element.
|
||||
* purposeoptions Array - An array of objects for the purpose select element.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"actionurl": "#",
|
||||
"contextlevel": 70,
|
||||
"newactivitydefaults": true,
|
||||
"modemodule": true,
|
||||
"activityoptions": [
|
||||
{ "name": "assign", "displayname": "Assignment" },
|
||||
{ "name": "forum", "displayname": "Forum", "selected": true },
|
||||
{ "name": "lesson", "displayname": "Lesson" },
|
||||
{ "name": "quiz", "displayname": "Quiz" }
|
||||
],
|
||||
"categoryoptions": [
|
||||
{ "id": 1, "name": "Category 1" },
|
||||
{ "id": 2, "name": "Category 2", "selected": true },
|
||||
{ "id": 3, "name": "Category 3" }
|
||||
],
|
||||
"purposeoptions": [
|
||||
{ "id": 1, "name": "Purpose 1" },
|
||||
{ "id": 2, "name": "Purpose 2" },
|
||||
{ "id": 3, "name": "Purpose 3", "selected": true }
|
||||
]
|
||||
}
|
||||
}}
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
{{#str}}defaultswarninginfo, tool_dataprivacy{{/str}}
|
||||
</div>
|
||||
<form method="post" action="{{actionurl}}" id="category_purpose_form">
|
||||
<input type="hidden" value="{{contextlevel}}" id="contextlevel" />
|
||||
{{#modemodule}}
|
||||
<div class="form-group">
|
||||
<label for="activity">{{#str}}activitymodule{{/str}}</label>
|
||||
{{^newactivitydefaults}}
|
||||
<input type="hidden" id="activity" value="{{#activityoptions}}{{#selected}}{{name}}{{/selected}}{{/activityoptions}}" />
|
||||
{{/newactivitydefaults}}
|
||||
<select class="form-control" {{#newactivitydefaults}}id="activity" {{/newactivitydefaults}}{{^newactivitydefaults}}disabled{{/newactivitydefaults}}>
|
||||
{{#activityoptions}}
|
||||
<option value="{{name}}" {{#selected}}selected{{/selected}}>{{displayname}}</option>
|
||||
{{/activityoptions}}
|
||||
</select>
|
||||
</div>
|
||||
{{/modemodule}}
|
||||
<div class="form-group">
|
||||
<label for="category">{{#str}}category, tool_dataprivacy{{/str}}</label>
|
||||
<select class="form-control" id="category">
|
||||
{{#categoryoptions}}
|
||||
<option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option>
|
||||
{{/categoryoptions}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="purpose">{{#str}}purpose, tool_dataprivacy{{/str}}</label>
|
||||
<select class="form-control" id="purpose">
|
||||
{{#purposeoptions}}
|
||||
<option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option>
|
||||
{{/purposeoptions}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="1" id="override">
|
||||
<label class="form-check-label" for="override">
|
||||
{{#str}}overrideinstances, tool_dataprivacy{{/str}}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
@@ -61,6 +61,12 @@
|
||||
{{#external}}
|
||||
<span class="badge badge-pill badge-notice">{{#str}}external, tool_dataprivacy{{/str}}</span>
|
||||
{{/external}}
|
||||
{{#deprecated}}
|
||||
<span class="badge badge-pill badge-warning">{{#str}}deprecated, tool_dataprivacy{{/str}}</span>
|
||||
{{/deprecated}}
|
||||
{{#userlistnoncompliance}}
|
||||
<span class="badge badge-pill badge-warning">{{#str}}userlistnoncompliant, tool_dataprivacy{{/str}}</span>
|
||||
{{/userlistnoncompliance}}
|
||||
</div>
|
||||
|
||||
{{#compliant}}
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
<dd>{{#str}}requiresattentionexplanation, tool_dataprivacy{{/str}}</dd>
|
||||
<dt><span class="badge badge-pill badge-notice">{{#str}}external, tool_dataprivacy{{/str}}</span></dt>
|
||||
<dd>{{#str}}externalexplanation, tool_dataprivacy{{/str}}</dd>
|
||||
<dt><span class="badge badge-pill badge-warning">{{#str}}deprecated, tool_dataprivacy{{/str}}</span></dt>
|
||||
<dd>{{#str}}deprecatedexplanation, tool_dataprivacy{{/str}}</dd>
|
||||
<dt><span class="badge badge-pill badge-warning">{{#str}}userlistnoncompliant, tool_dataprivacy{{/str}}</span></dt>
|
||||
<dd>{{#str}}userlistexplanation, tool_dataprivacy{{/str}}</dd>
|
||||
</dl>
|
||||
<hr />
|
||||
<div class="clearfix"><a class="tool_dataprivacy-expand-all pull-right" href="#" data-visibility-state='visible'>{{#str}}visible, tool_dataprivacy{{/str}}</a></div>
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
* string requestedby The one making the request.
|
||||
* string requesttype The request type.
|
||||
* string requestdate The date the request was made.
|
||||
* string requestorigin The name of the site the request originates from.
|
||||
* string requestoriginurl The homepage of the site the request originates from.
|
||||
* string requestcomments Additional details regarding the request.
|
||||
* bool forself Whether the request has been made on behalf of another user or not.
|
||||
* string datarequestsurl The URL to the data requests page.
|
||||
@@ -42,6 +44,8 @@
|
||||
"requestedby": "Angus Zhang",
|
||||
"requesttype": "Export user data",
|
||||
"requestdate": "31 January 2018",
|
||||
"requestorigin": "My Amazing Site",
|
||||
"requestoriginurl": "https://www.bestmoodlesiteever.com",
|
||||
"requestcomments": "Dear admin,<br/> I would like to request a copy of my son's user data. Thanks!",
|
||||
"forself": true,
|
||||
"datarequestsurl": "#"
|
||||
@@ -90,6 +94,14 @@
|
||||
</td>
|
||||
</tr>
|
||||
{{/forself}}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{#str}}requestorigin, tool_dataprivacy{{/str}}
|
||||
</th>
|
||||
<td>
|
||||
<a href="{{requestoriginurl}}">{{{requestorigin}}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{#str}}requestcomments, tool_dataprivacy{{/str}}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template tool_dataprivacy/data_requests_bulk_actions
|
||||
|
||||
Moodle template for the bulk action select element in the data requests page.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* options - Array of options for the select with value and name.
|
||||
* perpage - HTML content of the records per page select element.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"options": [
|
||||
{
|
||||
"value": 1,
|
||||
"name": "Approve"
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"name": "Deny"
|
||||
}
|
||||
],
|
||||
"perpage" : "<div class='singleselect'></div>"
|
||||
}
|
||||
}}
|
||||
<div class="m-t-1 d-inline-block w-100">
|
||||
<div class="pull-left">
|
||||
<select id="bulk-action" class="select custom-select">
|
||||
{{#options}}
|
||||
<option value="{{ value }}">{{ name }}</option>
|
||||
{{/options}}
|
||||
</select>
|
||||
<button class="btn btn-primary" id="confirm-bulk-action">{{# str}} confirm {{/ str}}</button>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{{{ perpage }}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,74 @@
|
||||
{{!
|
||||
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/defaults_display
|
||||
|
||||
Renders the current default category and purpose.
|
||||
|
||||
Classes required for JS:
|
||||
|
||||
Data attributes required for JS:
|
||||
|
||||
Context variables required for this template:
|
||||
* contextlevel Number - The context level.
|
||||
* categoryid Number - The category ID.
|
||||
* purposeid Number - The purpose ID.
|
||||
* category String - The category name.
|
||||
* purpose String - The purpose name.
|
||||
* canedit Boolean - Whether this is being rendered for editing purposes.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"category": "Awesome default category",
|
||||
"categoryid": 1,
|
||||
"purpose": "Awesome default purpose",
|
||||
"purposeid": 2,
|
||||
"canedit": true,
|
||||
"contextlevel": 70
|
||||
}
|
||||
}}
|
||||
<div class="row-fluid rtl-compatible m-t-1 m-b-1">
|
||||
<div class="col-md-9 span9">
|
||||
<div class="row-fluid rtl-compatible m-t-1 m-b-1">
|
||||
<div class="col-md-3 span3">
|
||||
<strong>{{#str}}category, tool_dataprivacy{{/str}}</strong>
|
||||
</div>
|
||||
<div class="col-md-9 span9">
|
||||
{{category}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid rtl-compatible m-t-1 m-b-1">
|
||||
<div class="col-md-3 span3">
|
||||
<strong>{{#str}}purpose, tool_dataprivacy{{/str}}</strong>
|
||||
</div>
|
||||
<div class="col-md-9 span9">
|
||||
{{purpose}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 span3 align-self-center">
|
||||
{{#canedit}}
|
||||
<button class="btn btn-default" {{!
|
||||
}}data-action="edit-level-defaults" {{!
|
||||
}}data-contextlevel="{{contextlevel}}" {{!
|
||||
}}data-category="{{categoryid}}" {{!
|
||||
}}data-purpose="{{purposeid}}">
|
||||
{{#str}}edit{{/str}}
|
||||
</button>
|
||||
{{/canedit}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,147 @@
|
||||
{{!
|
||||
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/defaults_page
|
||||
|
||||
Manage data registry defaults.
|
||||
|
||||
Classes required for JS:
|
||||
|
||||
Data attributes required for JS:
|
||||
|
||||
Context variables required for this template:
|
||||
* contextlevel Number - The context level.
|
||||
* modecoursecat Boolean - Whether we're displaying defaults for course categories.
|
||||
* modecourse Boolean - Whether we're displaying defaults for courses.
|
||||
* modemodule Boolean - Whether we're displaying defaults for activity modules.
|
||||
* modeblock Boolean - Whether we're displaying defaults for blocks.
|
||||
* coursecaturl String - The URL for the course category defaults tab.
|
||||
* courseurl String - The URL for the course defaults tab.
|
||||
* moduleurl String - The URL for the activity module defaults tab.
|
||||
* blockurl String - The URL for the block defaults tab.
|
||||
* purposeid Number - The purpose ID for this context level.
|
||||
* canedit Boolean - Whether this is being rendered for editing purposes.
|
||||
* categoryid Number - The ID of the default category for this context level.
|
||||
* purposeid Number - The ID of the default purpose for this context level.
|
||||
* category String - The category name.
|
||||
* purpose String - The purpose name.
|
||||
* otherdefaults Array - An array containing the defaults for the activity modules.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"contextlevel": 70,
|
||||
"modecoursecat": false,
|
||||
"modecourse": false,
|
||||
"modemodule": true,
|
||||
"modeblock": false,
|
||||
"coursecaturl": "#",
|
||||
"courseurl": "#",
|
||||
"moduleurl": "#",
|
||||
"blockurl": "#",
|
||||
"category": "Awesome default category",
|
||||
"purpose": "Awesome default purpose",
|
||||
"canedit": true,
|
||||
"otherdefaults": [
|
||||
{
|
||||
"name": "Assignment",
|
||||
"category": "Category for activity modules",
|
||||
"purpose": "Assessments"
|
||||
},
|
||||
{
|
||||
"name": "Forum",
|
||||
"category": "Category for activity modules",
|
||||
"purpose": "Social interactions"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-tabs card-header-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#modecoursecat}}active{{/modecoursecat}}" href="{{coursecaturl}}">{{#str}}categories{{/str}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#modecourse}}active{{/modecourse}}" href="{{courseurl}}">{{#str}}courses{{/str}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#modemodule}}active{{/modemodule}}" href="{{moduleurl}}">{{#str}}activitymodules{{/str}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#modeblock}}active{{/modeblock}}" href="{{blockurl}}">{{#str}}blocks{{/str}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body context-level-view">
|
||||
<div class="alert alert-primary" role="alert">
|
||||
{{#str}}defaultsinfo, tool_dataprivacy{{/str}}
|
||||
</div>
|
||||
<h4 class="card-title" id="defaults-header">
|
||||
{{#modecoursecat}}{{#str}}categories{{/str}}{{/modecoursecat}}
|
||||
{{#modecourse}}{{#str}}courses{{/str}}{{/modecourse}}
|
||||
{{#modemodule}}{{#str}}activitymodules{{/str}}{{/modemodule}}
|
||||
{{#modeblock}}{{#str}}blocks{{/str}}{{/modeblock}}
|
||||
</h4>
|
||||
<div>
|
||||
{{> tool_dataprivacy/defaults_display}}
|
||||
{{#canedit}}
|
||||
{{#modemodule}}
|
||||
<button class="btn btn-primary" data-action="new-activity-defaults" data-contextlevel="{{contextlevel}}">
|
||||
{{#str}}addnewdefaults, tool_dataprivacy{{/str}}
|
||||
</button>
|
||||
{{/modemodule}}
|
||||
{{/canedit}}
|
||||
{{#modemodule}}
|
||||
<table class="m-t-1 table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th scope="col">{{#str}}category, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}purpose, tool_dataprivacy{{/str}}</th>
|
||||
{{#canedit}}
|
||||
<th scope="col">{{#str}}actions{{/str}}</th>
|
||||
{{/canedit}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#otherdefaults}}
|
||||
<tr>
|
||||
<th scope="row">{{name}}</th>
|
||||
<td>{{category}}</td>
|
||||
<td>{{purpose}}</td>
|
||||
{{#canedit}}
|
||||
<td>
|
||||
{{#actions}}
|
||||
{{> core/action_menu_link}}
|
||||
{{/actions}}
|
||||
</td>
|
||||
{{/canedit}}
|
||||
</tr>
|
||||
{{/otherdefaults}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/modemodule}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#js}}
|
||||
// Initialise the JS.
|
||||
require(['tool_dataprivacy/defaultsactions'], function(ActionsMod) {
|
||||
ActionsMod.init();
|
||||
});
|
||||
{{/js}}
|
||||
@@ -0,0 +1,41 @@
|
||||
{{!
|
||||
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/delete_activity_defaults
|
||||
|
||||
Renders the confirmation.
|
||||
|
||||
Classes required for JS:
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* activityname String - The activity name.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"activityname": "Assignment"
|
||||
}
|
||||
}}
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
{{#str}}defaultswarninginfo, tool_dataprivacy{{/str}}
|
||||
</div>
|
||||
<div>
|
||||
{{#str}}deletedefaultsconfirmation, tool_dataprivacy, {{activityname}}{{/str}}
|
||||
</div>
|
||||
@@ -54,36 +54,49 @@
|
||||
}}
|
||||
|
||||
{{#navigation}}
|
||||
{{> core/action_link}}
|
||||
<div class="m-b-1">
|
||||
{{> core/action_link}}
|
||||
</div>
|
||||
{{/navigation}}
|
||||
|
||||
<div data-region="purposes" class="m-t-3 m-b-1">
|
||||
<h3>{{#str}}purposes, tool_dataprivacy{{/str}}</h3>
|
||||
<p>
|
||||
{{#str}}purposeoverview, tool_dataprivacy{{/str}}
|
||||
</p>
|
||||
|
||||
<div data-region="purposes" class="m-b-1">
|
||||
<div class="m-y-1">
|
||||
<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>
|
||||
<table class="generaltable fullwidth">
|
||||
<table class="table table-striped table-hover">
|
||||
<caption class="accesshide">{{#str}}purposeslist, tool_dataprivacy{{/str}}</caption>
|
||||
<thead>
|
||||
<tr style="display: flex;">
|
||||
<th scope="col" class="col-md-2">{{#str}}name{{/str}}</th>
|
||||
<th scope="col" class="col-md-3">{{#str}}description{{/str}}</th>
|
||||
<th scope="col" class="col-md-2">{{#str}}lawfulbases, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col" class="col-md-2">{{#str}}sensitivedatareasons, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col" class="col-md-1">{{#str}}retentionperiod, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col" class="col-md-1">{{#str}}protected, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col" class="col-md-1">{{#str}}actions{{/str}}</th>
|
||||
<tr>
|
||||
<th scope="col" class="col-name-description">{{#str}}name{{/str}}</th>
|
||||
<th scope="col">{{#str}}lawfulbases, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}sensitivedatareasons, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}retentionperiod, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}protected, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}roleoverrides, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}actions{{/str}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#purposes}}
|
||||
<tr data-purposeid="{{id}}">
|
||||
<td class="col-md-2">{{{name}}}</td>
|
||||
<td class="col-md-3">{{{description}}}</td>
|
||||
<td class="col-md-2">
|
||||
<ul>
|
||||
<td>
|
||||
<dl>
|
||||
<dt>
|
||||
{{{name}}}
|
||||
</dt>
|
||||
<dd>
|
||||
{{{description}}}
|
||||
</dd>
|
||||
</dl>
|
||||
</td>
|
||||
<td>
|
||||
<ul class="list-unstyled">
|
||||
{{#formattedlawfulbases}}
|
||||
<li>
|
||||
<span>{{name}}{{# pix }} i/info, core, {{description}} {{/ pix }}</span>
|
||||
@@ -91,8 +104,8 @@
|
||||
{{/formattedlawfulbases}}
|
||||
</ul>
|
||||
</td>
|
||||
<td class="col-md-2">
|
||||
<ul>
|
||||
<td>
|
||||
<ul class="list-unstyled">
|
||||
{{#formattedsensitivedatareasons}}
|
||||
<li>
|
||||
<span>{{name}}{{# pix }} i/info, core, {{description}} {{/ pix }}</span>
|
||||
@@ -100,8 +113,8 @@
|
||||
{{/formattedsensitivedatareasons}}
|
||||
</ul>
|
||||
</td>
|
||||
<td class="col-md-1">{{formattedretentionperiod}}</td>
|
||||
<td class="col-md-1">
|
||||
<td>{{formattedretentionperiod}}</td>
|
||||
<td>
|
||||
{{#protected}}
|
||||
{{#pix}}i/checked, core, {{#str}}yes{{/str}}{{/pix}}
|
||||
{{/protected}}
|
||||
@@ -109,7 +122,15 @@
|
||||
{{#str}}no{{/str}}
|
||||
{{/protected}}
|
||||
</td>
|
||||
<td class="col-md-1">
|
||||
<td>
|
||||
{{#roleoverrides}}
|
||||
{{#str}}yes{{/str}}
|
||||
{{/roleoverrides}}
|
||||
{{^roleoverrides}}
|
||||
{{#str}}no{{/str}}
|
||||
{{/roleoverrides}}
|
||||
</td>
|
||||
<td>
|
||||
{{#actions}}
|
||||
{{> core/action_menu}}
|
||||
{{/actions}}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
{{!
|
||||
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/summary
|
||||
|
||||
Summary
|
||||
|
||||
Classes required for JS:
|
||||
|
||||
Data attributes required for JS:
|
||||
|
||||
Context variables required for this template:
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
|
||||
"contexts": [
|
||||
{
|
||||
"contextname": "Site",
|
||||
"category":
|
||||
{
|
||||
"name": "Test category",
|
||||
"description": "<p>Description for category</p>"
|
||||
},
|
||||
"purpose":
|
||||
{
|
||||
"name": "Test purpose",
|
||||
"description": "<p>Description for purpose</p>",
|
||||
"lawfulbases": "gdpr_art_6_1_c",
|
||||
"sensitivedatareasons": "gdpr_art_9_2_f",
|
||||
"formattedlawfulbases": [
|
||||
{
|
||||
"name": "Lawful base 1(a)",
|
||||
"description": "We need your information"
|
||||
},
|
||||
{
|
||||
"name": "Lawful base 1(b)",
|
||||
"description": "We really do need your information"
|
||||
}
|
||||
],
|
||||
"formattedsensitivedatareasons": [
|
||||
{
|
||||
"name": "Sensitive data reason number 1",
|
||||
"description": "Number 1"
|
||||
},
|
||||
{
|
||||
"name": "Sensitive data reason number 1",
|
||||
"description": "Number 2"
|
||||
}
|
||||
],
|
||||
"formattedretentionperiod": "10 Years"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<h2>{{#str}}dataretentionsummary, tool_dataprivacy{{/str}}</h2>
|
||||
<p>{{#str}}dataretentionexplanation, tool_dataprivacy{{/str}}</p>
|
||||
<div>
|
||||
{{#contexts}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><h3>{{contextname}}</h3></div>
|
||||
<div class="card-body p-l-2 p-r-2">
|
||||
|
||||
{{#category.name}}
|
||||
<h4>{{#str}}category, tool_dataprivacy{{/str}}</h4>
|
||||
<dl>
|
||||
<dt>{{category.name}}</dt>
|
||||
<dd>{{{category.description}}}</dd>
|
||||
</dl>
|
||||
<hr />
|
||||
{{/category.name}}
|
||||
<h4>{{#str}}purpose, tool_dataprivacy{{/str}}</h4>
|
||||
<dl>
|
||||
<dt>{{purpose.name}}</dt>
|
||||
<dd>{{{purpose.description}}}</dd>
|
||||
<dt>{{#str}}retentionperiod, tool_dataprivacy{{/str}}</dt>
|
||||
<dd>{{purpose.formattedretentionperiod}}</dd>
|
||||
</dl>
|
||||
{{#purpose.lawfulbases}}
|
||||
<table class="table table-bordered">
|
||||
<thead><tr><th colspan="2">{{#str}}lawfulbases, tool_dataprivacy{{/str}}</th></tr></thead>
|
||||
<tbody>
|
||||
{{#purpose.formattedlawfulbases}}
|
||||
<tr>
|
||||
<td>{{name}}</td>
|
||||
<td>{{description}}</td>
|
||||
</tr>
|
||||
{{/purpose.formattedlawfulbases}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/purpose.lawfulbases}}
|
||||
{{#purpose.sensitivedatareasons}}
|
||||
<table class="table table-bordered">
|
||||
<thead><tr><th colspan="2">{{#str}}sensitivedatareasons, tool_dataprivacy{{/str}}</th></tr></thead>
|
||||
<tbody>
|
||||
{{#purpose.formattedsensitivedatareasons}}
|
||||
<tr>
|
||||
<td>{{name}}</td>
|
||||
<td>{{description}}</td>
|
||||
</tr>
|
||||
{{/purpose.formattedsensitivedatareasons}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/purpose.sensitivedatareasons}}
|
||||
</div>
|
||||
</div>
|
||||
{{/contexts}}
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,284 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Step definitions to generate database fixtures for the data privacy tool.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @category test
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
|
||||
|
||||
use Behat\Gherkin\Node\TableNode as TableNode;
|
||||
use Behat\Behat\Tester\Exception\PendingException as PendingException;
|
||||
use tool_dataprivacy\api;
|
||||
|
||||
/**
|
||||
* Step definitions to generate database fixtures for the data privacy tool.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @category test
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_tool_dataprivacy extends behat_base {
|
||||
|
||||
/**
|
||||
* Each element specifies:
|
||||
* - The data generator suffix used.
|
||||
* - The required fields.
|
||||
* - The mapping between other elements references and database field names.
|
||||
* @var array
|
||||
*/
|
||||
protected static $elements = array(
|
||||
'categories' => array(
|
||||
'datagenerator' => 'category',
|
||||
'required' => array()
|
||||
),
|
||||
'purposes' => array(
|
||||
'datagenerator' => 'purpose',
|
||||
'required' => array()
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates the specified element. More info about available elements in http://docs.moodle.org/dev/Acceptance_testing#Fixtures.
|
||||
*
|
||||
* @Given /^the following data privacy "(?P<element_string>(?:[^"]|\\")*)" exist:$/
|
||||
*
|
||||
* @param string $elementname The name of the entity to add
|
||||
* @param TableNode $data
|
||||
*/
|
||||
public function the_following_data_categories_exist($elementname, TableNode $data) {
|
||||
|
||||
// Now that we need them require the data generators.
|
||||
require_once(__DIR__.'/../../../../../lib/phpunit/classes/util.php');
|
||||
|
||||
if (empty(self::$elements[$elementname])) {
|
||||
throw new PendingException($elementname . ' data generator is not implemented');
|
||||
}
|
||||
|
||||
$datagenerator = testing_util::get_data_generator();
|
||||
$dataprivacygenerator = $datagenerator->get_plugin_generator('tool_dataprivacy');
|
||||
|
||||
$elementdatagenerator = self::$elements[$elementname]['datagenerator'];
|
||||
$requiredfields = self::$elements[$elementname]['required'];
|
||||
if (!empty(self::$elements[$elementname]['switchids'])) {
|
||||
$switchids = self::$elements[$elementname]['switchids'];
|
||||
}
|
||||
|
||||
foreach ($data->getHash() as $elementdata) {
|
||||
|
||||
// Check if all the required fields are there.
|
||||
foreach ($requiredfields as $requiredfield) {
|
||||
if (!isset($elementdata[$requiredfield])) {
|
||||
throw new Exception($elementname . ' requires the field ' . $requiredfield . ' to be specified');
|
||||
}
|
||||
}
|
||||
|
||||
// Switch from human-friendly references to ids.
|
||||
if (isset($switchids)) {
|
||||
foreach ($switchids as $element => $field) {
|
||||
$methodname = 'get_' . $element . '_id';
|
||||
|
||||
// Not all the switch fields are required, default vars will be assigned by data generators.
|
||||
if (isset($elementdata[$element])) {
|
||||
// Temp $id var to avoid problems when $element == $field.
|
||||
$id = $this->{$methodname}($elementdata[$element]);
|
||||
unset($elementdata[$element]);
|
||||
$elementdata[$field] = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preprocess the entities that requires a special treatment.
|
||||
if (method_exists($this, 'preprocess_' . $elementdatagenerator)) {
|
||||
$elementdata = $this->{'preprocess_' . $elementdatagenerator}($elementdata);
|
||||
}
|
||||
|
||||
// Creates element.
|
||||
$methodname = 'create_' . $elementdatagenerator;
|
||||
if (method_exists($dataprivacygenerator, $methodname)) {
|
||||
// Using data generators directly.
|
||||
$dataprivacygenerator->{$methodname}($elementdata);
|
||||
|
||||
} else if (method_exists($this, 'process_' . $elementdatagenerator)) {
|
||||
// Using an alternative to the direct data generator call.
|
||||
$this->{'process_' . $elementdatagenerator}($elementdata);
|
||||
} else {
|
||||
throw new PendingException($elementname . ' data generator is not implemented');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the data category and data storage purpose for the site.
|
||||
*
|
||||
* @Given /^I set the site category and purpose to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
|
||||
*
|
||||
* @param string $category The ID of the category to be set for the instance.
|
||||
* @param string $purpose The ID of the purpose to be set for the instance.
|
||||
*/
|
||||
public function i_set_the_site_category_and_purpose($category, $purpose) {
|
||||
$category = \tool_dataprivacy\category::get_record(['name' => $category]);
|
||||
$purpose = \tool_dataprivacy\purpose::get_record(['name' => $purpose]);
|
||||
$data = (object)[
|
||||
'contextlevel' => CONTEXT_SYSTEM,
|
||||
'categoryid' => $category->get('id'),
|
||||
'purposeid' => $purpose->get('id'),
|
||||
];
|
||||
api::set_contextlevel($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the data category and data storage purpose for a course category instance.
|
||||
*
|
||||
* @Given /^I set the category and purpose for the course category "(?P<categoryname_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
|
||||
*
|
||||
* @param string $name The instance name. It should match the name or the idnumber.
|
||||
* @param string $category The ID of the category to be set for the instance.
|
||||
* @param string $purpose The ID of the purpose to be set for the instance.
|
||||
*/
|
||||
public function i_set_the_category_and_purpose_for_course_category($name, $category, $purpose) {
|
||||
global $DB;
|
||||
|
||||
$params = [
|
||||
'name' => $name,
|
||||
'idnumber' => $name,
|
||||
];
|
||||
$select = 'name = :name OR idnumber = :idnumber';
|
||||
$coursecatid = $DB->get_field_select('course_categories', 'id', $select, $params, MUST_EXIST);
|
||||
$context = context_coursecat::instance($coursecatid);
|
||||
|
||||
$this->set_category_and_purpose($context->id, $category, $purpose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the data category and data storage purpose for a course instance.
|
||||
*
|
||||
* @Given /^I set the category and purpose for the course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
|
||||
*
|
||||
* @param string $name The instance name. It should match the fullname or the shortname, or the idnumber.
|
||||
* @param string $category The ID of the category to be set for the instance.
|
||||
* @param string $purpose The ID of the purpose to be set for the instance.
|
||||
*/
|
||||
public function i_set_the_category_and_purpose_for_course($name, $category, $purpose) {
|
||||
global $DB;
|
||||
|
||||
$params = [
|
||||
'shortname' => $name,
|
||||
'fullname' => $name,
|
||||
'idnumber' => $name,
|
||||
];
|
||||
$select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
|
||||
$courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
|
||||
$context = context_course::instance($courseid);
|
||||
|
||||
$this->set_category_and_purpose($context->id, $category, $purpose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the data category and data storage purpose for a course instance.
|
||||
*
|
||||
* @Given /^I set the category and purpose for the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" in course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
|
||||
*
|
||||
* @param string $name The instance name. It should match the name of the activity.
|
||||
* @param string $type The activity type. E.g. assign, quiz, forum, etc.
|
||||
* @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
|
||||
* @param string $category The ID of the category to be set for the instance.
|
||||
* @param string $purpose The ID of the purpose to be set for the instance.
|
||||
*/
|
||||
public function i_set_the_category_and_purpose_for_activity($name, $type, $coursename, $category, $purpose) {
|
||||
global $DB;
|
||||
|
||||
$params = [
|
||||
'shortname' => $coursename,
|
||||
'fullname' => $coursename,
|
||||
'idnumber' => $coursename,
|
||||
];
|
||||
$select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
|
||||
$courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
|
||||
|
||||
$cmid = null;
|
||||
$cms = get_coursemodules_in_course($type, $courseid);
|
||||
foreach ($cms as $cm) {
|
||||
if ($cm->name === $name || $cm->idnumber === $name) {
|
||||
$cmid = $cm->id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($cmid === null) {
|
||||
throw new coding_exception("Activity module '{$name}' of type '{$type}' not found!");
|
||||
}
|
||||
$context = context_module::instance($cmid);
|
||||
|
||||
$this->set_category_and_purpose($context->id, $category, $purpose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the data category and data storage purpose for a course instance.
|
||||
*
|
||||
* @Given /^I set the category and purpose for the "(?P<blockname_string>(?:[^"]|\\")*)" block in the "(?P<coursename_string>(?:[^"]|\\")*)" course to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
|
||||
*
|
||||
* @param string $name The instance name. It should match the name of the block. (e.g. online_users)
|
||||
* @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
|
||||
* @param string $category The ID of the category to be set for the instance.
|
||||
* @param string $purpose The ID of the purpose to be set for the instance.
|
||||
*/
|
||||
public function i_set_the_category_and_purpose_for_block($name, $coursename, $category, $purpose) {
|
||||
global $DB;
|
||||
|
||||
$params = [
|
||||
'shortname' => $coursename,
|
||||
'fullname' => $coursename,
|
||||
'idnumber' => $coursename,
|
||||
];
|
||||
$select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
|
||||
$courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
|
||||
|
||||
// Fetch the course context.
|
||||
$coursecontext = context_course::instance($courseid);
|
||||
|
||||
// Fetch the block record and context.
|
||||
$blockid = $DB->get_field('block_instances', 'id', ['blockname' => $name, 'parentcontextid' => $coursecontext->id]);
|
||||
$context = context_block::instance($blockid);
|
||||
|
||||
// Set the category and purpose.
|
||||
$this->set_category_and_purpose($context->id, $category, $purpose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the category and purpose for a context instance.
|
||||
*
|
||||
* @param int $contextid The context ID.
|
||||
* @param int $categoryname The category name.
|
||||
* @param int $purposename The purpose name.
|
||||
* @throws coding_exception
|
||||
*/
|
||||
protected function set_category_and_purpose($contextid, $categoryname, $purposename) {
|
||||
$category = \tool_dataprivacy\category::get_record(['name' => $categoryname]);
|
||||
$purpose = \tool_dataprivacy\purpose::get_record(['name' => $purposename]);
|
||||
|
||||
api::set_context_instance((object) [
|
||||
'contextid' => $contextid,
|
||||
'purposeid' => $purpose->get('id'),
|
||||
'categoryid' => $category->get('id'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,13 @@ Feature: Data delete from the privacy API
|
||||
| parent | tired | User | victim |
|
||||
And the following config values are set as admin:
|
||||
| contactdataprotectionofficer | 1 | tool_dataprivacy |
|
||||
And the following data privacy "categories" exist:
|
||||
| name |
|
||||
| Site category |
|
||||
And the following data privacy "purposes" exist:
|
||||
| name | retentionperiod |
|
||||
| Site purpose | P10Y |
|
||||
And I set the site category and purpose to "Site category" and "Site purpose"
|
||||
|
||||
@javascript
|
||||
Scenario: As admin, delete a user and their data
|
||||
@@ -30,7 +37,7 @@ Feature: Data delete from the privacy API
|
||||
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 "User" 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"
|
||||
@@ -38,7 +45,7 @@ Feature: Data delete from the privacy API
|
||||
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 open the action menu in "Victim User 1" "table_row"
|
||||
And I follow "Approve request"
|
||||
And I press "Approve request"
|
||||
And I should see "Approved" in the "Victim User 1" "table_row"
|
||||
@@ -67,7 +74,7 @@ Feature: Data delete from the privacy API
|
||||
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 open the action menu in "Victim User 1" "table_row"
|
||||
And I follow "Approve request"
|
||||
And I press "Approve request"
|
||||
|
||||
@@ -93,7 +100,7 @@ Feature: Data delete from the privacy API
|
||||
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 "User" 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"
|
||||
@@ -105,7 +112,7 @@ Feature: Data delete from the privacy API
|
||||
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 open the action menu in "Victim User 1" "table_row"
|
||||
And I follow "Approve request"
|
||||
And I press "Approve request"
|
||||
|
||||
|
||||
@@ -21,34 +21,41 @@ Feature: Data export from the privacy API
|
||||
And the following config values are set as admin:
|
||||
| contactdataprotectionofficer | 1 | tool_dataprivacy |
|
||||
| privacyrequestexpiry | 55 | tool_dataprivacy |
|
||||
And the following data privacy "categories" exist:
|
||||
| name |
|
||||
| Site category |
|
||||
And the following data privacy "purposes" exist:
|
||||
| name | retentionperiod |
|
||||
| Site purpose | P10Y |
|
||||
And I set the site category and purpose to "Site category" and "Site purpose"
|
||||
|
||||
@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 set the field "User" 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 open the action menu in "Victim User 1" "table_row"
|
||||
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 I open the action menu in "Victim User 1" "table_row"
|
||||
And following "Download" should download between "1" and "130000" 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 open the action menu in "Victim User 1" "table_row"
|
||||
And I should not see "Download"
|
||||
|
||||
@javascript
|
||||
@@ -67,7 +74,7 @@ Feature: Data export from the privacy API
|
||||
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 open the action menu in "Victim User 1" "table_row"
|
||||
And I follow "Approve request"
|
||||
And I press "Approve request"
|
||||
|
||||
@@ -79,8 +86,8 @@ Feature: Data export from the privacy API
|
||||
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 I open the action menu in "Victim User 1" "table_row"
|
||||
And following "Download" should download between "1" and "130000" bytes
|
||||
|
||||
And the following config values are set as admin:
|
||||
| privacyrequestexpiry | 1 | tool_dataprivacy |
|
||||
@@ -96,7 +103,7 @@ Feature: Data export from the privacy API
|
||||
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 "User" 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"
|
||||
@@ -107,7 +114,7 @@ Feature: Data export from the privacy API
|
||||
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 open the action menu in "Victim User 1" "table_row"
|
||||
And I follow "Approve request"
|
||||
And I press "Approve request"
|
||||
|
||||
@@ -119,8 +126,8 @@ Feature: Data export from the privacy API
|
||||
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 I open the action menu in "Victim User 1" "table_row"
|
||||
And following "Download" should download between "1" and "130000" bytes
|
||||
|
||||
And the following config values are set as admin:
|
||||
| privacyrequestexpiry | 1 | tool_dataprivacy |
|
||||
|
||||
@@ -7,7 +7,7 @@ Feature: Manage data categories
|
||||
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 open the action menu in "region-main" "region"
|
||||
And I choose "Categories" in the open action menu
|
||||
And I press "Add category"
|
||||
And I set the field "Name" to "Category 1"
|
||||
@@ -17,7 +17,7 @@ Feature: Manage data categories
|
||||
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"
|
||||
Given I open the action menu in "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"
|
||||
@@ -26,7 +26,7 @@ Feature: Manage data categories
|
||||
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"
|
||||
Given I open the action menu in "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'?"
|
||||
|
||||
@@ -35,7 +35,7 @@ Feature: Manage data requests
|
||||
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 open the action menu in "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
|
||||
@@ -43,16 +43,106 @@ Feature: Manage data requests
|
||||
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 open the action menu in "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 open the action menu in "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 open the action menu in "Jane Doe" "table_row"
|
||||
And I should see "View the request"
|
||||
But I should not see "Mark as complete"
|
||||
|
||||
@javascript
|
||||
Scenario: Bulk accepting requests
|
||||
Given I log in as "student1"
|
||||
And I follow "Profile" in the user menu
|
||||
And I should see "Data requests"
|
||||
And I click on "Data requests" "link"
|
||||
And I should see "New request"
|
||||
And I click on "New request" "link"
|
||||
And I should see "Type"
|
||||
And I should see "Comments"
|
||||
And I set the field "Type" to "Export all of my personal data"
|
||||
And I set the field "Comments" to "Comment1"
|
||||
And I press "Save changes"
|
||||
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 should see "Data requests"
|
||||
And I click on "Data requests" "link"
|
||||
And I should see "New request"
|
||||
And I click on "New request" "link"
|
||||
And I should see "Type"
|
||||
And I should see "Comments"
|
||||
And I set the field "Type" to "Export all of my personal data"
|
||||
And I set the field "Comments" to "Comment2"
|
||||
And I press "Save changes"
|
||||
And I should see "Your request has been submitted to the privacy officer"
|
||||
And I log out
|
||||
And I trigger cron
|
||||
And I log in as "admin"
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
And I should see "Comment1" in the "John Doe" "table_row"
|
||||
And I should see "Awaiting approval" in the "John Doe" "table_row"
|
||||
And I should see "Comment2" in the "Jane Doe" "table_row"
|
||||
And I should see "Awaiting approval" in the "Jane Doe" "table_row"
|
||||
And I click on ".selectrequests" "css_element" in the "John Doe" "table_row"
|
||||
And I click on ".selectrequests" "css_element" in the "Jane Doe" "table_row"
|
||||
And I set the field with xpath "//select[@id='bulk-action']" to "Approve"
|
||||
And I press "Confirm"
|
||||
And I should see "Approve requests"
|
||||
And I should see "Do you really want to bulk approve the selected data requests?"
|
||||
When I press "Approve requests"
|
||||
Then I should see "Approved" in the "John Doe" "table_row"
|
||||
And I should see "Approved" in the "Jane Doe" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Bulk denying requests
|
||||
Given I log in as "student1"
|
||||
And I follow "Profile" in the user menu
|
||||
And I should see "Data requests"
|
||||
And I click on "Data requests" "link"
|
||||
And I should see "New request"
|
||||
And I click on "New request" "link"
|
||||
And I should see "Type"
|
||||
And I should see "Comments"
|
||||
And I set the field "Type" to "Export all of my personal data"
|
||||
And I set the field "Comments" to "Comment1"
|
||||
And I press "Save changes"
|
||||
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 should see "Data requests"
|
||||
And I click on "Data requests" "link"
|
||||
And I should see "New request"
|
||||
And I click on "New request" "link"
|
||||
And I should see "Type"
|
||||
And I should see "Comments"
|
||||
And I set the field "Type" to "Export all of my personal data"
|
||||
And I set the field "Comments" to "Comment2"
|
||||
And I press "Save changes"
|
||||
And I should see "Your request has been submitted to the privacy officer"
|
||||
And I log out
|
||||
And I trigger cron
|
||||
And I log in as "admin"
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
And I should see "Comment1" in the "John Doe" "table_row"
|
||||
And I should see "Awaiting approval" in the "John Doe" "table_row"
|
||||
And I should see "Comment2" in the "Jane Doe" "table_row"
|
||||
And I should see "Awaiting approval" in the "Jane Doe" "table_row"
|
||||
And I click on ".selectrequests" "css_element" in the "John Doe" "table_row"
|
||||
And I click on ".selectrequests" "css_element" in the "Jane Doe" "table_row"
|
||||
And I set the field with xpath "//select[@id='bulk-action']" to "Deny"
|
||||
And I press "Confirm"
|
||||
And I should see "Deny requests"
|
||||
And I should see "Do you really want to bulk deny the selected data requests?"
|
||||
When I press "Deny requests"
|
||||
Then I should see "Rejected" in the "John Doe" "table_row"
|
||||
And I should see "Rejected" in the "Jane Doe" "table_row"
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
@tool @tool_dataprivacy @javascript
|
||||
Feature: Manage data registry defaults
|
||||
As the privacy officer
|
||||
In order to manage the data registry
|
||||
I need to be able to manage the default data categories and data storage purposes for various context levels.
|
||||
|
||||
Background:
|
||||
Given I log in as "admin"
|
||||
And the following "categories" exist:
|
||||
| name | idnumber | category |
|
||||
| Science and technology | scitech | |
|
||||
| Physics | st-phys | scitech |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Fundamentals of physics 1 | Physics 101 | st-phys |
|
||||
And the following "activities" exist:
|
||||
| activity | name | idnumber | course |
|
||||
| assign | Assignment 1 | assign1 | Physics 101 |
|
||||
| forum | Forum 1 | forum1 | Physics 101 |
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| online_users | Course | Physics 101 | course-view-* | site-post |
|
||||
And the following data privacy "categories" exist:
|
||||
| name |
|
||||
| Site category |
|
||||
| Category 1 |
|
||||
| Category 2 |
|
||||
And the following data privacy "purposes" exist:
|
||||
| name | retentionperiod |
|
||||
| Site purpose | P10Y |
|
||||
| Purpose 1 | P3Y |
|
||||
| Purpose 2 | P5Y |
|
||||
And I set the site category and purpose to "Site category" and "Site purpose"
|
||||
|
||||
# Setting a default for course categories should apply to everything beneath that category.
|
||||
Scenario: Set course category data registry defaults
|
||||
Given I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Set defaults" "link"
|
||||
And I should see "Inherit"
|
||||
And I press "Edit"
|
||||
And I set the field "Category" to "Category 1"
|
||||
And I set the field "Purpose" to "Purpose 1"
|
||||
When I press "Save changes"
|
||||
Then I should see "Category 1"
|
||||
And I should see "Purpose 1"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Science and technology" "link"
|
||||
And I wait until the page is ready
|
||||
And the field "categoryid" matches value "Not set (use the default value)"
|
||||
And the field "purposeid" matches value "Not set (use the default value)"
|
||||
And I should see "3 years"
|
||||
And I click on "Courses" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Physics 101" "link"
|
||||
And I wait until the page is ready
|
||||
And I should see "3 years"
|
||||
And I click on "Activities and resources" "link"
|
||||
And I wait until the page is ready
|
||||
And I should see "3 years"
|
||||
And I click on "Assignment 1 (Assignment)" "link"
|
||||
And I wait until the page is ready
|
||||
And I should see "3 years"
|
||||
|
||||
# When Setting a default for course categories, and overriding a specific category, only that category and its
|
||||
# children will be overridden.
|
||||
# If any child is a course category, it will get the default.
|
||||
Scenario: Set course category data registry defaults with override
|
||||
Given I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Set defaults" "link"
|
||||
And I press "Edit"
|
||||
And I set the field "Category" to "Category 1"
|
||||
And I set the field "Purpose" to "Purpose 1"
|
||||
And I press "Save changes"
|
||||
And I should see "Category 1"
|
||||
And I should see "Purpose 1"
|
||||
And I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
|
||||
When I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Science and technology" "link"
|
||||
And I wait until the page is ready
|
||||
Then the field "categoryid" matches value "Category 2"
|
||||
And the field "purposeid" matches value "Purpose 2"
|
||||
And I should see "5 years"
|
||||
And I click on "Courses" "link"
|
||||
And I wait until the page is ready
|
||||
# Physics 101 is also a category, so it will get the category default.
|
||||
And I click on "Physics 101" "link"
|
||||
And I wait until the page is ready
|
||||
And I should see "3 years"
|
||||
And I click on "Activities and resources" "link"
|
||||
And I wait until the page is ready
|
||||
And I should see "3 years"
|
||||
And I click on "Assignment 1 (Assignment)" "link"
|
||||
And I wait until the page is ready
|
||||
And I should see "3 years"
|
||||
|
||||
# When overriding a specific category, only that category and its children will be overridden.
|
||||
Scenario: Set course category data registry defaults with override
|
||||
Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
|
||||
When I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Science and technology" "link"
|
||||
And I wait until the page is ready
|
||||
Then the field "categoryid" matches value "Category 2"
|
||||
And the field "purposeid" matches value "Purpose 2"
|
||||
And I should see "5 years"
|
||||
And I click on "Courses" "link"
|
||||
And I wait until the page is ready
|
||||
# Physics 101 is also a category, so it will get the category default.
|
||||
And I click on "Physics 101" "link"
|
||||
And I wait until the page is ready
|
||||
And I should see "5 years"
|
||||
And I click on "Activities and resources" "link"
|
||||
And I wait until the page is ready
|
||||
And I should see "5 years"
|
||||
And I click on "Assignment 1 (Assignment)" "link"
|
||||
And I wait until the page is ready
|
||||
And I should see "5 years"
|
||||
|
||||
# Resetting instances removes custom values.
|
||||
Scenario: Set course category data registry defaults with override
|
||||
Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Set defaults" "link"
|
||||
And I press "Edit"
|
||||
And I set the field "Category" to "Category 1"
|
||||
And I set the field "Purpose" to "Purpose 1"
|
||||
When I click on "Reset instances with custom values" "checkbox"
|
||||
And I press "Save changes"
|
||||
And I should see "Category 1"
|
||||
And I should see "Purpose 1"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Science and technology" "link"
|
||||
And I wait until the page is ready
|
||||
Then the field "categoryid" matches value "Not set (use the default value)"
|
||||
And the field "purposeid" matches value "Not set (use the default value)"
|
||||
And I should see "3 years"
|
||||
|
||||
Scenario: Set course data registry defaults
|
||||
Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Set defaults" "link"
|
||||
And I click on "Courses" "link"
|
||||
And I should see "Inherit"
|
||||
And I should not see "Add a new module default"
|
||||
And I press "Edit"
|
||||
And I set the field "Category" to "Category 1"
|
||||
And I set the field "Purpose" to "Purpose 1"
|
||||
When I press "Save changes"
|
||||
Then I should see "Category 1"
|
||||
And I should see "Purpose 1"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Science and technology" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Courses" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Physics 101" "link"
|
||||
And I wait until the page is ready
|
||||
And the field "categoryid" matches value "Category 2"
|
||||
And the field "purposeid" matches value "Purpose 2"
|
||||
And I should see "5 years (after the course end date)"
|
||||
And I click on "Activities and resources" "link"
|
||||
And I wait until the page is ready
|
||||
And I should see "5 years"
|
||||
And I click on "Assignment 1 (Assignment)" "link"
|
||||
And I wait until the page is ready
|
||||
And I should see "5 years"
|
||||
|
||||
Scenario: Set course data registry defaults with override
|
||||
Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Set defaults" "link"
|
||||
And I click on "Courses" "link"
|
||||
And I should see "Inherit"
|
||||
And I should not see "Add a new module default"
|
||||
And I press "Edit"
|
||||
And I set the field "Category" to "Category 1"
|
||||
And I set the field "Purpose" to "Purpose 1"
|
||||
And I click on "Reset instances with custom values" "checkbox"
|
||||
When I press "Save changes"
|
||||
Then I should see "Category 1"
|
||||
And I should see "Purpose 1"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Science and technology" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Courses" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Physics 101" "link"
|
||||
And I wait until the page is ready
|
||||
And the field "categoryid" matches value "Not set (use the default value)"
|
||||
And the field "purposeid" matches value "Not set (use the default value)"
|
||||
And I should see "3 years (after the course end date)"
|
||||
And I click on "Activities and resources" "link"
|
||||
And I wait until the page is ready
|
||||
And I should see "3 years"
|
||||
And I click on "Assignment 1 (Assignment)" "link"
|
||||
And I wait until the page is ready
|
||||
And I should see "3 years"
|
||||
|
||||
Scenario: Set module level data registry defaults
|
||||
Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Set defaults" "link"
|
||||
And I click on "Activity modules" "link"
|
||||
And I should see "Inherit"
|
||||
And I should see "Add a new module default"
|
||||
And I press "Edit"
|
||||
And I set the field "Category" to "Category 1"
|
||||
And I set the field "Purpose" to "Purpose 1"
|
||||
When I press "Save changes"
|
||||
Then I should see "Category 1"
|
||||
And I should see "Purpose 1"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Science and technology" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Courses" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Physics 101" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Activities and resources" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Assignment 1 (Assignment)" "link"
|
||||
And I wait until the page is ready
|
||||
And the field "categoryid" matches value "Category 2"
|
||||
And the field "purposeid" matches value "Purpose 2"
|
||||
And I should see "5 years (after the course end date)"
|
||||
|
||||
Scenario: Set module level data registry defaults with override
|
||||
Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Set defaults" "link"
|
||||
And I click on "Activity modules" "link"
|
||||
And I should see "Inherit"
|
||||
And I should see "Add a new module default"
|
||||
And I press "Edit"
|
||||
And I set the field "Category" to "Category 1"
|
||||
And I set the field "Purpose" to "Purpose 1"
|
||||
And I click on "Reset instances with custom values" "checkbox"
|
||||
When I press "Save changes"
|
||||
Then I should see "Category 1"
|
||||
And I should see "Purpose 1"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Science and technology" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Courses" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Physics 101" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Activities and resources" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Assignment 1 (Assignment)" "link"
|
||||
And I wait until the page is ready
|
||||
And the field "categoryid" matches value "Not set (use the default value)"
|
||||
And the field "purposeid" matches value "Not set (use the default value)"
|
||||
And I click on "Forum 1 (Forum)" "link"
|
||||
And I wait until the page is ready
|
||||
And the field "categoryid" matches value "Not set (use the default value)"
|
||||
And the field "purposeid" matches value "Not set (use the default value)"
|
||||
And I should see "3 years (after the course end date)"
|
||||
|
||||
Scenario: Set data registry defaults for an activity module
|
||||
Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Set defaults" "link"
|
||||
And I click on "Activity modules" "link"
|
||||
And I should see "Inherit"
|
||||
And I should see "Add a new module default"
|
||||
And I press "Add a new module default"
|
||||
And I set the field "Activity module" to "Assignment"
|
||||
And I set the field "Category" to "Category 1"
|
||||
And I set the field "Purpose" to "Purpose 1"
|
||||
When I press "Save changes"
|
||||
Then I should see "Category 1" in the "Assignment" "table_row"
|
||||
And I should see "Purpose 1" in the "Assignment" "table_row"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Science and technology" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Courses" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Physics 101" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Activities and resources" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Assignment 1 (Assignment)" "link"
|
||||
And I wait until the page is ready
|
||||
And the field "categoryid" matches value "Category 2"
|
||||
And the field "purposeid" matches value "Purpose 2"
|
||||
And I should see "5 years (after the course end date)"
|
||||
|
||||
Scenario: Set data registry defaults for an activity module with override
|
||||
Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Set defaults" "link"
|
||||
And I click on "Activity modules" "link"
|
||||
And I should see "Inherit"
|
||||
And I should see "Add a new module default"
|
||||
And I press "Add a new module default"
|
||||
And I set the field "Activity module" to "Assignment"
|
||||
And I set the field "Category" to "Category 1"
|
||||
And I set the field "Purpose" to "Purpose 1"
|
||||
And I click on "Reset instances with custom values" "checkbox"
|
||||
When I press "Save changes"
|
||||
Then I should see "Category 1" in the "Assignment" "table_row"
|
||||
And I should see "Purpose 1" in the "Assignment" "table_row"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Science and technology" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Courses" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Physics 101" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Activities and resources" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Assignment 1 (Assignment)" "link"
|
||||
And I wait until the page is ready
|
||||
And the field "categoryid" matches value "Not set (use the default value)"
|
||||
And the field "purposeid" matches value "Not set (use the default value)"
|
||||
And I should see "3 years (after the course end date)"
|
||||
|
||||
Scenario: Set block category data registry defaults
|
||||
Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Set defaults" "link"
|
||||
And I click on "Blocks" "link"
|
||||
And I should see "Inherit"
|
||||
And I should not see "Add a new module default"
|
||||
And I press "Edit"
|
||||
And I set the field "Category" to "Category 1"
|
||||
And I set the field "Purpose" to "Purpose 1"
|
||||
When I press "Save changes"
|
||||
Then I should see "Category 1"
|
||||
And I should see "Purpose 1"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Science and technology" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Courses" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Physics 101" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Blocks" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Online users" "link"
|
||||
And I wait until the page is ready
|
||||
And the field "categoryid" matches value "Category 2"
|
||||
And the field "purposeid" matches value "Purpose 2"
|
||||
And I should see "5 years (after the course end date)"
|
||||
|
||||
Scenario: Set course category data registry defaults with override
|
||||
Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Set defaults" "link"
|
||||
And I click on "Blocks" "link"
|
||||
And I should see "Inherit"
|
||||
And I should not see "Add a new module default"
|
||||
And I press "Edit"
|
||||
And I set the field "Category" to "Category 1"
|
||||
And I set the field "Purpose" to "Purpose 1"
|
||||
And I click on "Reset instances with custom values" "checkbox"
|
||||
When I press "Save changes"
|
||||
Then I should see "Category 1"
|
||||
And I should see "Purpose 1"
|
||||
And I navigate to "Users > Privacy and policies > Data registry" in site administration
|
||||
And I click on "Science and technology" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Courses" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Physics 101" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Blocks" "link"
|
||||
And I wait until the page is ready
|
||||
And I click on "Online users" "link"
|
||||
And I wait until the page is ready
|
||||
And the field "categoryid" matches value "Not set (use the default value)"
|
||||
And the field "purposeid" matches value "Not set (use the default value)"
|
||||
And I should see "3 years (after the course end date)"
|
||||
@@ -7,7 +7,7 @@ Feature: Manage data storage purposes
|
||||
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 open the action menu in "region-main" "region"
|
||||
And I choose "Purposes" in the open action menu
|
||||
And I press "Add purpose"
|
||||
And I set the field "Name" to "Purpose 1"
|
||||
@@ -26,10 +26,10 @@ Feature: Manage data storage purposes
|
||||
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"
|
||||
And "Purpose 1 Purpose 1 description" row "5" column of "List of data purposes" table should contain "No"
|
||||
|
||||
Scenario: Update a data storage purpose
|
||||
Given I click on "Actions" "link" in the "Purpose 1" "table_row"
|
||||
Given I open the action menu in "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"
|
||||
@@ -45,10 +45,10 @@ Feature: Manage data storage purposes
|
||||
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"
|
||||
And "Purpose 1 edited Purpose 1 description edited" row "5" column of "List of data purposes" table should not contain "No"
|
||||
|
||||
Scenario: Delete a data storage purpose
|
||||
Given I click on "Actions" "link" in the "Purpose 1" "table_row"
|
||||
Given I open the action menu in "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'?"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for the data_registry class.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \tool_dataprivacy\data_registry;
|
||||
|
||||
/**
|
||||
* Unit tests for the data_registry class.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_dataprivacy_dataregistry_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Ensure that the get_effective_context_value only errors if provided an inappropriate element.
|
||||
*
|
||||
* This test is not great because we only test a limited set of values. This is a fault of the underlying API.
|
||||
*/
|
||||
public function test_get_effective_context_value_invalid_element() {
|
||||
$this->expectException(coding_exception::class);
|
||||
data_registry::get_effective_context_value(\context_system::instance(), 'invalid');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the get_effective_contextlevel_value only errors if provided an inappropriate element.
|
||||
*
|
||||
* This test is not great because we only test a limited set of values. This is a fault of the underlying API.
|
||||
*/
|
||||
public function test_get_effective_contextlevel_value_invalid_element() {
|
||||
$this->expectException(coding_exception::class);
|
||||
data_registry::get_effective_contextlevel_value(\context_system::instance(), 'invalid');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Tests for the data_request persistent.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
require_once('data_privacy_testcase.php');
|
||||
|
||||
use tool_dataprivacy\api;
|
||||
|
||||
/**
|
||||
* Tests for the data_request persistent.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_dataprivacy_data_request_testcase extends data_privacy_testcase {
|
||||
|
||||
/**
|
||||
* Data provider for testing is_resettable, and is_active.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function status_state_provider() : array {
|
||||
return [
|
||||
[
|
||||
'state' => api::DATAREQUEST_STATUS_PENDING,
|
||||
'resettable' => false,
|
||||
'active' => false,
|
||||
],
|
||||
[
|
||||
'state' => api::DATAREQUEST_STATUS_PREPROCESSING,
|
||||
'resettable' => false,
|
||||
'active' => false,
|
||||
],
|
||||
[
|
||||
'state' => api::DATAREQUEST_STATUS_AWAITING_APPROVAL,
|
||||
'resettable' => true,
|
||||
'active' => true,
|
||||
],
|
||||
[
|
||||
'state' => api::DATAREQUEST_STATUS_APPROVED,
|
||||
'resettable' => true,
|
||||
'active' => true,
|
||||
],
|
||||
[
|
||||
'state' => api::DATAREQUEST_STATUS_PROCESSING,
|
||||
'resettable' => false,
|
||||
'active' => false,
|
||||
],
|
||||
[
|
||||
'state' => api::DATAREQUEST_STATUS_COMPLETE,
|
||||
'resettable' => false,
|
||||
'active' => false,
|
||||
],
|
||||
[
|
||||
'state' => api::DATAREQUEST_STATUS_CANCELLED,
|
||||
'resettable' => false,
|
||||
'active' => false,
|
||||
],
|
||||
[
|
||||
'state' => api::DATAREQUEST_STATUS_REJECTED,
|
||||
'resettable' => true,
|
||||
'active' => false,
|
||||
],
|
||||
[
|
||||
'state' => api::DATAREQUEST_STATUS_DOWNLOAD_READY,
|
||||
'resettable' => false,
|
||||
'active' => false,
|
||||
],
|
||||
[
|
||||
'state' => api::DATAREQUEST_STATUS_EXPIRED,
|
||||
'resettable' => false,
|
||||
'active' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the pseudo states of a data request with an export request.
|
||||
*
|
||||
* @dataProvider status_state_provider
|
||||
* @param int $status
|
||||
* @param bool $resettable
|
||||
* @param bool $active
|
||||
*/
|
||||
public function test_pseudo_states_export(int $status, bool $resettable, bool $active) {
|
||||
$uut = new \tool_dataprivacy\data_request();
|
||||
$uut->set('status', $status);
|
||||
$uut->set('type', api::DATAREQUEST_TYPE_EXPORT);
|
||||
|
||||
$this->assertEquals($resettable, $uut->is_resettable());
|
||||
$this->assertEquals($active, $uut->is_active());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the pseudo states of a data request with a delete request.
|
||||
*
|
||||
* @dataProvider status_state_provider
|
||||
* @param int $status
|
||||
* @param bool $resettable
|
||||
* @param bool $active
|
||||
*/
|
||||
public function test_pseudo_states_delete(int $status, bool $resettable, bool $active) {
|
||||
$uut = new \tool_dataprivacy\data_request();
|
||||
$uut->set('status', $status);
|
||||
$uut->set('type', api::DATAREQUEST_TYPE_DELETE);
|
||||
|
||||
$this->assertEquals($resettable, $uut->is_resettable());
|
||||
$this->assertEquals($active, $uut->is_active());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the pseudo states of a data request.
|
||||
*
|
||||
* @dataProvider status_state_provider
|
||||
* @param int $status
|
||||
*/
|
||||
public function test_can_reset_others($status) {
|
||||
$uut = new \tool_dataprivacy\data_request();
|
||||
$uut->set('status', $status);
|
||||
$uut->set('type', api::DATAREQUEST_TYPE_OTHERS);
|
||||
|
||||
$this->assertFalse($uut->is_resettable());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for states which are not resettable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function non_resettable_provider() : array {
|
||||
$states = [];
|
||||
foreach ($this->status_state_provider() as $thisstatus) {
|
||||
if (!$thisstatus['resettable']) {
|
||||
$states[] = $thisstatus;
|
||||
}
|
||||
}
|
||||
|
||||
return $states;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that requests which are not resettable cause an exception to be thrown.
|
||||
*
|
||||
* @dataProvider non_resettable_provider
|
||||
* @param int $status
|
||||
*/
|
||||
public function test_non_resubmit_request($status) {
|
||||
$uut = new \tool_dataprivacy\data_request();
|
||||
$uut->set('status', $status);
|
||||
|
||||
$this->expectException(\moodle_exception::class);
|
||||
$this->expectExceptionMessage(get_string('cannotreset', 'tool_dataprivacy'));
|
||||
|
||||
$uut->resubmit_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a rejected request can be reset.
|
||||
*/
|
||||
public function test_resubmit_request() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$uut = new \tool_dataprivacy\data_request();
|
||||
$uut->set('status', api::DATAREQUEST_STATUS_REJECTED);
|
||||
$uut->set('type', api::DATAREQUEST_TYPE_DELETE);
|
||||
$uut->set('comments', 'Foo');
|
||||
$uut->set('requestedby', 42);
|
||||
$uut->set('dpo', 98);
|
||||
|
||||
$newrequest = $uut->resubmit_request();
|
||||
|
||||
$this->assertEquals('Foo', $newrequest->get('comments'));
|
||||
$this->assertEquals(42, $newrequest->get('requestedby'));
|
||||
$this->assertEquals(98, $newrequest->get('dpo'));
|
||||
$this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $newrequest->get('status'));
|
||||
$this->assertEquals(api::DATAREQUEST_TYPE_DELETE, $newrequest->get('type'));
|
||||
|
||||
$this->assertEquals(api::DATAREQUEST_STATUS_REJECTED, $uut->get('status'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that an active request can be reset.
|
||||
*/
|
||||
public function test_resubmit_active_request() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$uut = new \tool_dataprivacy\data_request();
|
||||
$uut->set('status', api::DATAREQUEST_STATUS_APPROVED);
|
||||
$uut->set('type', api::DATAREQUEST_TYPE_DELETE);
|
||||
$uut->set('comments', 'Foo');
|
||||
$uut->set('requestedby', 42);
|
||||
$uut->set('dpo', 98);
|
||||
|
||||
$newrequest = $uut->resubmit_request();
|
||||
|
||||
$this->assertEquals('Foo', $newrequest->get('comments'));
|
||||
$this->assertEquals(42, $newrequest->get('requestedby'));
|
||||
$this->assertEquals(98, $newrequest->get('dpo'));
|
||||
$this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $newrequest->get('status'));
|
||||
$this->assertEquals(api::DATAREQUEST_TYPE_DELETE, $newrequest->get('type'));
|
||||
|
||||
$this->assertEquals(api::DATAREQUEST_STATUS_REJECTED, $uut->get('status'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a data request for the user.
|
||||
*
|
||||
* @param int $userid
|
||||
* @param int $type
|
||||
* @param int $status
|
||||
* @return data_request
|
||||
*/
|
||||
public function create_request_for_user_with_status(int $userid, int $type, int $status) : data_request {
|
||||
$request = new data_request(0, (object) [
|
||||
'userid' => $userid,
|
||||
'type' => $type,
|
||||
'status' => $status,
|
||||
]);
|
||||
|
||||
$request->save();
|
||||
|
||||
return $request;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,9 @@
|
||||
*/
|
||||
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\category;
|
||||
use tool_dataprivacy\data_request;
|
||||
use tool_dataprivacy\purpose;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
global $CFG;
|
||||
@@ -62,6 +64,9 @@ class tool_dataprivacy_expired_data_requests_testcase extends data_privacy_testc
|
||||
$dpouser = $this->getDataGenerator()->create_user();
|
||||
$this->assign_site_dpo($dpouser);
|
||||
|
||||
// Set site purpose.
|
||||
$this->create_system_purpose();
|
||||
|
||||
// Set request expiry to 5 minutes.
|
||||
set_config('privacyrequestexpiry', 300, 'tool_dataprivacy');
|
||||
|
||||
@@ -130,6 +135,9 @@ class tool_dataprivacy_expired_data_requests_testcase extends data_privacy_testc
|
||||
$admin = get_admin();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Set site purpose.
|
||||
$this->create_system_purpose();
|
||||
|
||||
// Create export request.
|
||||
$datarequest = api::create_data_request($admin->id, api::DATAREQUEST_TYPE_EXPORT);
|
||||
$requestid = $datarequest->get('id');
|
||||
@@ -170,4 +178,28 @@ class tool_dataprivacy_expired_data_requests_testcase extends data_privacy_testc
|
||||
$result = data_request::is_expired($request);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a site (system context) purpose and category.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function create_system_purpose() {
|
||||
$purpose = new purpose(0, (object) [
|
||||
'name' => 'Test purpose ' . rand(1, 1000),
|
||||
'retentionperiod' => 'P1D',
|
||||
'lawfulbases' => 'gdpr_art_6_1_a',
|
||||
]);
|
||||
$purpose->create();
|
||||
|
||||
$cat = new category(0, (object) ['name' => 'Test category']);
|
||||
$cat->create();
|
||||
|
||||
$record = (object) [
|
||||
'purposeid' => $purpose->get('id'),
|
||||
'categoryid' => $cat->get('id'),
|
||||
'contextlevel' => CONTEXT_SYSTEM,
|
||||
];
|
||||
api::set_contextlevel($record);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ global $CFG;
|
||||
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\context_instance;
|
||||
use tool_dataprivacy\external;
|
||||
|
||||
/**
|
||||
@@ -39,71 +40,85 @@ use tool_dataprivacy\external;
|
||||
*/
|
||||
class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
|
||||
|
||||
/** @var stdClass The user making the request. */
|
||||
protected $requester;
|
||||
|
||||
/** @var int The data request ID. */
|
||||
protected $requestid;
|
||||
|
||||
/**
|
||||
* Setup function- we will create a course and add an assign instance to it.
|
||||
*/
|
||||
protected function setUp() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
|
||||
$comment = 'sample comment';
|
||||
|
||||
// Login as user.
|
||||
$this->setUser($requester->id);
|
||||
|
||||
// Test data request creation.
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
$this->requestid = $datarequest->get('id');
|
||||
$this->requester = $requester;
|
||||
|
||||
// Log out the user and set force login to true.
|
||||
$this->setUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::approve_data_request() with the user not logged in.
|
||||
*/
|
||||
public function test_approve_data_request_not_logged_in() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
// Test data request creation.
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
// Log out the user and set force login to true.
|
||||
$this->setUser();
|
||||
|
||||
$this->expectException(require_login_exception::class);
|
||||
external::approve_data_request($this->requestid);
|
||||
external::approve_data_request($datarequest->get('id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::approve_data_request() with the user not having a DPO role.
|
||||
*/
|
||||
public function test_approve_data_request_not_dpo() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
// Test data request creation.
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
// Login as the requester.
|
||||
$this->setUser($this->requester->id);
|
||||
$this->setUser($requester);
|
||||
$this->expectException(required_capability_exception::class);
|
||||
external::approve_data_request($this->requestid);
|
||||
external::approve_data_request($datarequest->get('id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::approve_data_request() for request that's not ready for approval
|
||||
*/
|
||||
public function test_approve_data_request_not_waiting_for_approval() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
// Test data request creation.
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
|
||||
$this->setAdminUser();
|
||||
$this->expectException(moodle_exception::class);
|
||||
external::approve_data_request($this->requestid);
|
||||
external::approve_data_request($datarequest->get('id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::approve_data_request()
|
||||
*/
|
||||
public function test_approve_data_request() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
// Test data request creation.
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
|
||||
$this->setAdminUser();
|
||||
api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
$result = external::approve_data_request($this->requestid);
|
||||
api::update_request_status($datarequest->get('id'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
$result = external::approve_data_request($datarequest->get('id'));
|
||||
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
|
||||
$this->assertTrue($return->result);
|
||||
$this->assertEmpty($return->warnings);
|
||||
@@ -113,10 +128,13 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
|
||||
* Test for external::approve_data_request() for a non-existent request ID.
|
||||
*/
|
||||
public function test_approve_data_request_non_existent() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
|
||||
$this->setAdminUser();
|
||||
api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
$result = external::approve_data_request($this->requestid + 1);
|
||||
|
||||
$result = external::approve_data_request(1);
|
||||
|
||||
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
|
||||
$this->assertFalse($return->result);
|
||||
$this->assertCount(1, $return->warnings);
|
||||
@@ -128,13 +146,21 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
|
||||
* Test for external::cancel_data_request() of another user.
|
||||
*/
|
||||
public function test_cancel_data_request_other_user() {
|
||||
$generator = $this->getDataGenerator();
|
||||
$otheruser = $generator->create_user();
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Login as another user.
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$otheruser = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
// Test data request creation.
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
// Login as other user.
|
||||
$this->setUser($otheruser);
|
||||
|
||||
$result = external::cancel_data_request($this->requestid);
|
||||
$result = external::cancel_data_request($datarequest->get('id'));
|
||||
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
|
||||
$this->assertFalse($return->result);
|
||||
$this->assertCount(1, $return->warnings);
|
||||
@@ -142,14 +168,107 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
|
||||
$this->assertEquals('errorrequestnotfound', $warning['warningcode']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cancellation of a request where you are the requester of another user's data.
|
||||
*/
|
||||
public function test_cancel_data_request_other_user_as_requester() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$otheruser = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
// Assign requester as otheruser'sparent.
|
||||
$systemcontext = \context_system::instance();
|
||||
$parentrole = $generator->create_role();
|
||||
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
|
||||
role_assign($parentrole, $requester->id, \context_user::instance($otheruser->id));
|
||||
|
||||
// Test data request creation.
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($otheruser->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
$result = external::cancel_data_request($datarequest->get('id'));
|
||||
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
|
||||
$this->assertTrue($return->result);
|
||||
$this->assertEmpty($return->warnings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cancellation of a request where you are the requester of another user's data.
|
||||
*/
|
||||
public function test_cancel_data_request_requester_lost_permissions() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$otheruser = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
// Assign requester as otheruser'sparent.
|
||||
$systemcontext = \context_system::instance();
|
||||
$parentrole = $generator->create_role();
|
||||
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
|
||||
role_assign($parentrole, $requester->id, \context_user::instance($otheruser->id));
|
||||
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($otheruser->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
// Unassign the role.
|
||||
role_unassign($parentrole, $requester->id, \context_user::instance($otheruser->id)->id);
|
||||
|
||||
// This user can no longer make the request.
|
||||
$this->expectException(required_capability_exception::class);
|
||||
|
||||
$result = external::cancel_data_request($datarequest->get('id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cancellation of a request where you are the requester of another user's data.
|
||||
*/
|
||||
public function test_cancel_data_request_other_user_as_child() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$otheruser = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
// Assign requester as otheruser'sparent.
|
||||
$systemcontext = \context_system::instance();
|
||||
$parentrole = $generator->create_role();
|
||||
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
|
||||
role_assign($parentrole, $requester->id, \context_user::instance($otheruser->id));
|
||||
|
||||
// Test data request creation.
|
||||
$this->setUser($otheruser);
|
||||
$datarequest = api::create_data_request($otheruser->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
$result = external::cancel_data_request($datarequest->get('id'));
|
||||
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
|
||||
$this->assertTrue($return->result);
|
||||
$this->assertEmpty($return->warnings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::cancel_data_request()
|
||||
*/
|
||||
public function test_cancel_data_request() {
|
||||
// Login as the requester.
|
||||
$this->setUser($this->requester);
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
// Test data request creation.
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
// Test cancellation.
|
||||
$this->setUser($requester);
|
||||
$result = external::cancel_data_request($datarequest->get('id'));
|
||||
|
||||
$result = external::cancel_data_request($this->requestid);
|
||||
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
|
||||
$this->assertTrue($return->result);
|
||||
$this->assertEmpty($return->warnings);
|
||||
@@ -159,10 +278,12 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
|
||||
* Test contact DPO.
|
||||
*/
|
||||
public function test_contact_dpo() {
|
||||
$generator = $this->getDataGenerator();
|
||||
$user = $generator->create_user();
|
||||
$this->setUser($user);
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$user = $generator->create_user();
|
||||
|
||||
$this->setUser($user);
|
||||
$message = 'Hello world!';
|
||||
$result = external::contact_dpo($message);
|
||||
$return = (object) external_api::clean_returnvalue(external::contact_dpo_returns(), $result);
|
||||
@@ -174,10 +295,12 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
|
||||
* Test contact DPO with message containing invalid input.
|
||||
*/
|
||||
public function test_contact_dpo_with_nasty_input() {
|
||||
$generator = $this->getDataGenerator();
|
||||
$user = $generator->create_user();
|
||||
$this->setUser($user);
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$user = $generator->create_user();
|
||||
|
||||
$this->setUser($user);
|
||||
$this->expectException('invalid_parameter_exception');
|
||||
external::contact_dpo('de<>\\..scription');
|
||||
}
|
||||
@@ -186,38 +309,77 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
|
||||
* Test for external::deny_data_request() with the user not logged in.
|
||||
*/
|
||||
public function test_deny_data_request_not_logged_in() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
// Test data request creation.
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
// Log out.
|
||||
$this->setUser();
|
||||
$this->expectException(require_login_exception::class);
|
||||
external::deny_data_request($this->requestid);
|
||||
external::deny_data_request($datarequest->get('id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::deny_data_request() with the user not having a DPO role.
|
||||
*/
|
||||
public function test_deny_data_request_not_dpo() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
// Login as the requester.
|
||||
$this->setUser($this->requester->id);
|
||||
$this->setUser($requester);
|
||||
$this->expectException(required_capability_exception::class);
|
||||
external::deny_data_request($this->requestid);
|
||||
external::deny_data_request($datarequest->get('id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::deny_data_request() for request that's not ready for approval
|
||||
*/
|
||||
public function test_deny_data_request_not_waiting_for_approval() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
|
||||
$this->setAdminUser();
|
||||
$this->expectException(moodle_exception::class);
|
||||
external::deny_data_request($this->requestid);
|
||||
external::deny_data_request($datarequest->get('id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::deny_data_request()
|
||||
*/
|
||||
public function test_deny_data_request() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
|
||||
$this->setAdminUser();
|
||||
api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
$result = external::approve_data_request($this->requestid);
|
||||
api::update_request_status($datarequest->get('id'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
$result = external::approve_data_request($datarequest->get('id'));
|
||||
$return = (object) external_api::clean_returnvalue(external::deny_data_request_returns(), $result);
|
||||
$this->assertTrue($return->result);
|
||||
$this->assertEmpty($return->warnings);
|
||||
@@ -227,10 +389,12 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
|
||||
* Test for external::deny_data_request() for a non-existent request ID.
|
||||
*/
|
||||
public function test_deny_data_request_non_existent() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
|
||||
$this->setAdminUser();
|
||||
api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
$result = external::deny_data_request($this->requestid + 1);
|
||||
$result = external::deny_data_request(1);
|
||||
|
||||
$return = (object) external_api::clean_returnvalue(external::deny_data_request_returns(), $result);
|
||||
$this->assertFalse($return->result);
|
||||
$this->assertCount(1, $return->warnings);
|
||||
@@ -242,34 +406,62 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
|
||||
* Test for external::get_data_request() with the user not logged in.
|
||||
*/
|
||||
public function test_get_data_request_not_logged_in() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
$this->setUser();
|
||||
$this->expectException(require_login_exception::class);
|
||||
external::get_data_request($this->requestid);
|
||||
external::get_data_request($datarequest->get('id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::get_data_request() with the user not having a DPO role.
|
||||
*/
|
||||
public function test_get_data_request_not_dpo() {
|
||||
$generator = $this->getDataGenerator();
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$otheruser = $generator->create_user();
|
||||
// Login as the requester.
|
||||
$comment = 'sample comment';
|
||||
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
// Login as the otheruser.
|
||||
$this->setUser($otheruser);
|
||||
$this->expectException(required_capability_exception::class);
|
||||
external::get_data_request($this->requestid);
|
||||
external::get_data_request($datarequest->get('id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::get_data_request()
|
||||
*/
|
||||
public function test_get_data_request() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = new testing_data_generator();
|
||||
$requester = $generator->create_user();
|
||||
$comment = 'sample comment';
|
||||
|
||||
$this->setUser($requester);
|
||||
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
|
||||
|
||||
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
|
||||
$this->setAdminUser();
|
||||
$result = external::get_data_request($this->requestid);
|
||||
$result = external::get_data_request($datarequest->get('id'));
|
||||
|
||||
$return = (object) external_api::clean_returnvalue(external::get_data_request_returns(), $result);
|
||||
$this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $return->result['type']);
|
||||
$this->assertEquals('sample comment', $return->result['comments']);
|
||||
$this->assertEquals($this->requester->id, $return->result['userid']);
|
||||
$this->assertEquals($this->requester->id, $return->result['requestedby']);
|
||||
$this->assertEquals($requester->id, $return->result['userid']);
|
||||
$this->assertEquals($requester->id, $return->result['requestedby']);
|
||||
$this->assertEmpty($return->warnings);
|
||||
}
|
||||
|
||||
@@ -277,9 +469,503 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
|
||||
* Test for external::get_data_request() for a non-existent request ID.
|
||||
*/
|
||||
public function test_get_data_request_non_existent() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
|
||||
$this->setAdminUser();
|
||||
$this->expectException(dml_missing_record_exception::class);
|
||||
external::get_data_request($this->requestid + 1);
|
||||
external::get_data_request(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \tool_dataprivacy\external::set_context_defaults()
|
||||
* when called by a user that doesn't have the manage registry capability.
|
||||
*/
|
||||
public function test_set_context_defaults_no_capability() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
$user = $generator->create_user();
|
||||
$this->setUser($user);
|
||||
$this->expectException(required_capability_exception::class);
|
||||
external::set_context_defaults(CONTEXT_COURSECAT, context_instance::INHERIT, context_instance::INHERIT, '', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \tool_dataprivacy\external::set_context_defaults().
|
||||
*
|
||||
* We're just checking the module context level here to test the WS function.
|
||||
* More testing is done in \tool_dataprivacy_api_testcase::test_set_context_defaults().
|
||||
*
|
||||
* @dataProvider get_options_provider
|
||||
* @param bool $modulelevel Whether defaults are to be applied on the module context level or for an activity only.
|
||||
* @param bool $override Whether to override instances.
|
||||
*/
|
||||
public function test_set_context_defaults($modulelevel, $override) {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$this->setAdminUser();
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
// Generate course cat, course, block, assignment, forum instances.
|
||||
$coursecat = $generator->create_category();
|
||||
$course = $generator->create_course(['category' => $coursecat->id]);
|
||||
$assign = $generator->create_module('assign', ['course' => $course->id]);
|
||||
list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign');
|
||||
$assigncontext = context_module::instance($assigncm->id);
|
||||
|
||||
// Generate purpose and category.
|
||||
$category1 = api::create_category((object)['name' => 'Test category 1']);
|
||||
$category2 = api::create_category((object)['name' => 'Test category 2']);
|
||||
$purpose1 = api::create_purpose((object)[
|
||||
'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
|
||||
]);
|
||||
$purpose2 = api::create_purpose((object)[
|
||||
'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
|
||||
]);
|
||||
|
||||
// Set a custom purpose and ID for this assignment instance.
|
||||
$assignctxinstance = api::set_context_instance((object) [
|
||||
'contextid' => $assigncontext->id,
|
||||
'purposeid' => $purpose1->get('id'),
|
||||
'categoryid' => $category1->get('id'),
|
||||
]);
|
||||
|
||||
$modulename = $modulelevel ? 'assign' : '';
|
||||
$categoryid = $category2->get('id');
|
||||
$purposeid = $purpose2->get('id');
|
||||
$result = external::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, $modulename, $override);
|
||||
|
||||
// Extract the result.
|
||||
$return = external_api::clean_returnvalue(external::set_context_defaults_returns(), $result);
|
||||
$this->assertTrue($return['result']);
|
||||
|
||||
// Check the assignment context instance.
|
||||
$instanceexists = context_instance::record_exists($assignctxinstance->get('id'));
|
||||
if ($override) {
|
||||
// The custom assign instance should have been deleted.
|
||||
$this->assertFalse($instanceexists);
|
||||
} else {
|
||||
// The custom assign instance should still exist.
|
||||
$this->assertTrue($instanceexists);
|
||||
}
|
||||
|
||||
// Check the saved defaults.
|
||||
list($savedpurpose, $savedcategory) = \tool_dataprivacy\data_registry::get_defaults(CONTEXT_MODULE, $modulename);
|
||||
$this->assertEquals($categoryid, $savedcategory);
|
||||
$this->assertEquals($purposeid, $savedpurpose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \tool_dataprivacy\external::get_category_options()
|
||||
* when called by a user that doesn't have the manage registry capability.
|
||||
*/
|
||||
public function test_get_category_options_no_capability() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user);
|
||||
|
||||
$this->expectException(required_capability_exception::class);
|
||||
external::get_category_options(true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for \tool_dataprivacy_external_testcase::test_XX_options().
|
||||
*/
|
||||
public function get_options_provider() {
|
||||
return [
|
||||
[false, false],
|
||||
[false, true],
|
||||
[true, false],
|
||||
[true, true],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \tool_dataprivacy\external::get_category_options().
|
||||
*
|
||||
* @dataProvider get_options_provider
|
||||
* @param bool $includeinherit Whether "Inherit" would be included to the options.
|
||||
* @param bool $includenotset Whether "Not set" would be included to the options.
|
||||
*/
|
||||
public function test_get_category_options($includeinherit, $includenotset) {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Prepare our expected options.
|
||||
$expectedoptions = [];
|
||||
if ($includeinherit) {
|
||||
$expectedoptions[] = [
|
||||
'id' => context_instance::INHERIT,
|
||||
'name' => get_string('inherit', 'tool_dataprivacy'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($includenotset) {
|
||||
$expectedoptions[] = [
|
||||
'id' => context_instance::NOTSET,
|
||||
'name' => get_string('notset', 'tool_dataprivacy'),
|
||||
];
|
||||
}
|
||||
|
||||
for ($i = 1; $i <= 3; $i++) {
|
||||
$category = api::create_category((object)['name' => 'Category ' . $i]);
|
||||
$expectedoptions[] = [
|
||||
'id' => $category->get('id'),
|
||||
'name' => $category->get('name'),
|
||||
];
|
||||
}
|
||||
|
||||
// Call the WS function.
|
||||
$result = external::get_category_options($includeinherit, $includenotset);
|
||||
|
||||
// Extract the options.
|
||||
$return = (object) external_api::clean_returnvalue(external::get_category_options_returns(), $result);
|
||||
$options = $return->options;
|
||||
|
||||
// Make sure everything checks out.
|
||||
$this->assertCount(count($expectedoptions), $options);
|
||||
foreach ($options as $option) {
|
||||
$this->assertContains($option, $expectedoptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \tool_dataprivacy\external::get_purpose_options()
|
||||
* when called by a user that doesn't have the manage registry capability.
|
||||
*/
|
||||
public function test_get_purpose_options_no_capability() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
$user = $generator->create_user();
|
||||
$this->setUser($user);
|
||||
$this->expectException(required_capability_exception::class);
|
||||
external::get_category_options(true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \tool_dataprivacy\external::get_purpose_options().
|
||||
*
|
||||
* @dataProvider get_options_provider
|
||||
* @param bool $includeinherit Whether "Inherit" would be included to the options.
|
||||
* @param bool $includenotset Whether "Not set" would be included to the options.
|
||||
*/
|
||||
public function test_get_purpose_options($includeinherit, $includenotset) {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Prepare our expected options.
|
||||
$expectedoptions = [];
|
||||
if ($includeinherit) {
|
||||
$expectedoptions[] = [
|
||||
'id' => context_instance::INHERIT,
|
||||
'name' => get_string('inherit', 'tool_dataprivacy'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($includenotset) {
|
||||
$expectedoptions[] = [
|
||||
'id' => context_instance::NOTSET,
|
||||
'name' => get_string('notset', 'tool_dataprivacy'),
|
||||
];
|
||||
}
|
||||
|
||||
for ($i = 1; $i <= 3; $i++) {
|
||||
$purpose = api::create_purpose((object)[
|
||||
'name' => 'Purpose ' . $i, 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
|
||||
]);
|
||||
$expectedoptions[] = [
|
||||
'id' => $purpose->get('id'),
|
||||
'name' => $purpose->get('name'),
|
||||
];
|
||||
}
|
||||
|
||||
// Call the WS function.
|
||||
$result = external::get_purpose_options($includeinherit, $includenotset);
|
||||
|
||||
// Extract the options.
|
||||
$return = (object) external_api::clean_returnvalue(external::get_purpose_options_returns(), $result);
|
||||
$options = $return->options;
|
||||
|
||||
// Make sure everything checks out.
|
||||
$this->assertCount(count($expectedoptions), $options);
|
||||
foreach ($options as $option) {
|
||||
$this->assertContains($option, $expectedoptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for \tool_dataprivacy_external_testcase::get_activity_options().
|
||||
*/
|
||||
public function get_activity_options_provider() {
|
||||
return [
|
||||
[false, false, true],
|
||||
[false, true, true],
|
||||
[true, false, true],
|
||||
[true, true, true],
|
||||
[false, false, false],
|
||||
[false, true, false],
|
||||
[true, false, false],
|
||||
[true, true, false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \tool_dataprivacy\external::get_activity_options().
|
||||
*
|
||||
* @dataProvider get_activity_options_provider
|
||||
* @param bool $inheritcategory Whether the category would be set to "Inherit".
|
||||
* @param bool $inheritpurpose Whether the purpose would be set to "Inherit".
|
||||
* @param bool $nodefaults Whether to fetch only activities that don't have defaults.
|
||||
*/
|
||||
public function test_get_activity_options($inheritcategory, $inheritpurpose, $nodefaults) {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$category = api::create_category((object)['name' => 'Test category']);
|
||||
$purpose = api::create_purpose((object)[
|
||||
'name' => 'Test purpose ', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
|
||||
]);
|
||||
$categoryid = $category->get('id');
|
||||
$purposeid = $purpose->get('id');
|
||||
|
||||
if ($inheritcategory) {
|
||||
$categoryid = context_instance::INHERIT;
|
||||
}
|
||||
if ($inheritpurpose) {
|
||||
$purposeid = context_instance::INHERIT;
|
||||
}
|
||||
|
||||
// Set the context default for the assignment module.
|
||||
api::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, 'assign');
|
||||
|
||||
// Call the WS function.
|
||||
$result = external::get_activity_options($nodefaults);
|
||||
|
||||
// Extract the options.
|
||||
$return = (object) external_api::clean_returnvalue(external::get_activity_options_returns(), $result);
|
||||
$options = $return->options;
|
||||
|
||||
// Make sure the options list is not empty.
|
||||
$this->assertNotEmpty($options);
|
||||
|
||||
$pluginwithdefaults = [
|
||||
'name' => 'assign',
|
||||
'displayname' => get_string('pluginname', 'assign')
|
||||
];
|
||||
|
||||
// If we don't want plugins with defaults to be listed or if both of the category and purpose are set to inherit,
|
||||
// the assign module should be listed.
|
||||
if (!$nodefaults || ($inheritcategory && $inheritpurpose)) {
|
||||
$this->assertContains($pluginwithdefaults, $options);
|
||||
} else {
|
||||
$this->assertNotContains($pluginwithdefaults, $options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::bulk_approve_data_requests().
|
||||
*/
|
||||
public function test_bulk_approve_data_requests() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create delete data requests.
|
||||
$requester1 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($requester1->id);
|
||||
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
|
||||
$requestid1 = $datarequest1->get('id');
|
||||
|
||||
$requester2 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($requester2->id);
|
||||
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
|
||||
$requestid2 = $datarequest2->get('id');
|
||||
|
||||
// Approve the requests.
|
||||
$this->setAdminUser();
|
||||
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
$result = external::bulk_approve_data_requests([$requestid1, $requestid2]);
|
||||
|
||||
$return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
|
||||
$this->assertTrue($return->result);
|
||||
$this->assertEmpty($return->warnings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::bulk_approve_data_requests() for a non-existent request ID.
|
||||
*/
|
||||
public function test_bulk_approve_data_requests_non_existent() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
$result = external::bulk_approve_data_requests([42]);
|
||||
|
||||
$return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
|
||||
$this->assertFalse($return->result);
|
||||
$this->assertCount(1, $return->warnings);
|
||||
$warning = reset($return->warnings);
|
||||
$this->assertEquals('errorrequestnotfound', $warning['warningcode']);
|
||||
$this->assertEquals(42, $warning['item']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::bulk_deny_data_requests() for a user without permission to deny requests.
|
||||
*/
|
||||
public function test_bulk_approve_data_requests_no_permission() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create delete data requests.
|
||||
$requester1 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($requester1->id);
|
||||
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
|
||||
$requestid1 = $datarequest1->get('id');
|
||||
|
||||
$requester2 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($requester2->id);
|
||||
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
|
||||
$requestid2 = $datarequest2->get('id');
|
||||
|
||||
$this->setAdminUser();
|
||||
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
|
||||
// Approve the requests.
|
||||
$uut = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($uut);
|
||||
|
||||
$this->expectException(required_capability_exception::class);
|
||||
$result = external::bulk_approve_data_requests([$requestid1, $requestid2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::bulk_deny_data_requests() for a user without permission to deny requests.
|
||||
*/
|
||||
public function test_bulk_approve_data_requests_own_request() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create delete data requests.
|
||||
$requester1 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($requester1->id);
|
||||
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
|
||||
$requestid1 = $datarequest1->get('id');
|
||||
|
||||
$requester2 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($requester2->id);
|
||||
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
|
||||
$requestid2 = $datarequest2->get('id');
|
||||
|
||||
$this->setAdminUser();
|
||||
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
|
||||
// Deny the requests.
|
||||
$this->setUser($requester1);
|
||||
|
||||
$this->expectException(required_capability_exception::class);
|
||||
$result = external::bulk_approve_data_requests([$requestid1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::bulk_deny_data_requests().
|
||||
*/
|
||||
public function test_bulk_deny_data_requests() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create delete data requests.
|
||||
$requester1 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($requester1->id);
|
||||
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
|
||||
$requestid1 = $datarequest1->get('id');
|
||||
|
||||
$requester2 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($requester2->id);
|
||||
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
|
||||
$requestid2 = $datarequest2->get('id');
|
||||
|
||||
// Deny the requests.
|
||||
$this->setAdminUser();
|
||||
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
$result = external::bulk_deny_data_requests([$requestid1, $requestid2]);
|
||||
|
||||
$return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
|
||||
$this->assertTrue($return->result);
|
||||
$this->assertEmpty($return->warnings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::bulk_deny_data_requests() for a non-existent request ID.
|
||||
*/
|
||||
public function test_bulk_deny_data_requests_non_existent() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$this->setAdminUser();
|
||||
$result = external::bulk_deny_data_requests([42]);
|
||||
$return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
|
||||
|
||||
$this->assertFalse($return->result);
|
||||
$this->assertCount(1, $return->warnings);
|
||||
$warning = reset($return->warnings);
|
||||
$this->assertEquals('errorrequestnotfound', $warning['warningcode']);
|
||||
$this->assertEquals(42, $warning['item']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::bulk_deny_data_requests() for a user without permission to deny requests.
|
||||
*/
|
||||
public function test_bulk_deny_data_requests_no_permission() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create delete data requests.
|
||||
$requester1 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($requester1->id);
|
||||
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
|
||||
$requestid1 = $datarequest1->get('id');
|
||||
|
||||
$requester2 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($requester2->id);
|
||||
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
|
||||
$requestid2 = $datarequest2->get('id');
|
||||
|
||||
$this->setAdminUser();
|
||||
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
|
||||
// Deny the requests.
|
||||
$uut = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($uut);
|
||||
|
||||
$this->expectException(required_capability_exception::class);
|
||||
$result = external::bulk_deny_data_requests([$requestid1, $requestid2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for external::bulk_deny_data_requests() for a user cannot approve their own request.
|
||||
*/
|
||||
public function test_bulk_deny_data_requests_own_request() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create delete data requests.
|
||||
$requester1 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($requester1->id);
|
||||
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
|
||||
$requestid1 = $datarequest1->get('id');
|
||||
|
||||
$requester2 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($requester2->id);
|
||||
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
|
||||
$requestid2 = $datarequest2->get('id');
|
||||
|
||||
$this->setAdminUser();
|
||||
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
|
||||
// Deny the requests.
|
||||
$this->setUser($requester1);
|
||||
|
||||
$this->expectException(required_capability_exception::class);
|
||||
$result = external::bulk_deny_data_requests([$requestid1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for the filtered_userlist.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Unit tests for the filtered_userlist.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_dataprivacy_filtered_userlist_testcase extends advanced_testcase {
|
||||
/**
|
||||
* Test the apply_expired_contexts_filters function with arange of options.
|
||||
*
|
||||
* @dataProvider apply_expired_contexts_filters_provider
|
||||
* @param array $initial The set of userids in the initial filterlist.
|
||||
* @param array $expired The set of userids considered as expired.
|
||||
* @param array $unexpired The set of userids considered as unexpired.
|
||||
* @param array $expected The expected values.
|
||||
*/
|
||||
public function test_apply_expired_contexts_filters(array $initial, array $expired, array $unexpired, array $expected) {
|
||||
$userlist = $this->getMockBuilder(\tool_dataprivacy\filtered_userlist::class)
|
||||
->disableOriginalConstructor()
|
||||
->setMethods(null)
|
||||
->getMock();
|
||||
|
||||
$rc = new \ReflectionClass(\tool_dataprivacy\filtered_userlist::class);
|
||||
$rcm = $rc->getMethod('set_userids');
|
||||
$rcm->setAccessible(true);
|
||||
$rcm->invoke($userlist, $initial);
|
||||
|
||||
|
||||
$userlist->apply_expired_context_filters($expired, $unexpired);
|
||||
$filtered = $userlist->get_userids();
|
||||
|
||||
sort($expected);
|
||||
sort($filtered);
|
||||
$this->assertEquals($expected, $filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the apply_expired_contexts_filters function.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function apply_expired_contexts_filters_provider() : array {
|
||||
return [
|
||||
// Entire list should be preserved.
|
||||
'No overrides' => [
|
||||
'users' => [1, 2, 3, 4, 5],
|
||||
'expired' => [],
|
||||
'unexpired' => [],
|
||||
[1, 2, 3, 4, 5],
|
||||
],
|
||||
// The list should be filtered to only keep the expired users.
|
||||
'Expired only' => [
|
||||
'users' => [1, 2, 3, 4, 5],
|
||||
'expired' => [2, 3, 4],
|
||||
'unexpired' => [],
|
||||
'expected' => [2, 3, 4],
|
||||
],
|
||||
// The list should be filtered to remove any unexpired users.
|
||||
'Unexpired only' => [
|
||||
'users' => [1, 2, 3, 4, 5],
|
||||
'expired' => [],
|
||||
'unexpired' => [1, 5],
|
||||
'expected' => [2, 3, 4],
|
||||
],
|
||||
// The list should be filtered to only keep expired users who are not on the unexpired list.
|
||||
'Combination of expired and unexpired' => [
|
||||
'users' => [1, 2, 3, 4, 5],
|
||||
'expired' => [1, 2, 3],
|
||||
'unexpired' => [1, 5],
|
||||
'expected' => [2, 3],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Data privacy tool data generator.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @category test
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\category;
|
||||
use tool_dataprivacy\purpose;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Data privacy tool data generator class.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @category test
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_dataprivacy_generator extends component_generator_base {
|
||||
|
||||
/** @var int Number of created categories. */
|
||||
protected $categorycount = 0;
|
||||
|
||||
/** @var int Number of created purposes. */
|
||||
protected $purposecount = 0;
|
||||
|
||||
/**
|
||||
* Reset process.
|
||||
*
|
||||
* Do not call directly.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reset() {
|
||||
$this->categorycount = 0;
|
||||
$this->purposecount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new category.
|
||||
*
|
||||
* @param array|stdClass $record
|
||||
* @return category
|
||||
*/
|
||||
public function create_category($record = null) {
|
||||
$this->categorycount++;
|
||||
$i = $this->categorycount;
|
||||
$record = (object)$record;
|
||||
|
||||
if (!isset($record->name)) {
|
||||
$record->name = "Test purpose $i";
|
||||
}
|
||||
|
||||
if (!isset($record->description)) {
|
||||
$record->description = "{$record->name} description";
|
||||
}
|
||||
|
||||
$category = api::create_category($record);
|
||||
|
||||
return $category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new purpose.
|
||||
*
|
||||
* @param array|stdClass $record
|
||||
* @return purpose
|
||||
*/
|
||||
public function create_purpose($record = null) {
|
||||
$this->purposecount++;
|
||||
$i = $this->purposecount;
|
||||
$record = (object)$record;
|
||||
|
||||
if (!isset($record->name)) {
|
||||
$record->name = "Test purpose $i";
|
||||
}
|
||||
|
||||
if (!isset($record->description)) {
|
||||
$record->description = "{$record->name} $i description";
|
||||
}
|
||||
|
||||
if (!isset($record->retentionperiod)) {
|
||||
$record->retentionperiod = 'PT1M';
|
||||
}
|
||||
|
||||
if (!isset($record->lawfulbases)) {
|
||||
$record->lawfulbases = 'gdpr_art_6_1_a';
|
||||
}
|
||||
|
||||
$purpose = api::create_purpose($record);
|
||||
|
||||
return $purpose;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Tests for scheduled tasks.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
require_once('data_privacy_testcase.php');
|
||||
|
||||
use tool_dataprivacy\api;
|
||||
|
||||
/**
|
||||
* Tests for scheduled tasks.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_dataprivacy_task_testcase extends data_privacy_testcase {
|
||||
|
||||
/**
|
||||
* Test tearDown.
|
||||
*/
|
||||
public function tearDown() {
|
||||
\core_privacy\local\request\writer::reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a delete data request for pre-existing deleted users
|
||||
* is created when there are not any existing data requests
|
||||
* for that particular user.
|
||||
*/
|
||||
public function test_delete_existing_deleted_users_task_no_previous_requests() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Enable automatic creation of delete data requests.
|
||||
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
|
||||
|
||||
// Create a user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
// Mark the user as deleted.
|
||||
$user->deleted = 1;
|
||||
$DB->update_record('user', $user);
|
||||
|
||||
// The user should not have a delete data request.
|
||||
$this->assertCount(0, api::get_data_requests($user->id, [],
|
||||
[api::DATAREQUEST_TYPE_DELETE]));
|
||||
|
||||
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
|
||||
// After running the scheduled task, the deleted user should have a delete data request.
|
||||
$this->assertCount(1, api::get_data_requests($user->id, [],
|
||||
[api::DATAREQUEST_TYPE_DELETE]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a delete data request for pre-existing deleted users
|
||||
* is not being created when automatic creation of delete data requests is disabled.
|
||||
*/
|
||||
public function test_delete_existing_deleted_users_task_automatic_creation_disabled() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Disable automatic creation of delete data requests.
|
||||
set_config('automaticdeletionrequests', 0, 'tool_dataprivacy');
|
||||
|
||||
// Create a user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
// Mark the user as deleted.
|
||||
$user->deleted = 1;
|
||||
$DB->update_record('user', $user);
|
||||
|
||||
// The user should not have a delete data request.
|
||||
$this->assertCount(0, api::get_data_requests($user->id, [],
|
||||
[api::DATAREQUEST_TYPE_DELETE]));
|
||||
|
||||
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
|
||||
// After running the scheduled task, the deleted user should still not have a delete data request.
|
||||
$this->assertCount(0, api::get_data_requests($user->id, [],
|
||||
[api::DATAREQUEST_TYPE_DELETE]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a delete data request for pre-existing deleted users
|
||||
* is created when there are existing non-delete data requests
|
||||
* for that particular user.
|
||||
*/
|
||||
public function test_delete_existing_deleted_users_task_existing_export_data_requests() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Enable automatic creation of delete data requests.
|
||||
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
|
||||
|
||||
// Create a user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
// Create export data request for the user.
|
||||
api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT);
|
||||
// Mark the user as deleted.
|
||||
$user->deleted = 1;
|
||||
$DB->update_record('user', $user);
|
||||
|
||||
// The user should have a export data request.
|
||||
$this->assertCount(1, api::get_data_requests($user->id, [],
|
||||
[api::DATAREQUEST_TYPE_EXPORT]));
|
||||
// The user should not have a delete data request.
|
||||
$this->assertCount(0, api::get_data_requests($user->id, [],
|
||||
[api::DATAREQUEST_TYPE_DELETE]));
|
||||
|
||||
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
|
||||
// After running the scheduled task, the deleted user should have a delete data request.
|
||||
$this->assertCount(1, api::get_data_requests($user->id, [],
|
||||
[api::DATAREQUEST_TYPE_DELETE]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a delete data request for pre-existing deleted users
|
||||
* is not created when there are existing ongoing delete data requests
|
||||
* for that particular user.
|
||||
*/
|
||||
public function test_delete_existing_deleted_users_task_existing_ongoing_delete_data_requests() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Enable automatic creation of delete data requests.
|
||||
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
|
||||
|
||||
// Create a user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user);
|
||||
// Create delete data request for the user.
|
||||
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
$requestid = $datarequest->get('id');
|
||||
api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
|
||||
|
||||
// The user should have an ongoing delete data request.
|
||||
$this->assertCount(1, api::get_data_requests($user->id,
|
||||
[api::DATAREQUEST_STATUS_AWAITING_APPROVAL], [api::DATAREQUEST_TYPE_DELETE]));
|
||||
|
||||
// Mark the user as deleted.
|
||||
$user->deleted = 1;
|
||||
$DB->update_record('user', $user);
|
||||
|
||||
// The user should still have the existing ongoing delete data request.
|
||||
$this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id,
|
||||
[api::DATAREQUEST_STATUS_AWAITING_APPROVAL], [api::DATAREQUEST_TYPE_DELETE]));
|
||||
|
||||
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
|
||||
// After running the scheduled task, the user should have only one delete data request.
|
||||
$this->assertCount(1, api::get_data_requests($user->id, [],
|
||||
[api::DATAREQUEST_TYPE_DELETE]));
|
||||
// The user should not have a newly created delete data request.
|
||||
$this->assertCount(0, api::get_data_requests($user->id,
|
||||
[api::DATAREQUEST_STATUS_PENDING], [api::DATAREQUEST_TYPE_DELETE]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a delete data request for pre-existing deleted users
|
||||
* is not created when there are existing finished delete data requests
|
||||
* for that particular user.
|
||||
*/
|
||||
public function test_delete_existing_deleted_users_task_existing_finished_delete_data_requests() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Enable automatic creation of delete data requests.
|
||||
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
|
||||
|
||||
// Create a user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user);
|
||||
// Create delete data request for the user.
|
||||
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
$requestid = $datarequest->get('id');
|
||||
api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED);
|
||||
|
||||
// The user should have a delete data request.
|
||||
$this->assertCount(1, api::get_data_requests($user->id, [],
|
||||
[api::DATAREQUEST_TYPE_DELETE]));
|
||||
// The user should not have an ongoing data requests.
|
||||
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
|
||||
|
||||
// Mark the user as deleted.
|
||||
$user->deleted = 1;
|
||||
$DB->update_record('user', $user);
|
||||
|
||||
// The user should still have the existing finished delete data request.
|
||||
$this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id,
|
||||
[api::DATAREQUEST_STATUS_CANCELLED], [api::DATAREQUEST_TYPE_DELETE]));
|
||||
|
||||
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
|
||||
// After running the scheduled task, the user should still have one delete data requests.
|
||||
$this->assertCount(1, api::get_data_requests($user->id, [],
|
||||
[api::DATAREQUEST_TYPE_DELETE]));
|
||||
// The user should still have the existing finished delete data request.
|
||||
$this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id,
|
||||
[api::DATAREQUEST_STATUS_CANCELLED], [api::DATAREQUEST_TYPE_DELETE]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to execute a particular task.
|
||||
*
|
||||
* @param string $task The task.
|
||||
*/
|
||||
private function execute_task($task) {
|
||||
// Run the scheduled task.
|
||||
ob_start();
|
||||
$task = \core\task\manager::get_scheduled_task($task);
|
||||
$task->execute();
|
||||
ob_end_clean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Tests for the event observer.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \tool_dataprivacy\event\user_deleted_observer;
|
||||
use \tool_dataprivacy\api;
|
||||
|
||||
/**
|
||||
* Event observer test.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_dataprivacy_user_deleted_observer_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Ensure that a delete data request is created upon user deletion.
|
||||
*/
|
||||
public function test_create_delete_data_request() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Enable automatic creation of delete data requests.
|
||||
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
|
||||
|
||||
// Create another user who is not a DPO.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
$event = $this->trigger_delete_user_event($user);
|
||||
|
||||
user_deleted_observer::create_delete_data_request($event);
|
||||
// Validate that delete data request has been created.
|
||||
$this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a delete data request is not created upon user deletion if automatic creation of
|
||||
* delete data requests is disabled.
|
||||
*/
|
||||
public function test_create_delete_data_request_automatic_creation_disabled() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Disable automatic creation of delete data requests.
|
||||
set_config('automaticdeletionrequests', 0, 'tool_dataprivacy');
|
||||
|
||||
// Create another user who is not a DPO.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
$event = $this->trigger_delete_user_event($user);
|
||||
|
||||
user_deleted_observer::create_delete_data_request($event);
|
||||
// Validate that delete data request has been created.
|
||||
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a delete data request is being created upon user deletion
|
||||
* if an ongoing export data request (or any other except delete data request) for that user already exists.
|
||||
*/
|
||||
public function test_create_delete_data_request_export_data_request_preexists() {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Enable automatic creation of delete data requests.
|
||||
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
|
||||
|
||||
// Create another user who is not a DPO.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
// Create a delete data request for $user.
|
||||
api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT);
|
||||
// Validate that delete data request has been created.
|
||||
$this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_EXPORT));
|
||||
$this->assertEquals(0, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
|
||||
|
||||
$event = $this->trigger_delete_user_event($user);
|
||||
|
||||
user_deleted_observer::create_delete_data_request($event);
|
||||
// Validate that delete data request has been created.
|
||||
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a delete data request is not being created upon user deletion
|
||||
* if an ongoing delete data request for that user already exists.
|
||||
*/
|
||||
public function test_create_delete_data_request_ongoing_delete_data_request_preexists() {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Enable automatic creation of delete data requests.
|
||||
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
|
||||
|
||||
// Create another user who is not a DPO.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
// Create a delete data request for $user.
|
||||
api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
// Validate that delete data request has been created.
|
||||
$this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
|
||||
|
||||
$event = $this->trigger_delete_user_event($user);
|
||||
|
||||
user_deleted_observer::create_delete_data_request($event);
|
||||
// Validate that additional delete data request has not been created.
|
||||
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a delete data request is being created upon user deletion
|
||||
* if a finished delete data request (excluding complete) for that user already exists.
|
||||
*/
|
||||
public function test_create_delete_data_request_canceled_delete_data_request_preexists() {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Enable automatic creation of delete data requests.
|
||||
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
|
||||
|
||||
// Create another user who is not a DPO.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
// Create a delete data request for $user.
|
||||
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
$requestid = $datarequest->get('id');
|
||||
api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED);
|
||||
|
||||
// Validate that delete data request has been created and the status has been updated to 'Canceled'.
|
||||
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
|
||||
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
|
||||
|
||||
$event = $this->trigger_delete_user_event($user);
|
||||
|
||||
user_deleted_observer::create_delete_data_request($event);
|
||||
// Validate that additional delete data request has been created.
|
||||
$this->assertEquals(2, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
|
||||
$this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a delete data request is being created upon user deletion
|
||||
* if a completed delete data request for that user already exists.
|
||||
*/
|
||||
public function test_create_delete_data_request_completed_delete_data_request_preexists() {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Enable automatic creation of delete data requests.
|
||||
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
|
||||
|
||||
// Create another user who is not a DPO.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
// Create a delete data request for $user.
|
||||
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
$requestid = $datarequest->get('id');
|
||||
api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE);
|
||||
|
||||
// Validate that delete data request has been created and the status has been updated to 'Completed'.
|
||||
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
|
||||
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
|
||||
|
||||
$event = $this->trigger_delete_user_event($user);
|
||||
|
||||
user_deleted_observer::create_delete_data_request($event);
|
||||
// Validate that additional delete data request has not been created.
|
||||
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
|
||||
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to trigger and capture the delete user event.
|
||||
*
|
||||
* @param object $user The user object.
|
||||
* @return \core\event\user_deleted $event The returned event.
|
||||
*/
|
||||
private function trigger_delete_user_event($user) {
|
||||
|
||||
$sink = $this->redirectEvents();
|
||||
delete_user($user);
|
||||
$events = $sink->get_events();
|
||||
$sink->close();
|
||||
$event = reset($events);
|
||||
// Validate event data.
|
||||
$this->assertInstanceOf('\core\event\user_deleted', $event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user