Compare commits
1716 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e878219fb | |||
| e8c0748998 | |||
| 8360ef9293 | |||
| d750647969 | |||
| 2a3758ace1 | |||
| a868b63085 | |||
| ffe4e082b0 | |||
| e4a8e40912 | |||
| b51a2ccba5 | |||
| 0cdffdca4c | |||
| bdf22ec3b7 | |||
| 5d4597cda7 | |||
| 90a5693234 | |||
| aec6391bc2 | |||
| 21f27a2bd3 | |||
| 9f071b48c5 | |||
| 3717e40450 | |||
| f7e4dacd1e | |||
| c80db2680f | |||
| 438cf695bf | |||
| cc52293498 | |||
| 3eab97cc34 | |||
| e225d71259 | |||
| 2504aa33e0 | |||
| 816e6abb1b | |||
| d3f2f990dd | |||
| 427463a525 | |||
| f46413cb03 | |||
| 723d1a7475 | |||
| 070f24d006 | |||
| 911f748806 | |||
| 9ed8cafff3 | |||
| d85fed227b | |||
| 0b9e9bf89d | |||
| ed5a080034 | |||
| 419275478b | |||
| 6a06c7ca03 | |||
| 6b9cce6b98 | |||
| f8e7257327 | |||
| 37a1983740 | |||
| ebbb7f9a5f | |||
| 95209cb64a | |||
| 973c2551aa | |||
| cdfdef65b5 | |||
| 7f5479a142 | |||
| 5320dc6722 | |||
| c67c536dfe | |||
| 4c73fd62b0 | |||
| b38eb7882b | |||
| c4af3e8ec1 | |||
| d5cdc97b2f | |||
| bc2a000664 | |||
| 651f5c494b | |||
| 536faec42a | |||
| c7ff973845 | |||
| 3235bb616d | |||
| 86c240ddfa | |||
| a94b5db132 | |||
| 47ff684a89 | |||
| 50733f26f7 | |||
| d14925e818 | |||
| a5dfb4cb3b | |||
| 591a7952c2 | |||
| f4713ca7ec | |||
| ce0a325481 | |||
| e12a258678 | |||
| 2f9592c57a | |||
| 79cbc5ca4c | |||
| e6e776dc77 | |||
| f5dde8d05e | |||
| e8833c4a18 | |||
| a71f1f4336 | |||
| 6e3e7d3a01 | |||
| a37d6ab8ff | |||
| 96803a9db7 | |||
| dfff9d51bb | |||
| c9bb208ba8 | |||
| 7b5b613820 | |||
| 93551b5f58 | |||
| 6fb045026e | |||
| a6ad453ad7 | |||
| 6456fa0617 | |||
| 903be41022 | |||
| 72163b6f59 | |||
| 461981dca0 | |||
| d9b034dadb | |||
| 0937c18c6d | |||
| 2e80ca02f3 | |||
| 331aec2f27 | |||
| 6d31a91aff | |||
| df51b94410 | |||
| c96968b7e9 | |||
| e995a79f7b | |||
| f148fa37cb | |||
| 1e1ab33625 | |||
| b049fa2c8d | |||
| d6f6c78c73 | |||
| 4c0a87629f | |||
| fb5836b42e | |||
| 8fd354bc92 | |||
| 8374e2db3f | |||
| c4f137749d | |||
| 18735c9f51 | |||
| 77474f4733 | |||
| 41a15e6d73 | |||
| bcd2debd4e | |||
| 4440ca8121 | |||
| 4d67bcdb5e | |||
| af3214adae | |||
| a3f16eb4ea | |||
| 579700dcf4 | |||
| ea2ed45ec7 | |||
| c35399cc17 | |||
| f0f468ba1f | |||
| 8bb8d213db | |||
| a75f2c2f1b | |||
| 85da5ed5e2 | |||
| 82b6519647 | |||
| 799e4e2fbf | |||
| ecbcbb2a7f | |||
| 6699b20eb9 | |||
| 5af18c34e3 | |||
| fb911d3422 | |||
| 895ec79efd | |||
| 7ccf3ebdf2 | |||
| e3c4a1fc9f | |||
| 1bcf9c913f | |||
| ecf8e7eda9 | |||
| 9b6f09eeeb | |||
| 83e40aa825 | |||
| 008121e57f | |||
| 67bc6c6ebb | |||
| 649ffed15d | |||
| 57ac26edae | |||
| f05e71a443 | |||
| 0cccbe18f2 | |||
| 861b48d332 | |||
| 5719590cb1 | |||
| 571b22a693 | |||
| 603aaf66c4 | |||
| 99f29e2580 | |||
| 3ae9b14420 | |||
| 302a14d287 | |||
| 1e41439336 | |||
| b8ddf80ca6 | |||
| d2abeb3b3c | |||
| fd0bbd3201 | |||
| 1084a0afe4 | |||
| 43f894f2ba | |||
| 29086bc3b3 | |||
| 395b1f8fe7 | |||
| 1a20233e7f | |||
| 2a26069c13 | |||
| 4e9c2677e5 | |||
| 763c1bc6fb | |||
| e5f929a854 | |||
| 78bb834249 | |||
| a4c9320fae | |||
| c59ad632a2 | |||
| ab2a6da0da | |||
| fb14b2aab7 | |||
| cf775cb602 | |||
| 0603a6db6d | |||
| 9fb8d55550 | |||
| 9f19bb9dfb | |||
| 06e372a3c8 | |||
| 2980c823f8 | |||
| c025ce8af1 | |||
| 3144a87a57 | |||
| 07579a6bba | |||
| 1210555318 | |||
| 68bc7c4658 | |||
| f0f73daeb9 | |||
| 46f6c0da7b | |||
| b69c36612d | |||
| c2587d48ba | |||
| 9400329867 | |||
| 2486b50a7b | |||
| ac3a49cd4a | |||
| 9cf38a5d24 | |||
| 1b7a0a4d1a | |||
| d403ff368e | |||
| 541122f622 | |||
| 960fef4854 | |||
| 88e90e3a6b | |||
| 16e46ae78b | |||
| db3008e2fa | |||
| 06c98804f8 | |||
| 8e85d900db | |||
| e25999cfeb | |||
| a2740b43c5 | |||
| b14eb813a0 | |||
| 7ed8b29579 | |||
| 64d7b87888 | |||
| 5944f26805 | |||
| 420daee391 | |||
| 2afeb308b6 | |||
| eacb5e71d5 | |||
| ad2ee11aae | |||
| be5dd33e4d | |||
| a16553fc55 | |||
| a8159737cf | |||
| 88c0ab547b | |||
| 9b71e8a8b0 | |||
| 6c461125c5 | |||
| e3138b870e | |||
| eb18ff3cf3 | |||
| 8c435f5f42 | |||
| 75153c10e6 | |||
| 328aa06432 | |||
| 64a588ccca | |||
| 2844f968b0 | |||
| 82e028d7bd | |||
| c14b0c9bcd | |||
| 7c815adea4 | |||
| a5fb32a623 | |||
| c0d4b3ba08 | |||
| 4a6403f068 | |||
| 1e0cf52994 | |||
| 6ad3397d69 | |||
| 4fc6d79fa3 | |||
| cbf5a395a3 | |||
| be521ca625 | |||
| 16e61e37ac | |||
| e39f009680 | |||
| be1f9cfba2 | |||
| b895b8b74f | |||
| 4777f33bbe | |||
| 2f4185c8ba | |||
| 8eaffb287a | |||
| 1f666f276e | |||
| d7072f15e9 | |||
| cf1f48b322 | |||
| 787991f1d4 | |||
| b5a94c3e85 | |||
| 2e77251c8b | |||
| 124c24eb0e | |||
| f958fafc60 | |||
| 70a11e3daa | |||
| 58a6e4bfe4 | |||
| 6e81e436bf | |||
| 85fec2b605 | |||
| b8360b3cfb | |||
| 2baaed1860 | |||
| b56666b99c | |||
| 983ae52810 | |||
| 2c6d5bf004 | |||
| 11cf3ecc56 | |||
| 3ebff5cc76 | |||
| 609203ada6 | |||
| b50e1b2450 | |||
| 6649b4f560 | |||
| 365435182e | |||
| 279ec1f986 | |||
| b52735e2b4 | |||
| ee95b9c319 | |||
| 81e8a74a7b | |||
| 5a76a51e3a | |||
| 152e59f56d | |||
| d41a75ffad | |||
| f9d389f45b | |||
| 284b7c820e | |||
| 6a09040e4d | |||
| 46bd72dc4f | |||
| 1f3fd18541 | |||
| 247b70b0de | |||
| 61319327b1 | |||
| 1237fe9140 | |||
| e489ebbb0b | |||
| 15c3d7e208 | |||
| c1aa2c28c2 | |||
| 901c95587f | |||
| b39f7194a8 | |||
| 9ebaea25f6 | |||
| a67284cb47 | |||
| f14ed54861 | |||
| e618ee9fb5 | |||
| b4ce080632 | |||
| a660a74f7a | |||
| 017578d64f | |||
| e37c1f7ed0 | |||
| 57768467dc | |||
| b562767744 | |||
| 95f2f79218 | |||
| 021143d5cc | |||
| fcd19e1faf | |||
| c9d4bb2d53 | |||
| 4c6ae316d8 | |||
| 8b1bf65b22 | |||
| 6f60c7a6f2 | |||
| 8c6a45020e | |||
| 38f65afecf | |||
| cd05eba738 | |||
| 4b1a578e2e | |||
| 22497d9d74 | |||
| 1e90a547e9 | |||
| e2e9df59d7 | |||
| 609b020512 | |||
| da5d00f2b2 | |||
| 765c99d4d9 | |||
| 4b04cbdbab | |||
| 88d5dbb63f | |||
| 4412db99fc | |||
| ef227b403e | |||
| 96457d1739 | |||
| e293c3deb3 | |||
| 26f70e8f28 | |||
| 3fdd140150 | |||
| 29cb950320 | |||
| ac25e4ace6 | |||
| c00f5586f3 | |||
| 821a753844 | |||
| 3cd69fd335 | |||
| 67d77a2456 | |||
| 9ca8ccbefc | |||
| 4166d28e1f | |||
| 57e3305b41 | |||
| 80f46ee1c0 | |||
| 7d837832e5 | |||
| 43246bd859 | |||
| 29e7974006 | |||
| 7c512ad12f | |||
| 5b01d6249b | |||
| a36f9d20ed | |||
| 68da8600b7 | |||
| b366722a14 | |||
| 712fc8341c | |||
| ed72175e45 | |||
| 042748a342 | |||
| 98125d376a | |||
| aa3c3bd353 | |||
| 02fab5539c | |||
| a32a22f07f | |||
| f8be708257 | |||
| 2e8f29095f | |||
| 56ce8d00da | |||
| 40bf3feacd | |||
| 3691ff2f40 | |||
| fdfa0d497f | |||
| ecb4b0260e | |||
| da73f1364d | |||
| 3430f5252a | |||
| 2f56e8a287 | |||
| 566978d358 | |||
| 79d87ad683 | |||
| f4b78030ae | |||
| 97eac5487e | |||
| f5065aad46 | |||
| b5cc9d4e96 | |||
| 29d25b33a5 | |||
| 5f2edc4df7 | |||
| 5900a79aca | |||
| 18d086a8a4 | |||
| cb14d331fe | |||
| 56ec2b0949 | |||
| d8aba89429 | |||
| 4632b4a151 | |||
| 9d046294fb | |||
| 968c6115dc | |||
| e4eb5170a1 | |||
| 1340c03ba6 | |||
| 16668320a6 | |||
| a912d44652 | |||
| dfce9c02d2 | |||
| 9ffc3296cc | |||
| 7650dbcaae | |||
| 4015cb2d05 | |||
| b1aac17d18 | |||
| de341c20d4 | |||
| 5156ced934 | |||
| 512044f0c9 | |||
| 9d8470e4b8 | |||
| 613d4f9e6d | |||
| fc28257e09 | |||
| 672905cb4f | |||
| 1aaa130376 | |||
| ee38a8d1e4 | |||
| 084dadaa86 | |||
| 790c306bf7 | |||
| 16db1b8083 | |||
| f91e884d68 | |||
| 7dcc1e721c | |||
| f32c2f7c47 | |||
| 808e7c80dc | |||
| bb13181338 | |||
| f13544412e | |||
| c8b4ee45a4 | |||
| 773e24cad4 | |||
| 86eb5bb7e2 | |||
| 55a1e8b719 | |||
| 76570e405a | |||
| eeb374e2e6 | |||
| b4999d92d9 | |||
| 113c94cf9a | |||
| 583799d5b9 | |||
| 7f0903200f | |||
| 061db30ef7 | |||
| 91ef3d2e40 | |||
| 07baf8e940 | |||
| 5362c6f185 | |||
| 2a0ee3647f | |||
| 2496dc283c | |||
| 5b10127f11 | |||
| 551975b2ac | |||
| 111998407c | |||
| 7afbae7e1a | |||
| 7fc84f65de | |||
| 6cc427e025 | |||
| 1188a6e41b | |||
| 9cfb533e2d | |||
| 2203bf4ad4 | |||
| 82f74c8e2b | |||
| 2fbae51df0 | |||
| f087b00ea6 | |||
| 1bfa3f5697 | |||
| 3692fe595c | |||
| 10097b1b61 | |||
| 4c7af8f17b | |||
| 864374faf2 | |||
| f6b4c923ec | |||
| a702f2b935 | |||
| daa7d65f34 | |||
| b7e4fbb246 | |||
| 4745892272 | |||
| 559759aa4a | |||
| edb89914d8 | |||
| c2e3ca2f6c | |||
| c758547bd7 | |||
| f20b808629 | |||
| 6a58779607 | |||
| 83c4b6d01e | |||
| 7c72db7d7e | |||
| 3aca528730 | |||
| 0dc3fbf069 | |||
| c5dab021db | |||
| a08ecb70f0 | |||
| d5901aa09c | |||
| 394c5ae9b8 | |||
| 79bfd75d15 | |||
| 35e9c7202e | |||
| 4ff43a278b | |||
| 9485649edd | |||
| 673340157a | |||
| c6dd258bfd | |||
| f4a61ac99a | |||
| df367e966a | |||
| 276097e0d7 | |||
| b64e7253b2 | |||
| 02b46dd142 | |||
| ce2684efd9 | |||
| 23c05fe9ce | |||
| 944d0fd395 | |||
| fa2a738f80 | |||
| 35bc9f8bb2 | |||
| 2e36915e1f | |||
| aad94d9332 | |||
| 96c3b2042e | |||
| c7390b1c4f | |||
| 60b951eca5 | |||
| 89d45109aa | |||
| 690e03bfea | |||
| 37438b2a00 | |||
| 89e3b8aa80 | |||
| b87ad367d9 | |||
| 27cfbf2986 | |||
| 8575b451c4 | |||
| e3a1c26bd7 | |||
| 51b022db9b | |||
| 33a95ab889 | |||
| a6a0cc37ed | |||
| e452305994 | |||
| 135a5df634 | |||
| b78c5102ac | |||
| b9c412be9f | |||
| 01eeab4ed2 | |||
| 30f7f010a9 | |||
| f8c43813de | |||
| e92bb861d4 | |||
| 83a6d7a194 | |||
| 0a39efd896 | |||
| bfc740821b | |||
| 9112915855 | |||
| a6ff7933a1 | |||
| e35c7c2c50 | |||
| d699efb4ba | |||
| 0073588e27 | |||
| 80cf67eaba | |||
| a23caa49b9 | |||
| 0a519e70d4 | |||
| dfecd50ac3 | |||
| 56d8e413ba | |||
| 3955dde284 | |||
| dc47c7fc8c | |||
| d55565066c | |||
| 810bb4bef5 | |||
| b6f83c07ed | |||
| 58c02975d1 | |||
| ef212b671e | |||
| cac9d8d665 | |||
| 127c4954a5 | |||
| af1a540eab | |||
| 421b7b0c12 | |||
| 8b508a6004 | |||
| 8bee660d88 | |||
| 54216a57a8 | |||
| d078274a59 | |||
| 5fdb6183e5 | |||
| d6b498f6ae | |||
| fc2cde8f80 | |||
| 28ca1302e5 | |||
| 5809488bb6 | |||
| 4e0886f362 | |||
| 96388117d4 | |||
| c2650d3f3d | |||
| e7fd2113c0 | |||
| 32878692e3 | |||
| 696ecda501 | |||
| 31bfc01e9d | |||
| f779e204ac | |||
| 9ccfa64940 | |||
| 2ca939b838 | |||
| d04454fed5 | |||
| 315ffed9fb | |||
| 9933be5fad | |||
| 1978e7205d | |||
| 307ee3442b | |||
| 42add6e9ef | |||
| 6e493f59b8 | |||
| 512547df7e | |||
| 971d02f6f4 | |||
| 841a0bc6e2 | |||
| 17439b2c46 | |||
| b675d6c5ee | |||
| edfc17d7ba | |||
| ab16666f05 | |||
| 628a7bb557 | |||
| 058db3e6c8 | |||
| ca2f0f052c | |||
| 7ef3dbf5f4 | |||
| f0e03e6918 | |||
| bb56746b61 | |||
| 11a19454d7 | |||
| aa4f226f5c | |||
| a3c626c7e7 | |||
| 8ff8f4e558 | |||
| 63510633b4 | |||
| 82447d7ded | |||
| 6dae57a102 | |||
| 789dcd6975 | |||
| b908052850 | |||
| f96287e91b | |||
| 55df5e4c05 | |||
| 68987522ea | |||
| cd496c0307 | |||
| 977632b715 | |||
| d8231aee5e | |||
| d3b5e29e2e | |||
| 8f7f422709 | |||
| 4c7f930af8 | |||
| 7625b00200 | |||
| df2ed3c8bc | |||
| b2e67dc913 | |||
| a000765a13 | |||
| e1921cead6 | |||
| a3c30c9fc8 | |||
| 3af336b41a | |||
| a757b3f80b | |||
| d206c98d98 | |||
| a2793d3850 | |||
| ee522a9f42 | |||
| 23c79dff60 | |||
| b94e3e5f56 | |||
| f4e1906562 | |||
| bdbecafdc0 | |||
| e1f1cf9bec | |||
| a07e8d7023 | |||
| a14a44ee13 | |||
| 763f4e1e52 | |||
| 60987ecfae | |||
| 20957bbd12 | |||
| 4b56876fe4 | |||
| 33ae40340d | |||
| 923a97629d | |||
| 37e1f7dbd9 | |||
| 9ae66863bc | |||
| 180d76c5f8 | |||
| 4913f8f270 | |||
| d364d88a51 | |||
| fe552e47f0 | |||
| b0ea09e200 | |||
| a1d910f2e8 | |||
| f7dec77e56 | |||
| f0f4270a8d | |||
| 1523c8bc6f | |||
| 68c72aadc2 | |||
| 4c21639e66 | |||
| ead79c8627 | |||
| 50edf48571 | |||
| b427ab4f70 | |||
| 15c87e0bb1 | |||
| 4ead20ffea | |||
| f37269b124 | |||
| f9de59ada9 | |||
| f5cf23270a | |||
| 88c2c6758a | |||
| 0da99d3c4d | |||
| 4aa5cee144 | |||
| ad6b515ab6 | |||
| 56cc378799 | |||
| 3b8730628b | |||
| ce8ada051d | |||
| d2481db6b0 | |||
| e3eb339d89 | |||
| d906136aa5 | |||
| c675598ac0 | |||
| ea321f34c4 | |||
| bb34fe0838 | |||
| 748409df5b | |||
| d07431a2c6 | |||
| 60735e6bee | |||
| 9d870a800d | |||
| 82083929b1 | |||
| 52efa6a2f0 | |||
| 8396ade620 | |||
| e159265c55 | |||
| 46782ecb13 | |||
| 2a34d4b3fd | |||
| e1dca20127 | |||
| 73b5e30559 | |||
| d7c85ebb3d | |||
| 44451a6a40 | |||
| 9bb62d6cdd | |||
| 13ed41e499 | |||
| 9825658af9 | |||
| 2a939b9f7f | |||
| 57eae451f1 | |||
| 30d8fed964 | |||
| dc23a75d68 | |||
| 78552ea548 | |||
| 6404971918 | |||
| 414ca3e1c1 | |||
| 4d79fb98a5 | |||
| 199d36febc | |||
| 3cd0a770d6 | |||
| 55e5747d04 | |||
| 3433b0bfb5 | |||
| 7f4d3aac07 | |||
| 790235e123 | |||
| 0ee1075ec4 | |||
| f7d57a3d96 | |||
| bbb9ffb0c1 | |||
| e882740a56 | |||
| 1da0d646b3 | |||
| b193f7754d | |||
| 73c8d34ab8 | |||
| bf1b7ef8c9 | |||
| 662878b6b5 | |||
| bc35326e3e | |||
| df0a4ec014 | |||
| 8782043946 | |||
| f669a63cca | |||
| a25a4d30be | |||
| aae2aa2d81 | |||
| 1760650216 | |||
| 52a16f409d | |||
| 19768075ff | |||
| fdc3a96e54 | |||
| a25222e9c4 | |||
| eaae1a7ea1 | |||
| c81cc39a02 | |||
| a3d372061a | |||
| 5de480c21b | |||
| 446de56bfc | |||
| 5e997d239c | |||
| cb69e0ca88 | |||
| 0a0e5a03a0 | |||
| 12a3bcee4f | |||
| ddf649109d | |||
| 18b69cc33d | |||
| 4283575676 | |||
| e1e20d0c9b | |||
| 009f51644d | |||
| 5171746d5a | |||
| a395c8a60e | |||
| 575584cb94 | |||
| 8fe266c09d | |||
| a7b6eaa544 | |||
| 831b2f6729 | |||
| e2f9bdd44b | |||
| 707586883b | |||
| f5029c213a | |||
| b96835a505 | |||
| 5e0b16e369 | |||
| 7b9e3e0336 | |||
| eb5263ec3f | |||
| ac2524447e | |||
| ea1f81dd64 | |||
| 95c2cadcb8 | |||
| 116b0d26eb | |||
| 07c7af673b | |||
| a32ea64a88 | |||
| de6f2519a5 | |||
| 6539fe917f | |||
| 23b8d594b5 | |||
| 7b1593ad5c | |||
| b6960fa224 | |||
| 5704b265ab | |||
| a5915ed4fc | |||
| 73a8c336c9 | |||
| 710c4de820 | |||
| e6d622106c | |||
| 9383ee853c | |||
| fa92fcbe15 | |||
| f2eb4c0b8f | |||
| cc9c1ed9a0 | |||
| 76793973e5 | |||
| d3e89fec58 | |||
| 8757d7cb6b | |||
| f61b827260 | |||
| 2f6a91dbdb | |||
| f9ba909869 | |||
| 5b8e3123a9 | |||
| eaf9da10ad | |||
| 0f9997a4ec | |||
| 591b16786c | |||
| b1208d8bf8 | |||
| f4fcee9a9d | |||
| d320454567 | |||
| 24eeb8ba01 | |||
| 0feb84a5dd | |||
| 90be3a42f3 | |||
| 2b227258e0 | |||
| b6cfa1975a | |||
| 8b4a53ca75 | |||
| 7de43df7ad | |||
| 497d75ae71 | |||
| b9c01f1f70 | |||
| 3cf54a0743 | |||
| d7bd4532f8 | |||
| 1ba678ef3b | |||
| 062bc628df | |||
| 412b9c0451 | |||
| 5c61e693af | |||
| de2b3bfdd8 | |||
| 46120189e3 | |||
| 8f30163736 | |||
| 62b97d84ee | |||
| 899e07d9a8 | |||
| c85128260f | |||
| 527f895e93 | |||
| abca4753b4 | |||
| 4b91080095 | |||
| 76528a767e | |||
| 16b34d53f2 | |||
| 25934551cc | |||
| 4b31593964 | |||
| 9c9e2d3e5e | |||
| cd786f0d0d | |||
| 58d63790c2 | |||
| 00a7f99146 | |||
| 6d0014c5e1 | |||
| 85f052f4b8 | |||
| 17f5816739 | |||
| 9cb3947dff | |||
| efc90f828b | |||
| da5827b559 | |||
| 9f01d23302 | |||
| a4f148707e | |||
| 7d08cc710b | |||
| 79a266ee48 | |||
| ad33de11d9 | |||
| dd897cc398 | |||
| 68f64fe990 | |||
| a7ebe98214 | |||
| 8ac3bac143 | |||
| 4910753dce | |||
| d54cbaff46 | |||
| 9e715a225c | |||
| 015e70cbcf | |||
| 0b28290a50 | |||
| 63e4e0d69d | |||
| aff8604240 | |||
| 505851f045 | |||
| 5cccfd2eab | |||
| f9f8edce1d | |||
| a9354dbbd6 | |||
| 05b441c5ef | |||
| 9fc1390e35 | |||
| 360b7f43b8 | |||
| 20342fca7e | |||
| 598d0b3c20 | |||
| 3d9de9c6d6 | |||
| cdd2633a99 | |||
| 7027daa89c | |||
| bd77a90267 | |||
| 3288990694 | |||
| 64793424f2 | |||
| 56cd4f7b84 | |||
| fdf6d94a1b | |||
| 84a98a4202 | |||
| f38e592d5d | |||
| 25b6c54c68 | |||
| 49f281ac53 | |||
| e16d2a8fc6 | |||
| 5447955b2e | |||
| b1a8e921cd | |||
| 97a6d18372 | |||
| c2c4125f13 | |||
| b0a2591274 | |||
| 6cbefb4064 | |||
| 35fbfe23b5 | |||
| 053eb1d5b9 | |||
| 5b8a649d4a | |||
| 29f71e53b5 | |||
| 5f6e9d5185 | |||
| 290c9bc478 | |||
| 58cc3aa1ab | |||
| 658b6742f5 | |||
| 32a103cd69 | |||
| 88c29f7fbe | |||
| 7902349b06 | |||
| 0857000602 | |||
| ce55556b2e | |||
| ddcf2245a8 | |||
| 39a072f09f | |||
| d233c5d063 | |||
| f27a271a8e | |||
| 38839b0afb | |||
| 07606ab87f | |||
| 9143b3db12 | |||
| 063f886160 | |||
| e43fcd4373 | |||
| 4369b8d6e5 | |||
| 7cb0129e59 | |||
| 80ead07e28 | |||
| b6a5ae21c7 | |||
| 22b0849f45 | |||
| 73a5fcacf6 | |||
| 25b3094acb | |||
| 7fb4b0d52d | |||
| 86646b27d2 | |||
| 1afe64dbea | |||
| 56c4024330 | |||
| 7bbf87a853 | |||
| c55fd56b87 | |||
| 39e35674d1 | |||
| 6f3c6cf4ba | |||
| 8159f6ae0a | |||
| 6cd1ea6cf3 | |||
| 4daf168de7 | |||
| 3bea0fa387 | |||
| 6d03e5e554 | |||
| 9d0c5cbbc4 | |||
| 40a90154ca | |||
| e0f64e0feb | |||
| 1a33d0869a | |||
| f2211eb823 | |||
| 857cfb1429 | |||
| bd6cc5c145 | |||
| 1a1d03caeb | |||
| 7e3eaa5d6f | |||
| 512091f40c | |||
| 722f0c03c0 | |||
| 892e441634 | |||
| e24dfc1a07 | |||
| 44d00da135 | |||
| de736d0c84 | |||
| e78bd0cefb | |||
| e208669c64 | |||
| 098d616a30 | |||
| 995d434de1 | |||
| 366a85cbb4 | |||
| 0c94037196 | |||
| fa0a861400 | |||
| 068c66f242 | |||
| b26c30d8f9 | |||
| ac1c36c728 | |||
| 069dfe5fc3 | |||
| b1904ba9cb | |||
| 951ae21309 | |||
| b55b2e4f6e | |||
| b7d2f7e1d4 | |||
| d4a5339029 | |||
| 6185c3455a | |||
| 44b012e5fd | |||
| b8558f33aa | |||
| 9674218469 | |||
| e3ddf1e324 | |||
| f30b061f48 | |||
| 7f7e78269f | |||
| 744ee9322c | |||
| bdf43538fb | |||
| 0bf20c560e | |||
| 7817df3048 | |||
| 8baf412ef7 | |||
| 86eff20271 | |||
| 81e11e98f7 | |||
| 4dee91aabf | |||
| 5c74c84c9d | |||
| 8c682cd317 | |||
| 0603eabe6e | |||
| 5ffe934241 | |||
| 2816fa5725 | |||
| f29fda6b3e | |||
| de103e2329 | |||
| 8b32d682a5 | |||
| 117006e9be | |||
| 18c5565a1e | |||
| 60c2d866de | |||
| 6324e6cbdf | |||
| 107a70c18c | |||
| 38c7b40532 | |||
| d19ca2c152 | |||
| d61e7f1e3f | |||
| 7331c8af56 | |||
| 866cabf59d | |||
| 9477b1d568 | |||
| 38f9254cd4 | |||
| 6e5c713b60 | |||
| 9ab9aa573b | |||
| 1ca1b28e52 | |||
| 5f13e8876b | |||
| a16f2c8b55 | |||
| 3addb66f2d | |||
| 505a25b403 | |||
| bb76ffff21 | |||
| 375eb5e975 | |||
| 1db088f085 | |||
| 100e479daf | |||
| 191d470949 | |||
| c62c4c9c40 | |||
| ac4f0d0e51 | |||
| 76075bcf6b | |||
| f55f89162d | |||
| 2d5d5d7902 | |||
| a83a1d9ee0 | |||
| aba0de88be | |||
| a996aa4ea6 | |||
| 3a6fe7db1b | |||
| cfb272d0ae | |||
| 0de0739e8e | |||
| 593d8d250f | |||
| b0147a4737 | |||
| 975c91dec8 | |||
| 5166e72190 | |||
| 8946ee7937 | |||
| 4edb56a2a5 | |||
| 02459b6527 | |||
| f9f8a9575e | |||
| 7daf207db7 | |||
| 41ae59a8de | |||
| 20d47f1b5f | |||
| a779b53b2f | |||
| 7e0e5b02c5 | |||
| 054937e378 | |||
| a8e4094570 | |||
| 78cee5d64c | |||
| 4d5cb2751b | |||
| 9890340b21 | |||
| 9cd03b8b63 | |||
| 15ce896d08 | |||
| 2d6d31fcb7 | |||
| 44c460c6be | |||
| 8855e0041d | |||
| 1209d00d26 | |||
| 084180ba7d | |||
| 0a11a740b3 | |||
| 86196f93fb | |||
| 7a2eac678c | |||
| 9af75abf69 | |||
| 1c6390697e | |||
| 9b40d154ff | |||
| aae67cd6dd | |||
| 5211af1658 | |||
| 663de3c3e6 | |||
| b469c777ed | |||
| e8954b775d | |||
| 5802130950 | |||
| 140e251310 | |||
| 359a6c32e8 | |||
| 9cd49fbad4 | |||
| 713385769d | |||
| 5874ac4f4a | |||
| 9708b6e9b3 | |||
| 16c5c24d19 | |||
| 57b938da30 | |||
| ce774c66a1 | |||
| d82567eb7f | |||
| a0f1d5f0eb | |||
| e273e709bf | |||
| ade905e248 | |||
| 8f76f17fc6 | |||
| a93eb1182e | |||
| e4099493ca | |||
| 71d3c750eb | |||
| 02a31d5f96 | |||
| 906525ce89 | |||
| 9a74362a48 | |||
| 8682e82694 | |||
| eeef0230ef | |||
| 71ae8603f5 | |||
| de8c8a4f81 | |||
| d2bceb2031 | |||
| 9f1fd9955a | |||
| 9093ace988 | |||
| 7c0ecc11e7 | |||
| 4e6ec59f8c | |||
| a266d8a24f | |||
| 1d67ef4cc7 | |||
| 39116eeac9 | |||
| 60e45d8e0d | |||
| 3135d3f048 | |||
| f7035a839d | |||
| ffdc051c71 | |||
| aa5c98350f | |||
| 206285819c | |||
| 31867d8880 | |||
| d4e17846f6 | |||
| e34e5486cf | |||
| d283cf2cee | |||
| 3a832ad75e | |||
| 9cab25219d | |||
| ff5ec88a55 | |||
| 996090515e | |||
| 6d682a5e5f | |||
| b7c157aeb2 | |||
| 5e1b7e4d28 | |||
| d408738313 | |||
| 3702ecbe48 | |||
| 6e73318877 | |||
| d3931b12d2 | |||
| 8f95b79590 | |||
| f426155996 | |||
| a7ee1a75e5 | |||
| a756b01f52 | |||
| 9c1fc95578 | |||
| 5e8396aa71 | |||
| 0d20375579 | |||
| 412697a70f | |||
| c0bb26aaf9 | |||
| fee8383edc | |||
| 29eb2d47be | |||
| d82e50f0a9 | |||
| ee1e93d511 | |||
| aed6a3f001 | |||
| 3446e3a257 | |||
| 385caa2e24 | |||
| 7f08932a68 | |||
| 4540e074bc | |||
| d05c1e47db | |||
| 442e48c4f2 | |||
| 8235feb1ec | |||
| 5b75de57f2 | |||
| 11334441a8 | |||
| e7a3f9b34a | |||
| 646c0464ec | |||
| 6c48400b67 | |||
| 4c6cc6c42a | |||
| fbbfd84803 | |||
| 1e9477e238 | |||
| 701f3fdb5c | |||
| 8a45db9903 | |||
| e0d05a355a | |||
| bd27666c42 | |||
| d1787723db | |||
| 408b3e0952 | |||
| 7e973b26de | |||
| a82893ac5f | |||
| a027e6dfdc | |||
| 275a590d6b | |||
| 7ba9f14fe0 | |||
| 0d70e9b8e9 | |||
| 168028b097 | |||
| 77a2e5621f | |||
| af142c1c2d | |||
| 6e2fc5e5e1 | |||
| e64487ed57 | |||
| a2b7e585b2 | |||
| b223dc24a6 | |||
| b3da43961a | |||
| edeaac4f7e | |||
| 432d3cc846 | |||
| 59de100b8b | |||
| f1e0f1c194 | |||
| 444d046f9e | |||
| 64722afd45 | |||
| be092b7309 | |||
| 005b2bc8a0 | |||
| f4bd5f5e1a | |||
| 1a8342d9d4 | |||
| 6a87753e12 | |||
| 1d16496ee6 | |||
| e7dc32ad80 | |||
| fde541a1eb | |||
| 95ee02b253 | |||
| f562290f78 | |||
| 1273a6665b | |||
| 74ea4a91e1 | |||
| d035bebb1e | |||
| 4c51211800 | |||
| 61edefee12 | |||
| 304bd3b26b | |||
| 57c5494741 | |||
| f1019f4616 | |||
| bbdb62fffe | |||
| cce2977770 | |||
| 60357a5551 | |||
| ce5ea7e187 | |||
| 36b7d0471c | |||
| 580a0918b4 | |||
| 6aaf582982 | |||
| d6cbf8b8fb | |||
| e760287046 | |||
| f0b924f980 | |||
| 1260ed9e33 | |||
| 79df6aee8c | |||
| ee9c931987 | |||
| 01389839df | |||
| 88a78e9307 | |||
| 99e51855a6 | |||
| 5263019b3a | |||
| 442dc8270a | |||
| dab8d7a0dc | |||
| 9f256a8380 | |||
| 6f0614ef19 | |||
| 43dfc84482 | |||
| 5a7e921f70 | |||
| 0220cc4bdf | |||
| d470ef4c27 | |||
| efecaa34a4 | |||
| 11694360bd | |||
| 060e39220c | |||
| e016281821 | |||
| 2267c9fe7f | |||
| 8bcca194ce | |||
| 9bb1a66c3c | |||
| 627b0a7a45 | |||
| f873ff9ad2 | |||
| 0dfef1313f | |||
| e647f49fe4 | |||
| 088f6b973a | |||
| 3c613eb0f9 | |||
| 05403f4989 | |||
| a0a10c4d4c | |||
| f92552d372 | |||
| 769987cf28 | |||
| 3bee541afc | |||
| 069bf5a9dd | |||
| b5c2fef399 | |||
| 6c5b701bb5 | |||
| 696e063e65 | |||
| 0807b60603 | |||
| 2e7dbfaabb | |||
| d6e31537bc | |||
| d01d3d8eef | |||
| 7ac4ad9a8b | |||
| 9d349b8170 | |||
| bd20ed1a7c | |||
| f5d6dcfd06 | |||
| 466ae79141 | |||
| 821001ce9c | |||
| ac7cf1cfeb | |||
| f2057a1115 | |||
| 61c36e125e | |||
| 629c7f75b3 | |||
| bdefa64956 | |||
| bfc047cef0 | |||
| 9c9227995c | |||
| 3a69718c58 | |||
| cb8a553af9 | |||
| 83dc898b00 | |||
| 3e6e80f96f | |||
| 1b052996ff | |||
| e516332576 | |||
| 50615d7105 | |||
| 867e35d77f | |||
| 72337efdf7 | |||
| c0d8a1c3e1 | |||
| f6be98b942 | |||
| d694bf2435 | |||
| 2f58da96bb | |||
| 66f63a3bf6 | |||
| 349ac5cbd7 | |||
| 7985a77474 | |||
| 9df6a3972d | |||
| c24997f074 | |||
| 5e26c462d3 | |||
| bd547eb6dd | |||
| b4db1fd8ad | |||
| 8cdf03c5a9 | |||
| 17567f33c9 | |||
| 8509759768 | |||
| 20eb1297ef | |||
| ddf605c655 | |||
| 6efc731aab | |||
| 6ea7ce8acb | |||
| 74a30640d6 | |||
| 105bfe1d95 | |||
| e87bbfbe94 | |||
| 5450a19dec | |||
| 094730c7d2 | |||
| 76f2ed9c6a | |||
| 0af0d1d7f6 | |||
| 6415201009 | |||
| 99566b5a60 | |||
| 06a92988fa | |||
| 4202a6935e | |||
| 7c730dfe01 | |||
| 6e0f59fe32 | |||
| 6ed9356daf | |||
| 8539f6168e | |||
| 9e6ea43d93 | |||
| 6b65d96d70 | |||
| 98e96df381 | |||
| 127b7446a0 | |||
| a032e58de6 | |||
| 4db39b4571 | |||
| a9b13cfac3 | |||
| c1d4386dd7 | |||
| b3065a69ed | |||
| aba27d5317 | |||
| 7391dfc208 | |||
| 6d2fedaa47 | |||
| 02f31bb65b | |||
| 1e2f780503 | |||
| 57bebf7c4b | |||
| f940b5dcd9 | |||
| 7cccc7e03a | |||
| 76483d4f45 | |||
| d3ef856e79 | |||
| 5c4aef2664 | |||
| ff7ec97918 | |||
| 4bd4b46070 | |||
| 2392c10734 | |||
| 6b74d76fa9 | |||
| f86bd7ece0 | |||
| 805417c33d | |||
| 127b54ffbe | |||
| 8450d9948a | |||
| 3eca9bf311 | |||
| 6b3738bc20 | |||
| 1974156194 | |||
| f0a4839154 | |||
| 3ab6d8ba2b | |||
| 1d2b6c84a2 | |||
| 266744a6f9 | |||
| c3a1ccc244 | |||
| 96df578e8e | |||
| 6cf6523cc7 | |||
| 43eadce6fc | |||
| 98fe6eee32 | |||
| 671ad6b497 | |||
| 49bfb1e5f9 | |||
| 000a5d0bec | |||
| 936ed6067f | |||
| 238006f649 | |||
| d11038dd43 | |||
| 6b8b06911b | |||
| fe1ae23939 | |||
| 64b5d44abc | |||
| 526e3e8c97 | |||
| 8668391630 | |||
| f35e7a0eeb | |||
| 949f522c13 | |||
| ac6bef4e38 | |||
| 72bb7ac302 | |||
| 1b4cd6be30 | |||
| 10dae695ec | |||
| fc12d8d459 | |||
| c46c85055b | |||
| f4411ba700 | |||
| 37002deeda | |||
| 06e4325bda | |||
| 8aa12f5bff | |||
| 84bef5413c | |||
| df26945105 | |||
| b9b8d94716 | |||
| ec92a0762d | |||
| 9bd851f39c | |||
| 502b17bebd | |||
| 2857ca0c9f | |||
| 5a1aa729df | |||
| 6020745280 | |||
| 948485dbd6 | |||
| 1da88f2c6a | |||
| ab38db069e | |||
| c1a29fc252 | |||
| a1d8976005 | |||
| c0171e6090 | |||
| bdf7f7613e | |||
| b42457028a | |||
| 783f2a8b8e | |||
| ac496f22af | |||
| a1c3aedf5b | |||
| a2c9e82c42 | |||
| 82d2d27cd3 | |||
| e8d8bccb25 | |||
| d5859dacc8 | |||
| d738ae91e7 | |||
| daff43e9c7 | |||
| 6aedf45bee | |||
| fa9fffdc9a | |||
| 7ad1d28554 | |||
| 3f3385c46a | |||
| 11ebc149ed | |||
| c985ad6263 | |||
| 6a6d683283 | |||
| ab9f7ab790 | |||
| 3248a578e9 | |||
| 3cd9d0a689 | |||
| 8ee6b2a201 | |||
| 692049a9c1 | |||
| 36895ee702 | |||
| 68e2d67939 | |||
| dd6f34e334 | |||
| 310eff99fc | |||
| 5047c83b9c | |||
| 5b531aabee | |||
| 5723130d84 | |||
| f490505b23 | |||
| ed90c88864 | |||
| faabcf6d76 | |||
| 8837262d2c | |||
| e30562a127 | |||
| b43f63c11f | |||
| 78ccc2c951 | |||
| 9835dc02dd | |||
| 5420485076 | |||
| e69a3f20ee | |||
| 5e29146127 | |||
| 421619e6e3 | |||
| 39ffa8c6e3 | |||
| 53e622ba88 | |||
| 3f70323ba7 | |||
| 99d1a03857 | |||
| a2ddeab20e | |||
| bb05597274 | |||
| cb8c503098 | |||
| dfe3389cf5 | |||
| aac5333538 | |||
| c9038ad429 | |||
| 6d0a80dfc1 | |||
| f22286dd83 | |||
| c4030220a4 | |||
| e088a1f198 | |||
| b6f35bf63e | |||
| b5282bfb67 | |||
| 5721a09e82 | |||
| 21e4f90ca8 | |||
| 9cb0f99c37 | |||
| 4c66ff31c8 | |||
| 2542cb94e4 | |||
| 97be43646f | |||
| 4b1adeef8d | |||
| 0ef5bc0b18 | |||
| 8632026272 | |||
| 706ba3f3af | |||
| c2c97dee39 | |||
| c74949b071 | |||
| 6dc3988fa0 | |||
| 1d59ae4286 | |||
| 38d309036c | |||
| 3c2fa60af3 | |||
| a9b2db5880 | |||
| 3482ea90a6 | |||
| c50377f55f | |||
| 2a9787beee | |||
| 73df15dd67 | |||
| 3fe9524aaf | |||
| 0ec785bfa5 | |||
| c6fa9f2ea2 | |||
| c485c0c443 | |||
| 474ec7e94b | |||
| d7b6ef5729 | |||
| fb6ae10135 | |||
| cadff587be | |||
| 74fbce7560 | |||
| 6ede900c49 | |||
| 4f7cf4088d | |||
| 76486ab639 | |||
| a3809c4fc6 | |||
| b80a22f5ba | |||
| ef6bdbae8e | |||
| 85abbf80e2 | |||
| f2757d51a9 | |||
| 24e83b2807 | |||
| 4e37604528 | |||
| 8b2ae94442 | |||
| 4dec82b25b | |||
| e3796a0678 | |||
| 104c9ffae9 | |||
| c5a4536bb8 | |||
| 740db2e5c3 | |||
| 9fc60dfeb1 | |||
| c270b92ddb | |||
| 835d570210 | |||
| 7a81bcdb34 | |||
| 9818499ed8 | |||
| 5e0c2c9815 | |||
| 50e48b78e9 | |||
| f3a11484ad | |||
| af9b4f1f3b | |||
| 25c946f628 | |||
| 110f3a3d68 | |||
| b20a4bc15c | |||
| d4315bf53e | |||
| f31105a9f5 | |||
| 771c8455cb | |||
| 8297de9fa3 | |||
| e336f07cd7 | |||
| 2ff2c49c85 | |||
| 2f7aaf1d53 | |||
| 551f57926a | |||
| b8daa178de | |||
| 8405e6f1cc | |||
| 89076b49fb | |||
| 8fc16d9e6a | |||
| 5136441985 | |||
| 1afa3903ae | |||
| 4c32762ca7 | |||
| 313531b030 | |||
| 2b37ee6049 | |||
| a5f3967e57 | |||
| 5b78584fdb | |||
| ce390c4725 | |||
| 942d4df55b | |||
| 23628a98fc | |||
| 2daceaf000 | |||
| 8675c2c908 | |||
| 70ffd8bb2b | |||
| 6c1aa273f2 | |||
| 8cc2d39207 | |||
| cac9523423 | |||
| cb4bb6efc5 | |||
| 750121698f | |||
| a78f6cd123 | |||
| c2fea8ae43 | |||
| 0a520053cb | |||
| 1da4edbaf0 | |||
| 9eff7d9535 | |||
| e154204728 | |||
| 2d90a7d91a | |||
| 8ac7c623d3 | |||
| caa9881c0d | |||
| b01258d7f0 | |||
| f0188d7b66 | |||
| 4e52235a23 | |||
| ab94fee038 | |||
| 2d64d63373 | |||
| 6cb165d681 | |||
| 55fc303a5f | |||
| c453ea21e1 | |||
| 24e4ec9d60 | |||
| 25e9be8ea9 | |||
| 9ffcaa7db7 | |||
| 936d21d2dd | |||
| d20b4b2f23 | |||
| d587c4bd90 | |||
| 3828033e98 | |||
| 4892662238 | |||
| ccbd142a63 | |||
| 3b52f52351 | |||
| 173968b25d | |||
| 4cdd9e77dc | |||
| 7805012e59 | |||
| 483d8fefa8 | |||
| 654eac4f13 | |||
| 86699bedb0 | |||
| 914e26d916 | |||
| ced6691934 | |||
| 07e1fa272f | |||
| ff1fbf2992 | |||
| 49e435c00a | |||
| b67ae5a0ad | |||
| b2b1bb2d29 | |||
| 5a13884a0f | |||
| d42328e223 | |||
| 3879b520b7 | |||
| 6a3cffd3c6 | |||
| 40790755ce | |||
| 310a1f0895 | |||
| ed3468e40b | |||
| 9bedb84419 | |||
| c843630aee | |||
| 94f5d062bd | |||
| 233847bdb7 | |||
| f29d518ba8 | |||
| 749c7cc5a9 | |||
| bb3ce79c7e | |||
| 4b52a5b2c6 | |||
| af344834e1 | |||
| cdc9e56198 | |||
| 0e0fd72d98 | |||
| 543005a342 | |||
| 5c33665a74 | |||
| 362518ef34 | |||
| ff246f2260 | |||
| fc1b13ede9 | |||
| 458b0fb900 | |||
| 5df53c769e | |||
| 380a437428 | |||
| 5f6539e379 | |||
| b44ddd4a7f | |||
| 833e2398ef | |||
| df9443db71 | |||
| 4d23edfb37 | |||
| 11489b20a5 | |||
| b3483e7ede | |||
| dd05e0d29e | |||
| 1b37fe78f9 | |||
| 82f21031a3 | |||
| 6545e50f62 | |||
| 3d7630a158 | |||
| 2588126d66 | |||
| de71bef7cb | |||
| e310f10818 | |||
| 9b6938f99b | |||
| 9c42327dd1 | |||
| 28631a1455 | |||
| aa7deee9cc | |||
| 009a9ca500 | |||
| 7de4759799 | |||
| 33433bb8a1 | |||
| 9efe4b84ce | |||
| 12d4fe9ee6 | |||
| 8793e120b9 | |||
| 43f742e1e5 | |||
| 3f4db3e89e | |||
| bea7b33840 | |||
| 4900f375d7 | |||
| a94b1fed30 | |||
| 9337d85eb3 | |||
| 2a8f28520d | |||
| e972b9e909 | |||
| 74c35a9133 | |||
| b4b5124c73 | |||
| af963ad2ae | |||
| 2341e8695e | |||
| f55516fd46 | |||
| 36e337b176 | |||
| 23e9fea9a5 | |||
| 1408aa8bc8 | |||
| 2f86cad04e | |||
| fd77e38fc9 | |||
| 0191113d24 | |||
| 016e029ea6 | |||
| 8bc0bbf1ee | |||
| 1dc4c34d07 | |||
| 5339d26259 | |||
| bc7a379947 | |||
| e9553d973a | |||
| 4f7fa18201 | |||
| 697a28aadf | |||
| 26f5750319 | |||
| d4fea9b20f | |||
| af280e5929 | |||
| fa8d508f36 | |||
| e7bf43ba0d | |||
| 793da897d1 | |||
| e1aa99e99d | |||
| 06b8272791 | |||
| 15703fb9f1 | |||
| 20d8c489d8 | |||
| 1d97a7818e | |||
| 7c64406d4d | |||
| f71b39b960 | |||
| 44dd58756b | |||
| 1dbbcaf9ba | |||
| d0e1b2921d | |||
| 72107631d7 | |||
| 888cfdc75f | |||
| 9886790075 | |||
| f3323c22fe | |||
| d8e011f721 | |||
| 3b63cb559e | |||
| f5189e3aa3 | |||
| 2c643bb988 | |||
| 54e6ea018d | |||
| c5fd53af5d | |||
| e275b0c4a6 | |||
| 9482a77a04 | |||
| fab105578b | |||
| b4dcfe9a06 | |||
| a31a20a5c6 | |||
| e6e7a85087 | |||
| 612c718696 | |||
| 180988c999 | |||
| 59380a9a64 | |||
| d2593fadb6 | |||
| 904aec69a0 | |||
| d851bb5c2a | |||
| 6fa61e69a7 | |||
| c0e65fbbf8 | |||
| cc0b367755 | |||
| 1dc3e88cca | |||
| d04e88b78f | |||
| d77209e8c5 | |||
| 3f27598353 | |||
| 334ced71a1 | |||
| f3a817e959 | |||
| 9712975595 | |||
| 4656db12e1 | |||
| a1ec9e389e | |||
| 3714b3d104 | |||
| 251358b364 | |||
| d2b6ce1555 | |||
| 3c22418e0a | |||
| 94269e4505 | |||
| 0f272f06ea | |||
| 08ca997c76 | |||
| 7d52baba45 | |||
| 72f73d595d | |||
| bf67536031 | |||
| 7a78ae6c10 | |||
| 2cdbfd1dca | |||
| d8924ce166 | |||
| 9ccbf4386c | |||
| fe9d8a7274 | |||
| 9ec5fa2208 | |||
| bffc50ae9d | |||
| a634c88ced | |||
| 2eb7c6d6cb | |||
| 8ae9044ef4 | |||
| eef776a0b4 | |||
| ef57748905 | |||
| 9903e25871 | |||
| 557a9b5674 | |||
| 0ff993ffe3 | |||
| a8a9876c1d | |||
| fd308528cd | |||
| b0f0c2af7e | |||
| ba5dde0347 | |||
| 22cab09558 | |||
| b02f319b78 | |||
| e03ef397fc | |||
| cddd2b461b | |||
| ec41f1ce1b | |||
| 7ce29a90af | |||
| 4dda9579b3 | |||
| 9b77f93fa0 | |||
| 4fc3adb35b | |||
| d3e3f0f109 | |||
| 8bc2ee1a4a | |||
| 93e51e74b4 | |||
| 148ef43015 | |||
| 8b5dfd0b06 | |||
| 1ebd063f4e | |||
| 40579dc143 | |||
| f26629e665 | |||
| ab96a452c6 | |||
| 50e1bbb583 | |||
| 09b6fbdaca | |||
| 8f9c8a5826 | |||
| 9c607c0b8c | |||
| 6dc178e448 | |||
| 187d2a26c0 | |||
| c6755d6f34 | |||
| bea2c40f53 | |||
| 363622d03e | |||
| 649c96c259 | |||
| ac32f752d9 | |||
| db79038541 | |||
| c390ae4f8d | |||
| e0a8cfdb6d | |||
| 52f03c6fc6 | |||
| 9f2c4a9277 | |||
| 575e209b93 | |||
| 0b363e6c4b | |||
| 5d8821ae05 | |||
| 512a916207 | |||
| 86a7cf0909 | |||
| f93f81a9f6 | |||
| bf6f0f03cf | |||
| 903182e4a3 | |||
| e7554e4b67 | |||
| 167420bd2e | |||
| f479887c77 | |||
| 5441c8fef4 | |||
| b104cede75 | |||
| c3c8223ca2 | |||
| d4773c32d6 | |||
| c8511e2808 | |||
| b23e6519fe | |||
| dc7c48a9d7 | |||
| a210233bfb | |||
| 9dcf28af7d | |||
| c030a94b68 | |||
| bd8b3a91e0 | |||
| 582eb6e6e2 | |||
| 9b127bcb70 | |||
| 171a764049 | |||
| 0da8ef69a8 | |||
| 815be4e003 | |||
| d897846115 | |||
| 5427855d50 | |||
| 6d7093e370 | |||
| d7c805a9f0 | |||
| 2032603ae9 | |||
| e7e91b8bae | |||
| 30cbfbdd61 | |||
| ab7694d82e | |||
| 61ee9a304e | |||
| f59bc1b152 | |||
| 3aa57f20af | |||
| b875ad18b5 | |||
| 2d69b0fbe7 | |||
| c8663da1a9 | |||
| 1de2735b16 | |||
| 4a5adabe95 | |||
| b566d8002a | |||
| 2e404995a0 | |||
| 654ad71526 | |||
| 2b50975563 | |||
| 81fb99ed66 | |||
| deb358f16c | |||
| 881098f39c |
+10
-2
@@ -9,10 +9,18 @@
|
||||
#
|
||||
# See gitignore(5) man page for more details
|
||||
#
|
||||
|
||||
# Swap files (vim)
|
||||
[._]*.s[a-v][a-z]
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
# Temporary files including undo
|
||||
*~
|
||||
#
|
||||
/config.php
|
||||
/lib/editor/tinymce/extra/tools/temp/
|
||||
*~
|
||||
*.swp
|
||||
/tags
|
||||
/TAGS
|
||||
/cscope.*
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-1
@@ -56,7 +56,7 @@ matrix:
|
||||
include:
|
||||
# Run grunt/npm install on highest version ('node' is an alias for the latest node.js version.)
|
||||
- php: 7.2
|
||||
env: DB=none TASK=GRUNT NVM_VERSION='8.9'
|
||||
env: DB=none TASK=GRUNT NVM_VERSION='lts/carbon'
|
||||
|
||||
exclude:
|
||||
# MySQL - it's just too slow.
|
||||
@@ -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
|
||||
|
||||
|
||||
+14
-2
@@ -137,6 +137,16 @@ module.exports = function(grunt) {
|
||||
}
|
||||
}
|
||||
},
|
||||
sass: {
|
||||
dist: {
|
||||
files: {
|
||||
"theme/boost/style/moodle.css": "theme/boost/scss/preset/default.scss"
|
||||
}
|
||||
},
|
||||
options: {
|
||||
includePaths: ["theme/boost/scss/"]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
options: {
|
||||
nospawn: true // We need not to spawn so config can be changed dynamically.
|
||||
@@ -214,7 +224,8 @@ module.exports = function(grunt) {
|
||||
'# Generated by "grunt ignorefiles"',
|
||||
'theme/bootstrapbase/style/',
|
||||
'theme/clean/style/custom.css',
|
||||
'theme/more/style/custom.css'
|
||||
'theme/more/style/custom.css',
|
||||
'theme/boost/style/moodle.css'
|
||||
].concat(thirdPartyPaths);
|
||||
grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
|
||||
};
|
||||
@@ -362,6 +373,7 @@ module.exports = function(grunt) {
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
grunt.loadNpmTasks('grunt-contrib-less');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-sass');
|
||||
grunt.loadNpmTasks('grunt-eslint');
|
||||
grunt.loadNpmTasks('grunt-stylelint');
|
||||
|
||||
@@ -374,7 +386,7 @@ module.exports = function(grunt) {
|
||||
grunt.registerTask('js', ['amd', 'yui']);
|
||||
|
||||
// Register CSS taks.
|
||||
grunt.registerTask('css', ['stylelint:scss', 'stylelint:less', 'less:bootstrapbase', 'stylelint:css']);
|
||||
grunt.registerTask('css', ['stylelint:scss', 'sass', 'stylelint:less', 'less:bootstrapbase', 'stylelint:css']);
|
||||
|
||||
// Register the startup task.
|
||||
grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup);
|
||||
|
||||
@@ -82,11 +82,6 @@ require_once($CFG->libdir.'/installlib.php');
|
||||
require_once($CFG->libdir.'/adminlib.php');
|
||||
require_once($CFG->libdir.'/componentlib.class.php');
|
||||
|
||||
// make sure no tables are installed yet
|
||||
if ($DB->get_tables() ) {
|
||||
cli_error(get_string('clitablesexist', 'install'));
|
||||
}
|
||||
|
||||
$CFG->early_install_lang = true;
|
||||
get_string_manager(true);
|
||||
|
||||
@@ -109,12 +104,17 @@ list($options, $unrecognized) = cli_get_params(
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// We show help text even if tables are installed.
|
||||
if ($options['help']) {
|
||||
echo $help;
|
||||
die;
|
||||
}
|
||||
|
||||
// Make sure no tables are installed yet.
|
||||
if ($DB->get_tables() ) {
|
||||
cli_error(get_string('clitablesexist', 'install'));
|
||||
}
|
||||
|
||||
if (!$options['agree-license']) {
|
||||
cli_error('You have to agree to the license. --help prints out the help'); // TODO: localize
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -2435,4 +2437,366 @@
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="3.6" requires="3.1">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="5.5.31" />
|
||||
<VENDOR name="mysql" version="5.6" />
|
||||
<VENDOR name="postgres" version="9.4" />
|
||||
<VENDOR name="mssql" version="10.0" />
|
||||
<VENDOR name="oracle" version="11.2" />
|
||||
</DATABASE>
|
||||
<PHP version="7.0.0" level="required">
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="pcreunicodewarning" />
|
||||
</FEEDBACK>
|
||||
</PCREUNICODE>
|
||||
<PHP_EXTENSIONS>
|
||||
<PHP_EXTENSION name="iconv" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="iconvrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="mbstring" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="mbstringrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="curl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="curlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="openssl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="opensslrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="tokenizer" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlrpc" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="xmlrpcrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="ctype" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ctyperequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zip" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ziprequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zlib" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="gd" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="gdrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="simplexml" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="simplexmlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="spl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="splrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="pcre" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="dom" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xml" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlreader" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="intl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="intlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="json" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="settingmemorylimit" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="file_uploads" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingfileuploads" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="opcache.enable" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="opcacherecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
</PHP_SETTINGS>
|
||||
<CUSTOM_CHECKS>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbstorageengine" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="quizattemptsupgradedmessage" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="slashargumentswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unsupporteddbtablerowformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unoconvwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="libcurlwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfileformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfilepertable" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddblargeprefix" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="ishttpswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="incompleteunicodesupport" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="sixtyfourbitswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<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>
|
||||
|
||||
@@ -40,8 +40,9 @@ if ($unregistration && \core\hub\registration::is_registered()) {
|
||||
if ($siteunregistrationform->is_cancelled()) {
|
||||
redirect(new moodle_url('/admin/registration/index.php'));
|
||||
} else if ($data = $siteunregistrationform->get_data()) {
|
||||
if (\core\hub\registration::unregister($data->unpublishalladvertisedcourses,
|
||||
$data->unpublishalluploadedcourses)) {
|
||||
\core\hub\registration::unregister($data->unpublishalladvertisedcourses,
|
||||
$data->unpublishalluploadedcourses);
|
||||
if (!\core\hub\registration::is_registered()) {
|
||||
redirect(new moodle_url('/admin/registration/index.php'));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1990,7 +1990,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
if (empty($CFG->docroot) or $environment_result->plugin) {
|
||||
$report = get_string($stringtouse, 'admin', $rec);
|
||||
} else {
|
||||
$report = $this->doc_link(join($linkparts, '/'), get_string($stringtouse, 'admin', $rec));
|
||||
$report = $this->doc_link(join($linkparts, '/'), get_string($stringtouse, 'admin', $rec), true);
|
||||
}
|
||||
// Enclose report text in div so feedback text will be displayed underneath it.
|
||||
$report = html_writer::div($report);
|
||||
|
||||
@@ -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.
|
||||
|
||||
+111
-84
@@ -28,113 +28,139 @@ admin_externalpage_setup('searchareas');
|
||||
|
||||
$areaid = optional_param('areaid', null, PARAM_ALPHAEXT);
|
||||
$action = optional_param('action', null, PARAM_ALPHA);
|
||||
$indexingenabled = \core_search\manager::is_indexing_enabled(); // This restricts many of the actions on this page.
|
||||
|
||||
// Get a search manager instance, which we'll need for display and to handle some actions.
|
||||
try {
|
||||
$searchmanager = \core_search\manager::instance();
|
||||
} catch (core_search\engine_exception $searchmanagererror) {
|
||||
// Continue, we return an error later depending on the requested action.
|
||||
// In action cases, we'll throw this exception below. In non-action cases, we produce a lang string error.
|
||||
}
|
||||
|
||||
// Handle all the actions.
|
||||
if ($action) {
|
||||
|
||||
// If dealing with an areaid, we need to check that the area exists.
|
||||
if ($areaid) {
|
||||
// We need to check that the area exists.
|
||||
$area = \core_search\manager::get_search_area($areaid);
|
||||
if ($area === false) {
|
||||
throw new moodle_exception('invalidrequest');
|
||||
}
|
||||
}
|
||||
|
||||
if ($action !== 'enable' && $action !== 'disable') {
|
||||
// All actions but enable/disable need the search engine to be ready.
|
||||
if (!empty($searchmanagererror)) {
|
||||
throw $searchmanagererror;
|
||||
}
|
||||
// All the indexing actions.
|
||||
if (in_array($action, ['delete', 'indexall', 'reindexall', 'deleteall'])) {
|
||||
|
||||
// Show confirm prompt for all these actions as they may be inadvisable, or may cause
|
||||
// an interruption in search functionality, on production systems.
|
||||
if (!optional_param('confirm', 0, PARAM_INT)) {
|
||||
// Display confirmation prompt.
|
||||
$a = null;
|
||||
if ($areaid) {
|
||||
$a = html_writer::tag('strong', $area->get_visible_name());
|
||||
// All of these actions require that indexing is enabled.
|
||||
if ($indexingenabled) {
|
||||
|
||||
// For all of these actions, we strictly need a manager instance.
|
||||
if (isset($searchmanagererror)) {
|
||||
throw $searchmanagererror;
|
||||
}
|
||||
|
||||
$actionparams = ['sesskey' => sesskey(), 'action' => $action, 'confirm' => 1];
|
||||
if ($areaid) {
|
||||
$actionparams['areaid'] = $areaid;
|
||||
}
|
||||
$actionurl = new moodle_url('/admin/searchareas.php', $actionparams);
|
||||
$cancelurl = new moodle_url('/admin/searchareas.php');
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->confirm(get_string('confirm_' . $action, 'search', $a),
|
||||
// Show confirm prompt for all these actions as they may be inadvisable, or may cause
|
||||
// an interruption in search functionality, on production systems.
|
||||
if (!optional_param('confirm', 0, PARAM_INT)) {
|
||||
// Display confirmation prompt.
|
||||
$a = null;
|
||||
if ($areaid) {
|
||||
$a = html_writer::tag('strong', $area->get_visible_name());
|
||||
}
|
||||
|
||||
$actionparams = ['sesskey' => sesskey(), 'action' => $action, 'confirm' => 1];
|
||||
if ($areaid) {
|
||||
$actionparams['areaid'] = $areaid;
|
||||
}
|
||||
$actionurl = new moodle_url('/admin/searchareas.php', $actionparams);
|
||||
$cancelurl = new moodle_url('/admin/searchareas.php');
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->confirm(get_string('confirm_' . $action, 'search', $a),
|
||||
new single_button($actionurl, get_string('continue'), 'post', true),
|
||||
new single_button($cancelurl, get_string('cancel'), 'get'));
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
} else {
|
||||
// Confirmed, so run the required action.
|
||||
require_sesskey();
|
||||
|
||||
switch ($action) {
|
||||
case 'delete':
|
||||
$searchmanager->delete_index($areaid);
|
||||
\core\notification::success(get_string('searchindexdeleted', 'admin'));
|
||||
break;
|
||||
case 'indexall':
|
||||
$searchmanager->index();
|
||||
\core\notification::success(get_string('searchindexupdated', 'admin'));
|
||||
break;
|
||||
case 'reindexall':
|
||||
$searchmanager->index(true);
|
||||
\core\notification::success(get_string('searchreindexed', 'admin'));
|
||||
break;
|
||||
case 'deleteall':
|
||||
$searchmanager->delete_index();
|
||||
\core\notification::success(get_string('searchalldeleted', 'admin'));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Redirect back to the main page after taking action.
|
||||
redirect(new moodle_url('/admin/searchareas.php'));
|
||||
}
|
||||
}
|
||||
} else if (in_array($action, ['enable', 'disable'])) {
|
||||
// Toggling search areas requires no confirmation.
|
||||
require_sesskey();
|
||||
|
||||
switch ($action) {
|
||||
case 'enable':
|
||||
$area->set_enabled(true);
|
||||
\core\notification::success(get_string('searchareaenabled', 'admin'));
|
||||
break;
|
||||
case 'disable':
|
||||
$area->set_enabled(false);
|
||||
core\notification::success(get_string('searchareadisabled', 'admin'));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
redirect(new moodle_url('/admin/searchareas.php'));
|
||||
} else {
|
||||
// Invalid action.
|
||||
throw new moodle_exception('invalidaction');
|
||||
}
|
||||
}
|
||||
|
||||
// We are now taking an actual action, so require sesskey.
|
||||
require_sesskey();
|
||||
|
||||
switch ($action) {
|
||||
case 'enable':
|
||||
$area->set_enabled(true);
|
||||
\core\notification::add(get_string('searchareaenabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
|
||||
break;
|
||||
case 'disable':
|
||||
$area->set_enabled(false);
|
||||
\core\notification::add(get_string('searchareadisabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
|
||||
break;
|
||||
case 'delete':
|
||||
$search = \core_search\manager::instance();
|
||||
$search->delete_index($areaid);
|
||||
\core\notification::add(get_string('searchindexdeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
|
||||
break;
|
||||
case 'indexall':
|
||||
$searchmanager->index();
|
||||
\core\notification::add(get_string('searchindexupdated', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
|
||||
break;
|
||||
case 'reindexall':
|
||||
$searchmanager->index(true);
|
||||
\core\notification::add(get_string('searchreindexed', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
|
||||
break;
|
||||
case 'deleteall':
|
||||
$searchmanager->delete_index();
|
||||
\core\notification::add(get_string('searchalldeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
|
||||
break;
|
||||
default:
|
||||
throw new moodle_exception('invalidaction');
|
||||
break;
|
||||
}
|
||||
|
||||
// Redirect back to the main page after taking action.
|
||||
redirect(new moodle_url('/admin/searchareas.php'));
|
||||
// Display.
|
||||
if (isset($searchmanager) && $indexingenabled) {
|
||||
\core\notification::info(get_string('indexinginfo', 'admin'));
|
||||
} else if (isset($searchmanager)) {
|
||||
$params = (object) [
|
||||
'url' => (new moodle_url("/admin/settings.php?section=manageglobalsearch#admin-searchindexwhendisabled"))->out(false)
|
||||
];
|
||||
\core\notification::error(get_string('indexwhendisabledfullnotice', 'search', $params));
|
||||
} else {
|
||||
// In non-action cases, init errors are translated and displayed to the user as error notifications.
|
||||
$errorstr = get_string($searchmanagererror->errorcode, $searchmanagererror->module, $searchmanagererror->a);
|
||||
\core\notification::error($errorstr);
|
||||
}
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
$searchareas = \core_search\manager::get_search_areas_list();
|
||||
if (empty($searchmanagererror)) {
|
||||
$areasconfig = $searchmanager->get_areas_config($searchareas);
|
||||
} else {
|
||||
$areasconfig = false;
|
||||
}
|
||||
|
||||
if (!empty($searchmanagererror)) {
|
||||
$errorstr = get_string($searchmanagererror->errorcode, $searchmanagererror->module, $searchmanagererror->a);
|
||||
echo $OUTPUT->notification($errorstr, \core\output\notification::NOTIFY_ERROR);
|
||||
} else {
|
||||
echo $OUTPUT->notification(get_string('indexinginfo', 'admin'), \core\output\notification::NOTIFY_INFO);
|
||||
}
|
||||
|
||||
$table = new html_table();
|
||||
$table->id = 'core-search-areas';
|
||||
$table->head = [
|
||||
get_string('searcharea', 'search'),
|
||||
get_string('enable'),
|
||||
get_string('newestdocindexed', 'admin'),
|
||||
get_string('searchlastrun', 'admin'),
|
||||
get_string('searchindexactions', 'admin')
|
||||
];
|
||||
|
||||
$table->head = array(get_string('searcharea', 'search'), get_string('enable'), get_string('newestdocindexed', 'admin'),
|
||||
get_string('searchlastrun', 'admin'), get_string('searchindexactions', 'admin'));
|
||||
|
||||
$searchareas = \core_search\manager::get_search_areas_list();
|
||||
$areasconfig = isset($searchmanager) ? $searchmanager->get_areas_config($searchareas) : false;
|
||||
foreach ($searchareas as $area) {
|
||||
$areaid = $area->get_area_id();
|
||||
$columns = array(new html_table_cell($area->get_visible_name()));
|
||||
@@ -144,7 +170,7 @@ foreach ($searchareas as $area) {
|
||||
new pix_icon('t/hide', get_string('disable'), 'moodle', array('title' => '', 'class' => 'iconsmall')),
|
||||
null, array('title' => get_string('disable')));
|
||||
|
||||
if ($areasconfig) {
|
||||
if ($areasconfig && $indexingenabled) {
|
||||
$columns[] = $areasconfig[$areaid]->lastindexrun;
|
||||
|
||||
if ($areasconfig[$areaid]->indexingstart) {
|
||||
@@ -173,7 +199,11 @@ foreach ($searchareas as $area) {
|
||||
$columns[] = html_writer::alist($actions, ['class' => 'unstyled list-unstyled']);
|
||||
|
||||
} else {
|
||||
$blankrow = new html_table_cell(get_string('searchnotavailable', 'admin'));
|
||||
if (!$areasconfig) {
|
||||
$blankrow = new html_table_cell(get_string('searchnotavailable', 'admin'));
|
||||
} else {
|
||||
$blankrow = new html_table_cell(get_string('indexwhendisabledshortnotice', 'search'));
|
||||
}
|
||||
$blankrow->colspan = 3;
|
||||
$columns[] = $blankrow;
|
||||
}
|
||||
@@ -192,10 +222,7 @@ foreach ($searchareas as $area) {
|
||||
}
|
||||
|
||||
// Cross-search area tasks.
|
||||
$options = array();
|
||||
if (!empty($searchmanagererror)) {
|
||||
$options['disabled'] = true;
|
||||
}
|
||||
$options = (isset($searchmanager) && $indexingenabled) ? [] : ['disabled' => true];
|
||||
echo $OUTPUT->box_start('search-areas-actions');
|
||||
echo $OUTPUT->single_button(admin_searcharea_action_url('indexall'), get_string('searchupdateindex', 'admin'), 'get', $options);
|
||||
echo $OUTPUT->single_button(admin_searcharea_action_url('reindexall'), get_string('searchreindexindex', 'admin'), 'get', $options);
|
||||
@@ -204,7 +231,7 @@ echo $OUTPUT->box_end();
|
||||
|
||||
echo html_writer::table($table);
|
||||
|
||||
if (empty($searchmanagererror)) {
|
||||
if (isset($searchmanager)) {
|
||||
// Show information about queued index requests for specific contexts.
|
||||
$searchrenderer = $PAGE->get_renderer('core_search');
|
||||
echo $searchrenderer->render_index_requests_info($searchmanager->get_index_requests_info());
|
||||
|
||||
+2
-1
@@ -38,7 +38,8 @@ if (!($settingspage->check_access())) {
|
||||
$statusmsg = '';
|
||||
$errormsg = '';
|
||||
|
||||
if ($data = data_submitted() and confirm_sesskey()) {
|
||||
// Form is submitted with changed settings. Do not want to execute when modifying a block.
|
||||
if ($data = data_submitted() and confirm_sesskey() and isset($data->action) and $data->action == 'save-settings') {
|
||||
|
||||
$count = admin_write_settings($data);
|
||||
// Regardless of whether any setting change was written (a positive count), check validation errors for those that didn't.
|
||||
|
||||
@@ -7,7 +7,8 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
|
||||
|
||||
// "systempaths" settingpage
|
||||
$temp = new admin_settingpage('systempaths', new lang_string('systempaths','admin'));
|
||||
|
||||
$temp->add(new admin_setting_configexecutable('pathtophp', new lang_string('pathtophp', 'admin'),
|
||||
new lang_string('configpathtophp', 'admin'), ''));
|
||||
$temp->add(new admin_setting_configexecutable('pathtodu', new lang_string('pathtodu', 'admin'), new lang_string('configpathtodu', 'admin'), ''));
|
||||
$temp->add(new admin_setting_configexecutable('aspellpath', new lang_string('aspellpath', 'admin'), new lang_string('edhelpaspellpath'), ''));
|
||||
$temp->add(new admin_setting_configexecutable('pathtodot', new lang_string('pathtodot', 'admin'), new lang_string('pathtodot_help', 'admin'), ''));
|
||||
|
||||
@@ -15,10 +15,28 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
|
||||
|
||||
$optionalsubsystems->add(new admin_setting_configcheckbox('messaging', new lang_string('messaging', 'admin'), new lang_string('configmessaging','admin'), 1));
|
||||
|
||||
$optionalsubsystems->add(new admin_setting_configcheckbox('messaginghidereadnotifications', new lang_string('messaginghidereadnotifications', 'admin'), new lang_string('configmessaginghidereadnotifications','admin'), 0));
|
||||
|
||||
$options = array(DAYSECS=>new lang_string('secondstotime86400'), WEEKSECS=>new lang_string('secondstotime604800'), 2620800=>new lang_string('nummonths', 'moodle', 1), 15724800=>new lang_string('nummonths', 'moodle', 6),0=>new lang_string('never'));
|
||||
$optionalsubsystems->add(new admin_setting_configselect('messagingdeletereadnotificationsdelay', new lang_string('messagingdeletereadnotificationsdelay', 'admin'), new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'), 604800, $options));
|
||||
$options = array(
|
||||
DAYSECS => new lang_string('secondstotime86400'),
|
||||
WEEKSECS => new lang_string('secondstotime604800'),
|
||||
2620800 => new lang_string('nummonths', 'moodle', 1),
|
||||
7862400 => new lang_string('nummonths', 'moodle', 3),
|
||||
15724800 => new lang_string('nummonths', 'moodle', 6),
|
||||
0 => new lang_string('never')
|
||||
);
|
||||
$optionalsubsystems->add(new admin_setting_configselect(
|
||||
'messagingdeletereadnotificationsdelay',
|
||||
new lang_string('messagingdeletereadnotificationsdelay', 'admin'),
|
||||
new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'),
|
||||
604800,
|
||||
$options)
|
||||
);
|
||||
$optionalsubsystems->add(new admin_setting_configselect(
|
||||
'messagingdeleteallnotificationsdelay',
|
||||
new lang_string('messagingdeleteallnotificationsdelay', 'admin'),
|
||||
new lang_string('configmessagingdeleteallnotificationsdelay', 'admin'),
|
||||
2620800,
|
||||
$options)
|
||||
);
|
||||
|
||||
$optionalsubsystems->add(new admin_setting_configcheckbox('messagingallowemailoverride', new lang_string('messagingallowemailoverride', 'admin'), new lang_string('configmessagingallowemailoverride','admin'), 0));
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<div class="settingsform clearfix">
|
||||
{{#params}}
|
||||
<input type="hidden" name="{{name}}" value="{{value}}">
|
||||
<input type="hidden" name="action" value="save-settings">
|
||||
{{/params}}
|
||||
<input type="hidden" name="sesskey" value="{{sesskey}}">
|
||||
<input type="hidden" name="return" value="{{return}}">
|
||||
|
||||
@@ -153,6 +153,12 @@ class invalid_analysables implements \renderable, \templatable {
|
||||
$data->analysables[] = $obj;
|
||||
}
|
||||
|
||||
if (empty($data->analysables)) {
|
||||
$data->noanalysables = [
|
||||
'message' => get_string('noinvalidanalysables', 'tool_analytics'),
|
||||
'announce' => true,
|
||||
];
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ class models_list implements \renderable, \templatable {
|
||||
}
|
||||
|
||||
// Clear model.
|
||||
if (!empty($predictioncontexts)) {
|
||||
if (!empty($predictioncontexts) || $model->is_trained()) {
|
||||
$actionid = 'clear-' . $model->get_id();
|
||||
$PAGE->requires->js_call_amd('tool_analytics/model', 'confirmAction', [$actionid, 'clear']);
|
||||
$urlparams['action'] = 'clear';
|
||||
|
||||
@@ -62,7 +62,7 @@ $string['indicators'] = 'Indicators';
|
||||
$string['info'] = 'Info';
|
||||
$string['insights'] = 'Insights';
|
||||
$string['invalidanalysables'] = 'Invalid site elements';
|
||||
$string['invalidanalysablesinfo'] = 'This pages lists this site analysable elements that can not be used by this prediction model. The listed elements can not be used neither to train the prediction model nor the prediction model can get predictions for them.';
|
||||
$string['invalidanalysablesinfo'] = 'This page lists analysable elements that can\'t be used by this prediction model. The listed elements can\'t be used either to train the prediction model nor can the prediction model obtain predictions for them.';
|
||||
$string['invalidanalysablestable'] = 'Invalid site analysable elements table';
|
||||
$string['invalidprediction'] = 'Invalid to get predictions';
|
||||
$string['invalidtraining'] = 'Invalid to train the model';
|
||||
@@ -75,6 +75,7 @@ $string['nextpage'] = 'Next page';
|
||||
$string['nodatatoevaluate'] = 'There is no data to evaluate the model';
|
||||
$string['nodatatopredict'] = 'No new elements to get predictions for';
|
||||
$string['nodatatotrain'] = 'There is no new data that can be used for training';
|
||||
$string['noinvalidanalysables'] = 'This site does not contain any invalid analysable element.';
|
||||
$string['notdefined'] = 'Not yet defined';
|
||||
$string['pluginname'] = 'Analytic models';
|
||||
$string['predictionresults'] = 'Prediction results';
|
||||
|
||||
@@ -48,31 +48,38 @@
|
||||
<div class="box">
|
||||
<h3>{{#str}}modelinvalidanalysables, tool_analytics, {{modelname}}{{/str}}</h3>
|
||||
<div>{{#str}}invalidanalysablesinfo, tool_analytics{{/str}}</div>
|
||||
<div class="m-t-2 m-b-1">
|
||||
<span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
|
||||
<span>{{#next}}{{> core/single_button}}{{/next}}</span>
|
||||
</div>
|
||||
<table class="generaltable fullwidth">
|
||||
<caption class="accesshide">{{#str}}invalidanalysablestable, tool_analytics{{/str}}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{#str}}name{{/str}}</th>
|
||||
<th scope="col">{{#str}}invalidtraining, tool_analytics{{/str}}</th>
|
||||
<th scope="col">{{#str}}invalidprediction, tool_analytics{{/str}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#analysables}}
|
||||
<tr>
|
||||
<td>{{{url}}}</td>
|
||||
<td>{{validtraining}}</td>
|
||||
<td>{{validprediction}}</td>
|
||||
</tr>
|
||||
{{/analysables}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="m-t-1 m-b-2">
|
||||
<span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
|
||||
<span>{{#next}}{{> core/single_button}}{{/next}}</span>
|
||||
</div>
|
||||
{{#noanalysables}}
|
||||
<div class="m-t-2 m-b-1">
|
||||
{{> core/notification_info}}
|
||||
</div>
|
||||
{{/noanalysables}}
|
||||
{{^noanalysables}}
|
||||
<div class="m-t-2 m-b-1">
|
||||
<span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
|
||||
<span>{{#next}}{{> core/single_button}}{{/next}}</span>
|
||||
</div>
|
||||
<table class="generaltable fullwidth">
|
||||
<caption class="accesshide">{{#str}}invalidanalysablestable, tool_analytics{{/str}}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{#str}}name{{/str}}</th>
|
||||
<th scope="col">{{#str}}invalidtraining, tool_analytics{{/str}}</th>
|
||||
<th scope="col">{{#str}}invalidprediction, tool_analytics{{/str}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#analysables}}
|
||||
<tr>
|
||||
<td>{{{url}}}</td>
|
||||
<td>{{validtraining}}</td>
|
||||
<td>{{validprediction}}</td>
|
||||
</tr>
|
||||
{{/analysables}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="m-t-1 m-b-2">
|
||||
<span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
|
||||
<span>{{#next}}{{> core/single_button}}{{/next}}</span>
|
||||
</div>
|
||||
{{/noanalysables}}
|
||||
</div>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -218,7 +218,7 @@ if ($options['install']) {
|
||||
// Run behat command to get steps in feature files.
|
||||
$featurestepscmd = behat_command::get_behat_command(true);
|
||||
$featurestepscmd .= ' --config ' . behat_config_manager::get_behat_cli_config_filepath();
|
||||
$featurestepscmd .= ' --dry-run --format=moodle_step_count';
|
||||
$featurestepscmd .= ' --dry-run --format=moodle_stepcount';
|
||||
$processes = cli_execute_parallel(array($featurestepscmd), __DIR__ . "/../../../../");
|
||||
$status = print_update_step_output(array_pop($processes), $behatstepfile);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@ $string['selectusers'] = 'Select users to assign role';
|
||||
$string['taskname'] = 'Sync cohort role assignments';
|
||||
$string['thisuserroles'] = 'Roles assigned relative to this user';
|
||||
$string['privacy:metadata:tool_cohortroles'] = 'The Cohort roles management plugin stores user cohort role mappings.';
|
||||
$string['privacy:metadata:tool_cohortroles:id'] = 'The ID of the cohort role mapping record.';
|
||||
$string['privacy:metadata:tool_cohortroles:cohortid'] = 'The ID of the cohort.';
|
||||
$string['privacy:metadata:tool_cohortroles:roleid'] = 'The ID of the role.';
|
||||
$string['privacy:metadata:tool_cohortroles:userid'] = 'The ID of the user.';
|
||||
$string['privacy:metadata:tool_cohortroles:timecreated'] = 'The date/time of when the cohort role mapping was created.';
|
||||
$string['privacy:metadata:tool_cohortroles:timemodified'] = 'The date/time of when the cohort role mapping was modified.';
|
||||
$string['privacy:metadata:tool_cohortroles:usermodified'] = 'The ID of the user who last modified the cohort role mapping.';
|
||||
$string['privacy:metadata:tool_cohortroles:id'] = 'The ID of the cohort role mapping record';
|
||||
$string['privacy:metadata:tool_cohortroles:cohortid'] = 'The ID of the cohort';
|
||||
$string['privacy:metadata:tool_cohortroles:roleid'] = 'The ID of the role';
|
||||
$string['privacy:metadata:tool_cohortroles:userid'] = 'The ID of the user';
|
||||
$string['privacy:metadata:tool_cohortroles:timecreated'] = 'The time when the cohort role mapping was created';
|
||||
$string['privacy:metadata:tool_cohortroles:timemodified'] = 'The time when the cohort role mapping was modified';
|
||||
$string['privacy:metadata:tool_cohortroles:usermodified'] = 'The ID of the user who last modified the cohort role mapping';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -38,5 +38,8 @@ function xmldb_tool_customlang_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v3.4.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
// Automatically generated Moodle v3.5.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ $string['checkin'] = 'Save strings to language pack';
|
||||
$string['checkout'] = 'Open language pack for editing';
|
||||
$string['checkoutdone'] = 'Language pack loaded';
|
||||
$string['checkoutinprogress'] = 'Loading language pack';
|
||||
$string['confirmcheckin'] = 'You are about to save modifications to your local language pack. This will export the customised strings from the translator into you Moodle data directory and Moodle will start using the modified strings. Press \'Continue\' to proceed with saving.';
|
||||
$string['confirmcheckin'] = 'You are about to save modifications to your local language pack. This will export the customised strings from the translator into your site data directory and your site will start using the modified strings. Press \'Continue\' to proceed with saving.';
|
||||
$string['customlang:edit'] = 'Edit local translation';
|
||||
$string['customlang:view'] = 'View local translation';
|
||||
$string['filter'] = 'Filter strings';
|
||||
|
||||
@@ -134,11 +134,13 @@ class tool_customlang_renderer extends plugin_renderer_base {
|
||||
$output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'translatorsubmitted', 'value'=>1));
|
||||
$output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey()));
|
||||
$output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'p', 'value'=>$translator->currentpage));
|
||||
$save1 = html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'savecontinue', 'value'=>get_string('savecontinue', 'tool_customlang')));
|
||||
$save2 = html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'savecheckin', 'value'=>get_string('savecheckin', 'tool_customlang')));
|
||||
$output .= html_writer::tag('fieldset', $save1.$save2, array('class'=>'buttonsbar'));
|
||||
$save1 = html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'savecontinue',
|
||||
'value' => get_string('savecontinue', 'tool_customlang'), 'class' => 'btn btn-secondary'));
|
||||
$save2 = html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'savecheckin',
|
||||
'value' => get_string('savecheckin', 'tool_customlang'), 'class' => 'btn btn-secondary'));
|
||||
$output .= html_writer::tag('fieldset', $save1 . ' ' . $save2, array('class' => 'buttonsbar'));
|
||||
$output .= html_writer::table($table);
|
||||
$output .= html_writer::tag('fieldset', $save1.$save2, array('class'=>'buttonsbar'));
|
||||
$output .= html_writer::tag('fieldset', $save1 . ' ' . $save2, array('class' => 'buttonsbar'));
|
||||
$output .= html_writer::end_tag('div');
|
||||
$output .= html_writer::end_tag('form');
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f){var g={DELETE:'[data-action="deletecategory"]'},h=function(){this.registerEvents()};return h.prototype.registerEvents=function(){a(g.DELETE).click(function(g){g.preventDefault();var h=a(this).data("id"),i=a(this).data("name"),j=[{key:"deletecategory",component:"tool_dataprivacy",param:i},{key:"deletecategorytext",component:"tool_dataprivacy",param:i}];d.get_strings(j).then(function(d){var g=d[0],i=d[1];return e.create({title:g,body:i,type:e.types.SAVE_CANCEL}).then(function(d){return d.setSaveButtonText(g),d.getRoot().on(f.save,function(){var d={methodname:"tool_dataprivacy_delete_category",args:{id:h}};b.call([d])[0].done(function(b){b.result?a('tr[data-categoryid="'+h+'"]').remove():c.addNotification({message:b.warnings[0].message,type:"error"})}).fail(c.exception)}),d.getRoot().on(f.hidden,function(){d.destroy()}),d})}).done(function(a){a.show()}).fail(c.exception)})},{init:function(){return new h}}});
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f){var g={DELETE:'[data-action="deletecategory"]'},h=function(){this.registerEvents()};return h.prototype.registerEvents=function(){a(g.DELETE).click(function(g){g.preventDefault();var h=a(this).data("id"),i=a(this).data("name"),j=[{key:"deletecategory",component:"tool_dataprivacy"},{key:"deletecategorytext",component:"tool_dataprivacy",param:i},{key:"delete"}];d.get_strings(j).then(function(d){var g=d[0],i=d[1],j=d[2];return e.create({title:g,body:i,type:e.types.SAVE_CANCEL}).then(function(d){return d.setSaveButtonText(j),d.getRoot().on(f.save,function(){var d={methodname:"tool_dataprivacy_delete_category",args:{id:h}};b.call([d])[0].done(function(b){b.result?a('tr[data-categoryid="'+h+'"]').remove():c.addNotification({message:b.warnings[0].message,type:"error"})}).fail(c.exception)}),d.getRoot().on(f.hidden,function(){d.destroy()}),d})}).done(function(a){a.show()}).fail(c.exception)})},{init:function(){return new h}}});
|
||||
@@ -1 +1 @@
|
||||
define(["jquery","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","tool_dataprivacy/events"],function(a,b,c,d,e,f){var g=!1,h={APPROVE_BUTTON:'[data-action="approve"]',DENY_BUTTON:'[data-action="deny"]'},i=function(a){d.call(this,a),this.getFooter().find(h.APPROVE_BUTTON).length||b.exception({message:"No approve button found"}),this.getFooter().find(h.DENY_BUTTON).length||b.exception({message:"No deny button found"})};return i.TYPE="tool_dataprivacy-data_request",i.prototype=Object.create(d.prototype),i.prototype.constructor=i,i.prototype.registerEventListeners=function(){d.prototype.registerEventListeners.call(this),this.getModal().on(c.events.activate,h.APPROVE_BUTTON,function(b,c){var d=a.Event(f.approve);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this)),this.getModal().on(c.events.activate,h.DENY_BUTTON,function(b,c){var d=a.Event(f.deny);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this))},g||(e.register(i.TYPE,i,"tool_dataprivacy/data_request_modal"),g=!0),i});
|
||||
define(["jquery","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","tool_dataprivacy/events"],function(a,b,c,d,e,f){var g=!1,h={APPROVE_BUTTON:'[data-action="approve"]',DENY_BUTTON:'[data-action="deny"]',COMPLETE_BUTTON:'[data-action="complete"]'},i=function(a){d.call(this,a)};return i.TYPE="tool_dataprivacy-data_request",i.prototype=Object.create(d.prototype),i.prototype.constructor=i,i.prototype.registerEventListeners=function(){d.prototype.registerEventListeners.call(this),this.getModal().on(c.events.activate,h.APPROVE_BUTTON,function(b,c){var d=a.Event(f.approve);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this)),this.getModal().on(c.events.activate,h.DENY_BUTTON,function(b,c){var d=a.Event(f.deny);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this)),this.getModal().on(c.events.activate,h.COMPLETE_BUTTON,function(b,c){var d=a.Event(f.complete);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this))},g||(e.register(i.TYPE,i,"tool_dataprivacy/data_request_modal"),g=!0),i});
|
||||
@@ -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"}});
|
||||
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"],function(a,b,c,d,e,f){var g={DELETE:'[data-action="deletepurpose"]'},h=function(){this.registerEvents()};return h.prototype.registerEvents=function(){a(g.DELETE).click(function(g){g.preventDefault();var h=a(this).data("id"),i=a(this).data("name"),j=[{key:"deletepurpose",component:"tool_dataprivacy",param:i},{key:"deletepurposetext",component:"tool_dataprivacy",param:i}];d.get_strings(j).then(function(d){var g=d[0],i=d[1];return e.create({title:g,body:i,type:e.types.SAVE_CANCEL}).then(function(d){return d.setSaveButtonText(g),d.getRoot().on(f.save,function(){var d={methodname:"tool_dataprivacy_delete_purpose",args:{id:h}};b.call([d])[0].done(function(b){b.result?a('tr[data-purposeid="'+h+'"]').remove():c.addNotification({message:b.warnings[0].message,type:"error"})}).fail(c.exception)}),d.getRoot().on(f.hidden,function(){d.destroy()}),d})}).done(function(a){a.show()}).fail(c.exception)})},{init:function(){return new h}}});
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f){var g={DELETE:'[data-action="deletepurpose"]'},h=function(){this.registerEvents()};return h.prototype.registerEvents=function(){a(g.DELETE).click(function(g){g.preventDefault();var h=a(this).data("id"),i=a(this).data("name"),j=[{key:"deletepurpose",component:"tool_dataprivacy"},{key:"deletepurposetext",component:"tool_dataprivacy",param:i},{key:"delete"}];d.get_strings(j).then(function(d){var g=d[0],i=d[1],j=d[2];return e.create({title:g,body:i,type:e.types.SAVE_CANCEL}).then(function(d){return d.setSaveButtonText(j),d.getRoot().on(f.save,function(){var d={methodname:"tool_dataprivacy_delete_purpose",args:{id:h}};b.call([d])[0].done(function(b){b.result?a('tr[data-purposeid="'+h+'"]').remove():c.addNotification({message:b.warnings[0].message,type:"error"})}).fail(c.exception)}),d.getRoot().on(f.hidden,function(){d.destroy()}),d})}).done(function(a){a.show()}).fail(c.exception)})},{init:function(){return new h}}});
|
||||
@@ -0,0 +1 @@
|
||||
define(["jquery","core/form-autocomplete","core/str","core/notification"],function(a,b,c,d){var e={REQUEST_FILTERS:"#request-filters"},f=function(){var f=[{key:"filter",component:"moodle"},{key:"nofiltersapplied",component:"moodle"}];c.get_strings(f).then(function(a){var c=a[0],d=a[1];return b.enhance(e.REQUEST_FILTERS,!1,"",c,!1,!0,d,!0)}).fail(d.exception);var g=a(e.REQUEST_FILTERS).val();a(e.REQUEST_FILTERS).on("change",function(){var b=a(this).val();g.join(",")!==b.join(",")&&(0===b.length&&a("#filters-cleared").val(1),a(this.form).submit())})};return{init:function(){f()}}});
|
||||
+1
-1
@@ -1 +1 @@
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates","tool_dataprivacy/data_request_modal","tool_dataprivacy/events"],function(a,b,c,d,e,f,g,h,i){function j(a,g){var h=[],j="";switch(a){case i.approve:h=[{key:"approverequest",component:"tool_dataprivacy"},{key:"confirmapproval",component:"tool_dataprivacy"}],j="tool_dataprivacy_approve_data_request";break;case i.deny:h=[{key:"denyrequest",component:"tool_dataprivacy"},{key:"confirmdenial",component:"tool_dataprivacy"}],j="tool_dataprivacy_deny_data_request"}var k="";d.get_strings(h).then(function(a){k=a[0];var b=a[1];return e.create({title:k,body:b,type:e.types.SAVE_CANCEL})}).then(function(a){return a.setSaveButtonText(k),a.getRoot().on(f.save,function(){var a={requestid:g},d={methodname:j,args:a};b.call([d])[0].done(function(a){a.result?window.location.reload():c.addNotification({message:a.warnings[0].message,type:"error"})}).fail(c.exception)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a}).done(function(a){a.show()}).fail(c.exception)}var k={APPROVE_REQUEST:'[data-action="approve"]',DENY_REQUEST:'[data-action="deny"]',VIEW_REQUEST:'[data-action="view"]'},l=function(){this.registerEvents()};return l.prototype.registerEvents=function(){a(k.VIEW_REQUEST).click(function(d){d.preventDefault();var k=a(this).data("requestid"),l={requestid:k},m={methodname:"tool_dataprivacy_get_data_request",args:l},n=b.call([m]),o="",p=e.types.DEFAULT;a.when(n[0]).then(function(a){return a.result?(2==a.result.status&&(p=h.TYPE),o=a.result.typename,g.render("tool_dataprivacy/request_details",a.result)):(c.addNotification({message:a.warnings[0].message,type:"error"}),!1)}).then(function(a){return e.create({title:o,body:a,type:p,large:!0}).then(function(a){return a.getRoot().on(i.approve,function(){j(i.approve,k)}),a.getRoot().on(i.deny,function(){j(i.deny,k)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a})}).done(function(a){a.show()}).fail(c.exception)}),a(k.APPROVE_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.approve,c)}),a(k.DENY_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.deny,c)})},l});
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates","tool_dataprivacy/data_request_modal","tool_dataprivacy/events"],function(a,b,c,d,e,f,g,h,i){function j(a){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});
|
||||
@@ -58,25 +58,28 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents) {
|
||||
var stringkeys = [
|
||||
{
|
||||
key: 'deletecategory',
|
||||
component: 'tool_dataprivacy',
|
||||
param: categoryname
|
||||
component: 'tool_dataprivacy'
|
||||
},
|
||||
{
|
||||
key: 'deletecategorytext',
|
||||
component: 'tool_dataprivacy',
|
||||
param: categoryname
|
||||
},
|
||||
{
|
||||
key: 'delete'
|
||||
}
|
||||
];
|
||||
|
||||
Str.get_strings(stringkeys).then(function(langStrings) {
|
||||
var title = langStrings[0];
|
||||
var confirmMessage = langStrings[1];
|
||||
var buttonText = langStrings[2];
|
||||
return ModalFactory.create({
|
||||
title: title,
|
||||
body: confirmMessage,
|
||||
type: ModalFactory.types.SAVE_CANCEL
|
||||
}).then(function(modal) {
|
||||
modal.setSaveButtonText(title);
|
||||
modal.setSaveButtonText(buttonText);
|
||||
|
||||
// Handle save event.
|
||||
modal.getRoot().on(ModalEvents.save, function() {
|
||||
|
||||
@@ -234,6 +234,7 @@ define(['jquery', 'core/str', 'core/ajax', 'core/notification', 'core/templates'
|
||||
},
|
||||
fail: Notification.exception
|
||||
}]);
|
||||
return;
|
||||
}).catch(Notification.exception);
|
||||
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/m
|
||||
var SELECTORS = {
|
||||
APPROVE_BUTTON: '[data-action="approve"]',
|
||||
DENY_BUTTON: '[data-action="deny"]',
|
||||
COMPLETE_BUTTON: '[data-action="complete"]'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -38,14 +39,6 @@ define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/m
|
||||
*/
|
||||
var ModalDataRequest = function(root) {
|
||||
Modal.call(this, root);
|
||||
|
||||
if (!this.getFooter().find(SELECTORS.APPROVE_BUTTON).length) {
|
||||
Notification.exception({message: 'No approve button found'});
|
||||
}
|
||||
|
||||
if (!this.getFooter().find(SELECTORS.DENY_BUTTON).length) {
|
||||
Notification.exception({message: 'No deny button found'});
|
||||
}
|
||||
};
|
||||
|
||||
ModalDataRequest.TYPE = 'tool_dataprivacy-data_request';
|
||||
@@ -80,6 +73,16 @@ define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/m
|
||||
data.originalEvent.preventDefault();
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.getModal().on(CustomEvents.events.activate, SELECTORS.COMPLETE_BUTTON, function(e, data) {
|
||||
var completeEvent = $.Event(DataPrivacyEvents.complete);
|
||||
this.getRoot().trigger(completeEvent, this);
|
||||
|
||||
if (!completeEvent.isDefaultPrevented()) {
|
||||
this.hide();
|
||||
data.originalEvent.preventDefault();
|
||||
}
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
// Automatically register with the modal registry the first time this module is imported so that you can create modals
|
||||
|
||||
@@ -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,6 +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'
|
||||
};
|
||||
});
|
||||
|
||||
@@ -34,7 +34,6 @@ define(['jquery', 'core/url', 'core/str'], function($, url, str) {
|
||||
*
|
||||
* @param {object} targetnode The node that we want to expand / collapse
|
||||
* @param {object} thisnode The node that was clicked.
|
||||
* @return {null}
|
||||
*/
|
||||
expandCollapse: function(targetnode, thisnode) {
|
||||
if (targetnode.hasClass('hide')) {
|
||||
@@ -58,7 +57,6 @@ define(['jquery', 'core/url', 'core/str'], function($, url, str) {
|
||||
* Expand or collapse all nodes on this page.
|
||||
*
|
||||
* @param {string} nextstate The next state to change to.
|
||||
* @return {null}
|
||||
*/
|
||||
expandCollapseAll: function(nextstate) {
|
||||
var currentstate = (nextstate == 'visible') ? 'hide' : 'visible';
|
||||
@@ -75,6 +73,7 @@ define(['jquery', 'core/url', 'core/str'], function($, url, str) {
|
||||
|
||||
str.get_string(currentstate, 'tool_dataprivacy').then(function(langString) {
|
||||
$('.tool_dataprivacy-expand-all').html(langString);
|
||||
return;
|
||||
}).catch(Notification.exception);
|
||||
|
||||
$(':header i.fa').each(function() {
|
||||
|
||||
@@ -58,25 +58,28 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents) {
|
||||
var stringkeys = [
|
||||
{
|
||||
key: 'deletepurpose',
|
||||
component: 'tool_dataprivacy',
|
||||
param: purposename
|
||||
component: 'tool_dataprivacy'
|
||||
},
|
||||
{
|
||||
key: 'deletepurposetext',
|
||||
component: 'tool_dataprivacy',
|
||||
param: purposename
|
||||
},
|
||||
{
|
||||
key: 'delete'
|
||||
}
|
||||
];
|
||||
|
||||
Str.get_strings(stringkeys).then(function(langStrings) {
|
||||
var title = langStrings[0];
|
||||
var confirmMessage = langStrings[1];
|
||||
var buttonText = langStrings[2];
|
||||
return ModalFactory.create({
|
||||
title: title,
|
||||
body: confirmMessage,
|
||||
type: ModalFactory.types.SAVE_CANCEL
|
||||
}).then(function(modal) {
|
||||
modal.setSaveButtonText(title);
|
||||
modal.setSaveButtonText(buttonText);
|
||||
|
||||
// Handle save event.
|
||||
modal.getRoot().on(ModalEvents.save, function() {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* JS module for the data requests filter.
|
||||
*
|
||||
* @module tool_dataprivacy/request_filter
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'], function($, Autocomplete, Str, Notification) {
|
||||
|
||||
/**
|
||||
* Selectors.
|
||||
*
|
||||
* @access private
|
||||
* @type {{REQUEST_FILTERS: string}}
|
||||
*/
|
||||
var SELECTORS = {
|
||||
REQUEST_FILTERS: '#request-filters'
|
||||
};
|
||||
|
||||
/**
|
||||
* Init function.
|
||||
*
|
||||
* @method init
|
||||
* @private
|
||||
*/
|
||||
var init = function() {
|
||||
var stringkeys = [
|
||||
{
|
||||
key: 'filter',
|
||||
component: 'moodle'
|
||||
},
|
||||
{
|
||||
key: 'nofiltersapplied',
|
||||
component: 'moodle'
|
||||
}
|
||||
];
|
||||
|
||||
Str.get_strings(stringkeys).then(function(langstrings) {
|
||||
var placeholder = langstrings[0];
|
||||
var noSelectionString = langstrings[1];
|
||||
return Autocomplete.enhance(SELECTORS.REQUEST_FILTERS, false, '', placeholder, false, true, noSelectionString, true);
|
||||
}).fail(Notification.exception);
|
||||
|
||||
var last = $(SELECTORS.REQUEST_FILTERS).val();
|
||||
$(SELECTORS.REQUEST_FILTERS).on('change', function() {
|
||||
var current = $(this).val();
|
||||
// Prevent form from submitting unnecessarily, eg. on blur when no filter is selected.
|
||||
if (last.join(',') !== current.join(',')) {
|
||||
// If we're submitting without filters, set the hidden input 'filters-cleared' to 1.
|
||||
if (current.length === 0) {
|
||||
$('#filters-cleared').val(1);
|
||||
}
|
||||
$(this.form).submit();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return /** @alias module:core/form-autocomplete */ {
|
||||
/**
|
||||
* Initialise the unified user filter.
|
||||
*
|
||||
* @method init
|
||||
*/
|
||||
init: function() {
|
||||
init();
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -39,11 +39,39 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
* @type {{APPROVE_REQUEST: string}}
|
||||
* @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"]'
|
||||
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"]'
|
||||
};
|
||||
|
||||
/**
|
||||
* 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'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -73,16 +101,9 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
};
|
||||
|
||||
var promises = Ajax.call([request]);
|
||||
var modalTitle = '';
|
||||
var modalType = ModalFactory.types.DEFAULT;
|
||||
$.when(promises[0]).then(function(data) {
|
||||
if (data.result) {
|
||||
// Check if the status is awaiting approval.
|
||||
if (data.result.status == 2) {
|
||||
modalType = ModalDataRequest.TYPE;
|
||||
}
|
||||
modalTitle = data.result.typename;
|
||||
return Templates.render('tool_dataprivacy/request_details', data.result);
|
||||
return data.result;
|
||||
}
|
||||
// Fail.
|
||||
Notification.addNotification({
|
||||
@@ -91,61 +112,209 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
});
|
||||
return false;
|
||||
|
||||
}).then(function(html) {
|
||||
}).then(function(data) {
|
||||
var body = Templates.render('tool_dataprivacy/request_details', data);
|
||||
var templateContext = {
|
||||
approvedeny: data.approvedeny,
|
||||
canmarkcomplete: data.canmarkcomplete
|
||||
};
|
||||
return ModalFactory.create({
|
||||
title: modalTitle,
|
||||
body: html,
|
||||
type: modalType,
|
||||
large: true
|
||||
}).then(function(modal) {
|
||||
// Handle approve event.
|
||||
modal.getRoot().on(DataPrivacyEvents.approve, function() {
|
||||
showConfirmation(DataPrivacyEvents.approve, requestId);
|
||||
});
|
||||
|
||||
// Handle deny event.
|
||||
modal.getRoot().on(DataPrivacyEvents.deny, function() {
|
||||
showConfirmation(DataPrivacyEvents.deny, requestId);
|
||||
});
|
||||
|
||||
// Handle hidden event.
|
||||
modal.getRoot().on(ModalEvents.hidden, function() {
|
||||
// Destroy when hidden.
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
return modal;
|
||||
title: data.typename,
|
||||
body: body,
|
||||
type: ModalDataRequest.TYPE,
|
||||
large: true,
|
||||
templateContext: templateContext
|
||||
});
|
||||
}).done(function(modal) {
|
||||
|
||||
}).then(function(modal) {
|
||||
// Handle approve event.
|
||||
modal.getRoot().on(DataPrivacyEvents.approve, function() {
|
||||
showConfirmation(DataPrivacyEvents.approve, approveEventWsData(requestId));
|
||||
});
|
||||
|
||||
// Handle deny event.
|
||||
modal.getRoot().on(DataPrivacyEvents.deny, function() {
|
||||
showConfirmation(DataPrivacyEvents.deny, denyEventWsData(requestId));
|
||||
});
|
||||
|
||||
// Handle send event.
|
||||
modal.getRoot().on(DataPrivacyEvents.complete, function() {
|
||||
var params = {
|
||||
'requestid': requestId
|
||||
};
|
||||
handleSave('tool_dataprivacy_mark_complete', params);
|
||||
});
|
||||
|
||||
// Handle hidden event.
|
||||
modal.getRoot().on(ModalEvents.hidden, function() {
|
||||
// Destroy when hidden.
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
// Show the modal!
|
||||
modal.show();
|
||||
}).fail(Notification.exception);
|
||||
|
||||
return;
|
||||
|
||||
}).catch(Notification.exception);
|
||||
});
|
||||
|
||||
$(ACTIONS.APPROVE_REQUEST).click(function(e) {
|
||||
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();
|
||||
|
||||
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 = '';
|
||||
|
||||
switch (action) {
|
||||
case DataPrivacyEvents.approve:
|
||||
keys = [
|
||||
@@ -158,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 = [
|
||||
@@ -171,7 +351,30 @@ 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 = [
|
||||
{
|
||||
key: 'markcomplete',
|
||||
component: 'tool_dataprivacy'
|
||||
},
|
||||
{
|
||||
key: 'confirmcompletion',
|
||||
component: 'tool_dataprivacy'
|
||||
}
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -189,26 +392,7 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
|
||||
// Handle save event.
|
||||
modal.getRoot().on(ModalEvents.save, function() {
|
||||
// Confirm the request.
|
||||
var params = {
|
||||
'requestid': requestId
|
||||
};
|
||||
|
||||
var request = {
|
||||
methodname: wsfunction,
|
||||
args: params
|
||||
};
|
||||
|
||||
Ajax.call([request])[0].done(function(data) {
|
||||
if (data.result) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
Notification.addNotification({
|
||||
message: data.warnings[0].message,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}).fail(Notification.exception);
|
||||
handleSave(wsdata.wsfunction, wsdata.wsparams);
|
||||
});
|
||||
|
||||
// Handle hidden event.
|
||||
@@ -217,9 +401,39 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
return modal;
|
||||
}).done(function(modal) {
|
||||
modal.show();
|
||||
|
||||
return;
|
||||
|
||||
}).catch(Notification.exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a web service function and reloads the page on success and shows a notification.
|
||||
* Displays an error notification, otherwise.
|
||||
*
|
||||
* @param {String} wsfunction The web service function to call.
|
||||
* @param {Object} params The parameters for the web service functoon.
|
||||
*/
|
||||
function handleSave(wsfunction, params) {
|
||||
// Confirm the request.
|
||||
var request = {
|
||||
methodname: wsfunction,
|
||||
args: params
|
||||
};
|
||||
|
||||
Ajax.call([request])[0].done(function(data) {
|
||||
if (data.result) {
|
||||
// On success, reload the page so that the data request table will be updated.
|
||||
// TODO: Probably in the future, better to reload the table or the target data request via AJAX.
|
||||
window.location.reload();
|
||||
} else {
|
||||
// Add the notification.
|
||||
Notification.addNotification({
|
||||
message: data.warnings[0].message,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}).fail(Notification.exception);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$url = new moodle_url("/admin/tool/dataprivacy/categories.php");
|
||||
$title = get_string('editcategories', 'tool_dataprivacy');
|
||||
|
||||
@@ -31,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,6 +24,7 @@
|
||||
namespace tool_dataprivacy;
|
||||
|
||||
use coding_exception;
|
||||
use context_helper;
|
||||
use context_system;
|
||||
use core\invalid_persistent_exception;
|
||||
use core\message\message;
|
||||
@@ -40,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();
|
||||
|
||||
@@ -75,7 +77,7 @@ class api {
|
||||
/** The request is now being processed. */
|
||||
const DATAREQUEST_STATUS_PROCESSING = 4;
|
||||
|
||||
/** Data request completed. */
|
||||
/** Information/other request completed. */
|
||||
const DATAREQUEST_STATUS_COMPLETE = 5;
|
||||
|
||||
/** Data request cancelled by the user. */
|
||||
@@ -84,6 +86,21 @@ class api {
|
||||
/** Data request rejected by the DPO. */
|
||||
const DATAREQUEST_STATUS_REJECTED = 7;
|
||||
|
||||
/** Data request download ready. */
|
||||
const DATAREQUEST_STATUS_DOWNLOAD_READY = 8;
|
||||
|
||||
/** Data request expired. */
|
||||
const DATAREQUEST_STATUS_EXPIRED = 9;
|
||||
|
||||
/** Data delete request completed, account is removed. */
|
||||
const DATAREQUEST_STATUS_DELETED = 10;
|
||||
|
||||
/** 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.
|
||||
*
|
||||
@@ -95,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,20 +140,59 @@ class api {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of users with the Data Protection Officer role.
|
||||
* 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.
|
||||
*
|
||||
* @return array An array of the DPO role shortnames
|
||||
*/
|
||||
public static function get_dpo_role_names() : array {
|
||||
global $DB;
|
||||
|
||||
$dporoleids = self::get_assigned_privacy_officer_roles();
|
||||
$dponames = array();
|
||||
|
||||
if (!empty($dporoleids)) {
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($dporoleids);
|
||||
$dponames = $DB->get_fieldset_select('role', 'shortname', "id {$insql}", $inparams);
|
||||
}
|
||||
|
||||
return $dponames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of users with the 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, '.
|
||||
@@ -160,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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,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 (!api::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.
|
||||
@@ -212,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();
|
||||
@@ -232,16 +276,51 @@ class api {
|
||||
* (e.g. Users with the Data Protection Officer roles)
|
||||
*
|
||||
* @param int $userid The User ID.
|
||||
* @param int[] $statuses The status filters.
|
||||
* @param int[] $types The request type filters.
|
||||
* @param 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.
|
||||
* @return data_request[]
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public static function get_data_requests($userid = 0) {
|
||||
public static function get_data_requests($userid = 0, $statuses = [], $types = [], $creationmethods = [],
|
||||
$sort = '', $offset = 0, $limit = 0) {
|
||||
global $DB, $USER;
|
||||
$results = [];
|
||||
$sort = 'status ASC, timemodified ASC';
|
||||
$sqlparams = [];
|
||||
$sqlconditions = [];
|
||||
|
||||
// Set default sort.
|
||||
if (empty($sort)) {
|
||||
$sort = 'status ASC, timemodified ASC';
|
||||
}
|
||||
|
||||
// Set status filters.
|
||||
if (!empty($statuses)) {
|
||||
list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
|
||||
$sqlconditions[] = "status $statusinsql";
|
||||
}
|
||||
|
||||
// Set request type filter.
|
||||
if (!empty($types)) {
|
||||
list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
|
||||
$sqlconditions[] = "type $typeinsql";
|
||||
$sqlparams = array_merge($sqlparams, $typeparams);
|
||||
}
|
||||
|
||||
// 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.
|
||||
$select = "(userid = :userid OR requestedby = :requestedby)";
|
||||
$sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
|
||||
$params = [
|
||||
'userid' => $userid,
|
||||
'requestedby' => $userid
|
||||
@@ -256,20 +335,105 @@ class api {
|
||||
$alloweduserids = array_merge($alloweduserids, array_keys($children));
|
||||
}
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
|
||||
$select .= " AND userid $insql";
|
||||
$params = array_merge($params, $inparams);
|
||||
$sqlconditions[] .= "userid $insql";
|
||||
$select = implode(' AND ', $sqlconditions);
|
||||
$params = array_merge($params, $inparams, $sqlparams);
|
||||
|
||||
$results = data_request::get_records_select($select, $params, $sort);
|
||||
$results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit);
|
||||
} else {
|
||||
// If the current user is one of the site's Data Protection Officers, then fetch all data requests.
|
||||
if (self::is_site_dpo($USER->id)) {
|
||||
$results = data_request::get_records(null, $sort, '');
|
||||
if (!empty($sqlconditions)) {
|
||||
$select = implode(' AND ', $sqlconditions);
|
||||
$results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit);
|
||||
} else {
|
||||
$results = data_request::get_records(null, $sort, '', $offset, $limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If any are due to expire, expire them and re-fetch updated data.
|
||||
if (empty($statuses)
|
||||
|| in_array(self::DATAREQUEST_STATUS_DOWNLOAD_READY, $statuses)
|
||||
|| in_array(self::DATAREQUEST_STATUS_EXPIRED, $statuses)) {
|
||||
$expiredrequests = data_request::get_expired_requests($userid);
|
||||
|
||||
if (!empty($expiredrequests)) {
|
||||
data_request::expire($expiredrequests);
|
||||
$results = self::get_data_requests($userid, $statuses, $types, $creationmethods, $sort, $offset, $limit);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the count of data request records based on the given parameters.
|
||||
*
|
||||
* @param int $userid The User ID.
|
||||
* @param int[] $statuses The status filters.
|
||||
* @param int[] $types The request type filters.
|
||||
* @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 = [], $creationmethods = []) {
|
||||
global $DB, $USER;
|
||||
$count = 0;
|
||||
$sqlparams = [];
|
||||
$sqlconditions = [];
|
||||
if (!empty($statuses)) {
|
||||
list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
|
||||
$sqlconditions[] = "status $statusinsql";
|
||||
}
|
||||
if (!empty($types)) {
|
||||
list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
|
||||
$sqlconditions[] = "type $typeinsql";
|
||||
$sqlparams = array_merge($sqlparams, $typeparams);
|
||||
}
|
||||
if (!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)";
|
||||
$params = [
|
||||
'userid' => $userid,
|
||||
'requestedby' => $userid
|
||||
];
|
||||
|
||||
// Build a list of user IDs that the user is allowed to make data requests for.
|
||||
// Of course, the user should be included in this list.
|
||||
$alloweduserids = [$userid];
|
||||
// Get any users that the user can make data requests for.
|
||||
if ($children = helper::get_children_of_user($userid)) {
|
||||
// Get the list of user IDs of the children and merge to the allowed user IDs.
|
||||
$alloweduserids = array_merge($alloweduserids, array_keys($children));
|
||||
}
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
|
||||
$sqlconditions[] .= "userid $insql";
|
||||
$select = implode(' AND ', $sqlconditions);
|
||||
$params = array_merge($params, $inparams, $sqlparams);
|
||||
|
||||
$count = data_request::count_records_select($select, $params);
|
||||
} else {
|
||||
// If the current user is one of the site's Data Protection Officers, then fetch all data requests.
|
||||
if (self::is_site_dpo($USER->id)) {
|
||||
if (!empty($sqlconditions)) {
|
||||
$select = implode(' AND ', $sqlconditions);
|
||||
$count = data_request::count_records_select($select, $sqlparams);
|
||||
} else {
|
||||
$count = data_request::count_records();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
|
||||
*
|
||||
@@ -287,9 +451,12 @@ class api {
|
||||
self::DATAREQUEST_STATUS_COMPLETE,
|
||||
self::DATAREQUEST_STATUS_CANCELLED,
|
||||
self::DATAREQUEST_STATUS_REJECTED,
|
||||
self::DATAREQUEST_STATUS_DOWNLOAD_READY,
|
||||
self::DATAREQUEST_STATUS_EXPIRED,
|
||||
self::DATAREQUEST_STATUS_DELETED,
|
||||
];
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED);
|
||||
$select = 'type = :type AND userid = :userid AND status NOT ' . $insql;
|
||||
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
|
||||
@@ -298,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.
|
||||
*
|
||||
@@ -310,6 +519,9 @@ class api {
|
||||
self::DATAREQUEST_STATUS_COMPLETE,
|
||||
self::DATAREQUEST_STATUS_CANCELLED,
|
||||
self::DATAREQUEST_STATUS_REJECTED,
|
||||
self::DATAREQUEST_STATUS_DOWNLOAD_READY,
|
||||
self::DATAREQUEST_STATUS_EXPIRED,
|
||||
self::DATAREQUEST_STATUS_DELETED,
|
||||
];
|
||||
|
||||
return !in_array($status, $finalstatuses);
|
||||
@@ -333,7 +545,22 @@ class api {
|
||||
if ($dpoid) {
|
||||
$datarequest->set('dpo', $dpoid);
|
||||
}
|
||||
$datarequest->set('dpocomment', $comment);
|
||||
// Update the comment if necessary.
|
||||
if (!empty(trim($comment))) {
|
||||
$params = [
|
||||
'date' => userdate(time()),
|
||||
'comment' => $comment
|
||||
];
|
||||
$commenttosave = get_string('datecomment', 'tool_dataprivacy', $params);
|
||||
// Check if there's an existing DPO comment.
|
||||
$currentcomment = trim($datarequest->get('dpocomment'));
|
||||
if ($currentcomment) {
|
||||
// Append the new comment to the current comment and give them 1 line space in between.
|
||||
$commenttosave = $currentcomment . PHP_EOL . PHP_EOL . $commenttosave;
|
||||
}
|
||||
$datarequest->set('dpocomment', $commenttosave);
|
||||
}
|
||||
|
||||
return $datarequest->update();
|
||||
}
|
||||
|
||||
@@ -428,7 +655,6 @@ class api {
|
||||
* @param data_request $request The data request
|
||||
* @return int|false
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function notify_dpo($dpo, data_request $request) {
|
||||
@@ -465,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
|
||||
];
|
||||
@@ -490,16 +718,79 @@ 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.
|
||||
*
|
||||
* @param int $userid Target user id (subject of data request)
|
||||
* @param int $requesterid Requester user id (person who requsted it)
|
||||
* @param int|null $downloaderid Person who wants to download user id (default current)
|
||||
* @return bool
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) {
|
||||
global $USER;
|
||||
|
||||
if (!$downloaderid) {
|
||||
$downloaderid = $USER->id;
|
||||
}
|
||||
|
||||
$usercontext = \context_user::instance($userid);
|
||||
// If it's your own and you have the right capability, you can download it.
|
||||
if ($userid == $downloaderid && has_capability('tool/dataprivacy:downloadownrequest', $usercontext, $downloaderid)) {
|
||||
return true;
|
||||
}
|
||||
// If you can download anyone's in that context, you can download it.
|
||||
if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) {
|
||||
return true;
|
||||
}
|
||||
// If you can have the 'child access' ability to request in that context, and you are the one
|
||||
// who requested it, then you can download it.
|
||||
if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an action menu link to download a data request.
|
||||
*
|
||||
* @param \context_user $usercontext User context (of user who the data is for)
|
||||
* @param int $requestid Request id
|
||||
* @return \action_menu_link_secondary Action menu link
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public static function get_download_link(\context_user $usercontext, $requestid) {
|
||||
$downloadurl = moodle_url::make_pluginfile_url($usercontext->id,
|
||||
'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true);
|
||||
$downloadtext = get_string('download', 'tool_dataprivacy');
|
||||
return new \action_menu_link_secondary($downloadurl, null, $downloadtext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new data purpose.
|
||||
*
|
||||
@@ -507,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();
|
||||
|
||||
@@ -522,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 = '';
|
||||
}
|
||||
@@ -543,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.');
|
||||
@@ -558,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');
|
||||
}
|
||||
|
||||
@@ -570,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();
|
||||
|
||||
@@ -585,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);
|
||||
|
||||
@@ -602,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.');
|
||||
@@ -617,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');
|
||||
}
|
||||
|
||||
@@ -629,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);
|
||||
@@ -657,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();
|
||||
}
|
||||
|
||||
@@ -671,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');
|
||||
@@ -704,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;
|
||||
}
|
||||
@@ -720,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;
|
||||
}
|
||||
@@ -733,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');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -753,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;
|
||||
}
|
||||
@@ -768,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,
|
||||
@@ -787,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();
|
||||
}
|
||||
@@ -801,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();
|
||||
}
|
||||
@@ -816,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);
|
||||
@@ -824,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)
|
||||
@@ -900,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
|
||||
@@ -927,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;
|
||||
}
|
||||
@@ -939,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,52 +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);
|
||||
public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false,
|
||||
$forcedcategoryvalue = false, $component = '') {
|
||||
// Get the defaults for this context level.
|
||||
list($purposeid, $categoryid) = self::get_defaults($contextlevel, $component);
|
||||
|
||||
// Honour forced values.
|
||||
if ($forcedpurposevalue) {
|
||||
@@ -301,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];
|
||||
}
|
||||
|
||||
@@ -344,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.
|
||||
*
|
||||
@@ -85,6 +93,9 @@ class data_request extends persistent {
|
||||
api::DATAREQUEST_STATUS_COMPLETE,
|
||||
api::DATAREQUEST_STATUS_CANCELLED,
|
||||
api::DATAREQUEST_STATUS_REJECTED,
|
||||
api::DATAREQUEST_STATUS_DOWNLOAD_READY,
|
||||
api::DATAREQUEST_STATUS_EXPIRED,
|
||||
api::DATAREQUEST_STATUS_DELETED,
|
||||
],
|
||||
'type' => PARAM_INT
|
||||
],
|
||||
@@ -108,6 +119,173 @@ 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
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a completed data export request has expired.
|
||||
* The response will be valid regardless of the expiry scheduled task having run.
|
||||
*
|
||||
* @param data_request $request the data request object whose expiry will be checked.
|
||||
* @return bool true if the request has expired.
|
||||
*/
|
||||
public static function is_expired(data_request $request) {
|
||||
$result = false;
|
||||
|
||||
// Only export requests expire.
|
||||
if ($request->get('type') == api::DATAREQUEST_TYPE_EXPORT) {
|
||||
switch ($request->get('status')) {
|
||||
// Expired requests are obviously expired.
|
||||
case api::DATAREQUEST_STATUS_EXPIRED:
|
||||
$result = true;
|
||||
break;
|
||||
// Complete requests are expired if the expiry time has elapsed.
|
||||
case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
|
||||
$expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry');
|
||||
if ($expiryseconds > 0 && time() >= ($request->get('timemodified') + $expiryseconds)) {
|
||||
$result = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch completed data requests which are due to expire.
|
||||
*
|
||||
* @param int $userid Optional user ID to filter by.
|
||||
*
|
||||
* @return array Details of completed requests which are due to expire.
|
||||
*/
|
||||
public static function get_expired_requests($userid = 0) {
|
||||
global $DB;
|
||||
|
||||
$expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry');
|
||||
$expirytime = strtotime("-{$expiryseconds} second");
|
||||
$table = self::TABLE;
|
||||
$sqlwhere = 'type = :export_type AND status = :completestatus AND timemodified <= :expirytime';
|
||||
$params = array(
|
||||
'export_type' => api::DATAREQUEST_TYPE_EXPORT,
|
||||
'completestatus' => api::DATAREQUEST_STATUS_DOWNLOAD_READY,
|
||||
'expirytime' => $expirytime,
|
||||
);
|
||||
$sort = 'id';
|
||||
$fields = 'id, userid';
|
||||
|
||||
// Filter by user ID if specified.
|
||||
if ($userid > 0) {
|
||||
$sqlwhere .= ' AND (userid = :userid OR requestedby = :requestedby)';
|
||||
$params['userid'] = $userid;
|
||||
$params['requestedby'] = $userid;
|
||||
}
|
||||
|
||||
return $DB->get_records_select_menu($table, $sqlwhere, $params, $sort, $fields, 0, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expire a given set of data requests.
|
||||
* Update request status and delete the files.
|
||||
*
|
||||
* @param array $expiredrequests [requestid => userid]
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function expire($expiredrequests) {
|
||||
global $DB;
|
||||
|
||||
$ids = array_keys($expiredrequests);
|
||||
|
||||
if (count($ids) > 0) {
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($ids);
|
||||
$initialparams = array(api::DATAREQUEST_STATUS_EXPIRED, time());
|
||||
$params = array_merge($initialparams, $inparams);
|
||||
|
||||
$update = "UPDATE {" . self::TABLE . "}
|
||||
SET status = ?, timemodified = ?
|
||||
WHERE id $insql";
|
||||
|
||||
if ($DB->execute($update, $params)) {
|
||||
$fs = get_file_storage();
|
||||
|
||||
foreach ($expiredrequests as $id => $userid) {
|
||||
$usercontext = \context_user::instance($userid);
|
||||
$fs->delete_area_files($usercontext->id, 'tool_dataprivacy', 'export', $id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,151 +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 tool_dataprivacy\purpose;
|
||||
use tool_dataprivacy\context_instance;
|
||||
|
||||
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 \core_privacy\manager $privacymanager
|
||||
* @param \tool_dataprivacy\expired_context $expiredctx
|
||||
* @return \context|false
|
||||
*/
|
||||
protected function delete_expired_context(\core_privacy\manager $privacymanager, \tool_dataprivacy\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;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ use context_helper;
|
||||
use context_system;
|
||||
use context_user;
|
||||
use core\invalid_persistent_exception;
|
||||
use core\notification;
|
||||
use core_user;
|
||||
use dml_exception;
|
||||
use external_api;
|
||||
@@ -91,17 +92,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 {
|
||||
@@ -144,7 +158,7 @@ class external extends external_api {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny a data request.
|
||||
* Make a general enquiry to a DPO.
|
||||
*
|
||||
* @since Moodle 3.5
|
||||
* @param string $message The message to be sent to the DPO.
|
||||
@@ -210,7 +224,7 @@ class external extends external_api {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter description for deny_data_request().
|
||||
* Parameter description for contact_dpo().
|
||||
*
|
||||
* @since Moodle 3.5
|
||||
* @return external_description
|
||||
@@ -222,6 +236,71 @@ class external extends external_api {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter description for mark_complete().
|
||||
*
|
||||
* @since Moodle 3.5.2
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function mark_complete_parameters() {
|
||||
return new external_function_parameters([
|
||||
'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a user's general enquiry's status as complete.
|
||||
*
|
||||
* @since Moodle 3.5.2
|
||||
* @param int $requestid The request ID of the general enquiry.
|
||||
* @return array
|
||||
* @throws coding_exception
|
||||
* @throws invalid_parameter_exception
|
||||
* @throws invalid_persistent_exception
|
||||
* @throws restricted_context_exception
|
||||
* @throws dml_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function mark_complete($requestid) {
|
||||
global $USER;
|
||||
|
||||
$warnings = [];
|
||||
$params = external_api::validate_parameters(self::mark_complete_parameters(), [
|
||||
'requestid' => $requestid,
|
||||
]);
|
||||
$requestid = $params['requestid'];
|
||||
|
||||
// Validate context 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.
|
||||
if ($result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE, $USER->id, $message)) {
|
||||
// Add notification in the session to be shown when the page is reloaded on the JS side.
|
||||
notification::success(get_string('requestmarkedcomplete', 'tool_dataprivacy'));
|
||||
}
|
||||
|
||||
return [
|
||||
'result' => $result,
|
||||
'warnings' => $warnings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter description for mark_complete().
|
||||
*
|
||||
* @since Moodle 3.5.2
|
||||
* @return external_description
|
||||
*/
|
||||
public static function mark_complete_returns() {
|
||||
return new external_single_structure([
|
||||
'result' => new external_value(PARAM_BOOL, 'The processing result'),
|
||||
'warnings' => new external_warnings()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter description for get_data_request().
|
||||
*
|
||||
@@ -258,9 +337,9 @@ class external extends external_api {
|
||||
// Validate context.
|
||||
$context = context_system::instance();
|
||||
self::validate_context($context);
|
||||
$requestpersistent = new data_request($requestid);
|
||||
require_capability('tool/dataprivacy:managedatarequests', $context);
|
||||
|
||||
$requestpersistent = new data_request($requestid);
|
||||
$exporter = new data_request_exporter($requestpersistent, ['context' => $context]);
|
||||
$renderer = $PAGE->get_renderer('tool_dataprivacy');
|
||||
$result = $exporter->export($renderer);
|
||||
@@ -326,6 +405,9 @@ class external extends external_api {
|
||||
$result = false;
|
||||
if ($requestexists) {
|
||||
$result = api::approve_data_request($requestid);
|
||||
|
||||
// Add notification in the session to be shown when the page is reloaded on the JS side.
|
||||
notification::success(get_string('requestapproved', 'tool_dataprivacy'));
|
||||
} else {
|
||||
$warnings[] = [
|
||||
'item' => $requestid,
|
||||
@@ -353,6 +435,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().
|
||||
*
|
||||
@@ -395,6 +556,9 @@ class external extends external_api {
|
||||
$result = false;
|
||||
if ($requestexists) {
|
||||
$result = api::deny_data_request($requestid);
|
||||
|
||||
// Add notification in the session to be shown when the page is reloaded on the JS side.
|
||||
notification::success(get_string('requestdenied', 'tool_dataprivacy'));
|
||||
} else {
|
||||
$warnings[] = [
|
||||
'item' => $requestid,
|
||||
@@ -422,6 +586,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().
|
||||
*
|
||||
@@ -519,7 +762,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();
|
||||
@@ -587,6 +832,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 [
|
||||
@@ -636,7 +885,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();
|
||||
@@ -704,6 +955,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 [
|
||||
@@ -753,8 +1008,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();
|
||||
@@ -768,7 +1024,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) {
|
||||
@@ -823,8 +1078,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();
|
||||
@@ -834,6 +1090,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);
|
||||
@@ -963,9 +1220,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)) {
|
||||
@@ -975,24 +1232,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1039,6 +1299,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,16 @@ class data_request_exporter extends persistent_exporter {
|
||||
'optional' => true,
|
||||
'default' => false
|
||||
],
|
||||
'approvedeny' => [
|
||||
'type' => PARAM_BOOL,
|
||||
'optional' => true,
|
||||
'default' => false
|
||||
],
|
||||
'canmarkcomplete' => [
|
||||
'type' => PARAM_BOOL,
|
||||
'optional' => true,
|
||||
'default' => false
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -140,14 +150,19 @@ class data_request_exporter extends persistent_exporter {
|
||||
|
||||
$values['messagehtml'] = text_to_html($this->persistent->get('comments'));
|
||||
|
||||
$values['typename'] = helper::get_request_type_string($this->persistent->get('type'));
|
||||
$values['typenameshort'] = helper::get_shortened_request_type_string($this->persistent->get('type'));
|
||||
$requesttype = $this->persistent->get('type');
|
||||
$values['typename'] = helper::get_request_type_string($requesttype);
|
||||
$values['typenameshort'] = helper::get_shortened_request_type_string($requesttype);
|
||||
|
||||
$values['canreview'] = false;
|
||||
$values['approvedeny'] = false;
|
||||
$values['statuslabel'] = helper::get_request_status_string($this->persistent->get('status'));
|
||||
|
||||
switch ($this->persistent->get('status')) {
|
||||
case api::DATAREQUEST_STATUS_PENDING:
|
||||
$values['statuslabelclass'] = 'label-default';
|
||||
$values['statuslabelclass'] = 'label-info';
|
||||
// Request can be manually completed for general enquiry requests.
|
||||
$values['canmarkcomplete'] = $requesttype == api::DATAREQUEST_TYPE_OTHERS;
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_PREPROCESSING:
|
||||
$values['statuslabelclass'] = 'label-default';
|
||||
@@ -156,6 +171,8 @@ class data_request_exporter extends persistent_exporter {
|
||||
$values['statuslabelclass'] = 'label-info';
|
||||
// DPO can review the request once it's ready.
|
||||
$values['canreview'] = true;
|
||||
// Whether the DPO can approve or deny the request.
|
||||
$values['approvedeny'] = in_array($requesttype, [api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_TYPE_DELETE]);
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_APPROVED:
|
||||
$values['statuslabelclass'] = 'label-info';
|
||||
@@ -164,6 +181,8 @@ class data_request_exporter extends persistent_exporter {
|
||||
$values['statuslabelclass'] = 'label-info';
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_COMPLETE:
|
||||
case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
|
||||
case api::DATAREQUEST_STATUS_DELETED:
|
||||
$values['statuslabelclass'] = 'label-success';
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_CANCELLED:
|
||||
@@ -172,6 +191,9 @@ class data_request_exporter extends persistent_exporter {
|
||||
case api::DATAREQUEST_STATUS_REJECTED:
|
||||
$values['statuslabelclass'] = 'label-important';
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_EXPIRED:
|
||||
$values['statuslabelclass'] = 'label-default';
|
||||
break;
|
||||
}
|
||||
|
||||
return $values;
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\data_registry;
|
||||
use tool_dataprivacy\purpose;
|
||||
|
||||
/**
|
||||
* Context instance data form.
|
||||
@@ -144,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,
|
||||
@@ -167,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,
|
||||
@@ -186,12 +192,12 @@ class context_instance extends \core\form\persistent {
|
||||
/**
|
||||
* Returns the purpose display text.
|
||||
*
|
||||
* @param \tool_dataprivacy\purpose $effectivepurpose
|
||||
* @param purpose $effectivepurpose
|
||||
* @param int $retentioncontextlevel
|
||||
* @param \context $context The context, just for displaying (filters) purposes.
|
||||
* @return string
|
||||
*/
|
||||
protected static function get_retention_display_text(\tool_dataprivacy\purpose $effectivepurpose, $retentioncontextlevel, \context $context) {
|
||||
protected static function get_retention_display_text(purpose $effectivepurpose, $retentioncontextlevel, \context $context) {
|
||||
global $PAGE;
|
||||
|
||||
$renderer = $PAGE->get_renderer('tool_dataprivacy');
|
||||
|
||||
@@ -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.
|
||||
@@ -35,6 +36,23 @@ use tool_dataprivacy\api;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class helper {
|
||||
/** The default number of results to be shown per page. */
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
/** Filter constant associated with the request type filter. */
|
||||
const FILTER_TYPE = 1;
|
||||
|
||||
/** Filter constant associated with the request status filter. */
|
||||
const FILTER_STATUS = 2;
|
||||
|
||||
/** 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.
|
||||
@@ -45,16 +63,11 @@ class helper {
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function get_request_type_string($requesttype) {
|
||||
switch ($requesttype) {
|
||||
case api::DATAREQUEST_TYPE_EXPORT:
|
||||
return get_string('requesttypeexport', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_TYPE_DELETE:
|
||||
return get_string('requesttypedelete', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_TYPE_OTHERS:
|
||||
return get_string('requesttypeothers', 'tool_dataprivacy');
|
||||
default:
|
||||
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
|
||||
$types = self::get_request_types();
|
||||
if (!isset($types[$requesttype])) {
|
||||
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
|
||||
}
|
||||
return $types[$requesttype];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,16 +79,37 @@ class helper {
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function get_shortened_request_type_string($requesttype) {
|
||||
switch ($requesttype) {
|
||||
case api::DATAREQUEST_TYPE_EXPORT:
|
||||
return get_string('requesttypeexportshort', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_TYPE_DELETE:
|
||||
return get_string('requesttypedeleteshort', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_TYPE_OTHERS:
|
||||
return get_string('requesttypeothersshort', 'tool_dataprivacy');
|
||||
default:
|
||||
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
|
||||
$types = self::get_request_types_short();
|
||||
if (!isset($types[$requesttype])) {
|
||||
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
|
||||
}
|
||||
return $types[$requesttype];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key value-pairs of request type code and their string value.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_request_types() {
|
||||
return [
|
||||
api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexport', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedelete', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothers', 'tool_dataprivacy'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key value-pairs of request type code and their shortened string value.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_request_types_short() {
|
||||
return [
|
||||
api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexportshort', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedeleteshort', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothersshort', 'tool_dataprivacy'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,30 +117,64 @@ class helper {
|
||||
*
|
||||
* @param int $status The request status.
|
||||
* @return string
|
||||
* @throws coding_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function get_request_status_string($status) {
|
||||
switch ($status) {
|
||||
case api::DATAREQUEST_STATUS_PENDING:
|
||||
return get_string('statuspending', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_PREPROCESSING:
|
||||
return get_string('statuspreprocessing', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_AWAITING_APPROVAL:
|
||||
return get_string('statusawaitingapproval', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_APPROVED:
|
||||
return get_string('statusapproved', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_PROCESSING:
|
||||
return get_string('statusprocessing', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_COMPLETE:
|
||||
return get_string('statuscomplete', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_CANCELLED:
|
||||
return get_string('statuscancelled', 'tool_dataprivacy');
|
||||
case api::DATAREQUEST_STATUS_REJECTED:
|
||||
return get_string('statusrejected', 'tool_dataprivacy');
|
||||
default:
|
||||
throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
|
||||
$statuses = self::get_request_statuses();
|
||||
if (!isset($statuses[$status])) {
|
||||
throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
return $statuses[$status];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key value-pairs of request status code and string value.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_request_statuses() {
|
||||
return [
|
||||
api::DATAREQUEST_STATUS_PENDING => get_string('statuspending', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_PREPROCESSING => get_string('statuspreprocessing', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_AWAITING_APPROVAL => get_string('statusawaitingapproval', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_APPROVED => get_string('statusapproved', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_PROCESSING => get_string('statusprocessing', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_COMPLETE => get_string('statuscomplete', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_DOWNLOAD_READY => get_string('statusready', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_EXPIRED => get_string('statusexpired', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_CANCELLED => get_string('statuscancelled', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_REJECTED => get_string('statusrejected', 'tool_dataprivacy'),
|
||||
api::DATAREQUEST_STATUS_DELETED => get_string('statusdeleted', 'tool_dataprivacy'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +200,7 @@ class helper {
|
||||
'contextlevel' => CONTEXT_USER
|
||||
];
|
||||
|
||||
// The final list of users that we will return;
|
||||
// The final list of users that we will return.
|
||||
$finalresults = [];
|
||||
|
||||
// Our prospective list of users.
|
||||
@@ -146,4 +214,38 @@ class helper {
|
||||
}
|
||||
return $finalresults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get options for the data requests filter.
|
||||
*
|
||||
* @return array
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public static function get_request_filter_options() {
|
||||
$filters = [
|
||||
self::FILTER_TYPE => (object)[
|
||||
'name' => get_string('requesttype', 'tool_dataprivacy'),
|
||||
'options' => self::get_request_types_short()
|
||||
],
|
||||
self::FILTER_STATUS => (object)[
|
||||
'name' => get_string('requeststatus', 'tool_dataprivacy'),
|
||||
'options' => self::get_request_statuses()
|
||||
],
|
||||
self::FILTER_CREATION => (object)[
|
||||
'name' => get_string('requestcreation', 'tool_dataprivacy'),
|
||||
'options' => self::get_request_creation_methods()
|
||||
],
|
||||
];
|
||||
$options = [];
|
||||
foreach ($filters as $category => $filtercategory) {
|
||||
foreach ($filtercategory->options as $key => $name) {
|
||||
$option = (object)[
|
||||
'category' => $filtercategory->name,
|
||||
'name' => $name
|
||||
];
|
||||
$options["{$category}:{$key}"] = get_string('filteroption', 'tool_dataprivacy', $option);
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,11 +70,37 @@ class metadata_registry {
|
||||
$internaldata['compliant'] = false;
|
||||
}
|
||||
// Check to see if we are an external plugin.
|
||||
$componentshortname = explode('_', $component);
|
||||
// Plugin names can contain _ characters, limit to 2 to just remove initial plugintype.
|
||||
$componentshortname = explode('_', $component, 2);
|
||||
$shortname = array_pop($componentshortname);
|
||||
if (isset($contributedplugins[$plugintype][$shortname])) {
|
||||
$internaldata['external'] = true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace tool_dataprivacy\output;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use coding_exception;
|
||||
use dml_exception;
|
||||
use moodle_exception;
|
||||
use moodle_url;
|
||||
use renderable;
|
||||
@@ -34,7 +33,7 @@ use single_select;
|
||||
use stdClass;
|
||||
use templatable;
|
||||
use tool_dataprivacy\data_request;
|
||||
use tool_dataprivacy\output\expired_contexts_table;
|
||||
use tool_dataprivacy\local\helper;
|
||||
|
||||
/**
|
||||
* Class containing data for a user's data requests.
|
||||
@@ -44,9 +43,6 @@ use tool_dataprivacy\output\expired_contexts_table;
|
||||
*/
|
||||
class data_deletion_page implements renderable, templatable {
|
||||
|
||||
/** The default number of results to be shown per page. */
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
/** @var data_request[] $requests List of data requests. */
|
||||
protected $filter = null;
|
||||
|
||||
@@ -57,7 +53,7 @@ class data_deletion_page implements renderable, templatable {
|
||||
* Construct this renderable.
|
||||
*
|
||||
* @param \tool_dataprivacy\data_request[] $filter
|
||||
* @param \tool_dataprivacy\expired_contexts_table $expiredcontextstable
|
||||
* @param expired_contexts_table $expiredcontextstable
|
||||
*/
|
||||
public function __construct($filter, expired_contexts_table $expiredcontextstable) {
|
||||
$this->filter = $filter;
|
||||
@@ -70,7 +66,6 @@ class data_deletion_page implements renderable, templatable {
|
||||
* @param renderer_base $output
|
||||
* @return stdClass
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public function export_for_template(renderer_base $output) {
|
||||
@@ -87,7 +82,7 @@ class data_deletion_page implements renderable, templatable {
|
||||
$data->filter = $filterselector->export_for_template($output);
|
||||
|
||||
ob_start();
|
||||
$this->expiredcontextstable->out(self::DEFAULT_PAGE_SIZE, true);
|
||||
$this->expiredcontextstable->out(helper::DEFAULT_PAGE_SIZE, true);
|
||||
$expiredcontexts = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$data->expiredcontexts = $expiredcontexts;
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -24,20 +24,17 @@
|
||||
namespace tool_dataprivacy\output;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use action_menu;
|
||||
use action_menu_link_secondary;
|
||||
use coding_exception;
|
||||
use context_system;
|
||||
use dml_exception;
|
||||
use moodle_exception;
|
||||
use moodle_url;
|
||||
use renderable;
|
||||
use renderer_base;
|
||||
use single_select;
|
||||
use stdClass;
|
||||
use templatable;
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\data_request;
|
||||
use tool_dataprivacy\external\data_request_exporter;
|
||||
use tool_dataprivacy\local\helper;
|
||||
|
||||
/**
|
||||
* Class containing data for a user's data requests.
|
||||
@@ -47,16 +44,21 @@ use tool_dataprivacy\external\data_request_exporter;
|
||||
*/
|
||||
class data_requests_page implements renderable, templatable {
|
||||
|
||||
/** @var data_request[] $requests List of data requests. */
|
||||
protected $requests = [];
|
||||
/** @var data_requests_table $table The data requests table. */
|
||||
protected $table;
|
||||
|
||||
/** @var int[] $filters The applied filters. */
|
||||
protected $filters = [];
|
||||
|
||||
/**
|
||||
* Construct this renderable.
|
||||
*
|
||||
* @param data_request[] $requests
|
||||
* @param data_requests_table $table The data requests table.
|
||||
* @param int[] $filters The applied filters.
|
||||
*/
|
||||
public function __construct($requests) {
|
||||
$this->requests = $requests;
|
||||
public function __construct($table, $filters) {
|
||||
$this->table = $table;
|
||||
$this->filters = $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,43 +80,17 @@ class data_requests_page implements renderable, templatable {
|
||||
$data->httpsite = array('message' => $httpwarningmessage, 'announce' => 1);
|
||||
}
|
||||
|
||||
$requests = [];
|
||||
foreach ($this->requests as $request) {
|
||||
$requestid = $request->get('id');
|
||||
$status = $request->get('status');
|
||||
$requestexporter = new data_request_exporter($request, ['context' => context_system::instance()]);
|
||||
$item = $requestexporter->export($output);
|
||||
$url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
|
||||
$filteroptions = helper::get_request_filter_options();
|
||||
$filter = new request_filter($filteroptions, $this->filters, $url);
|
||||
$data->filter = $filter->export_for_template($output);
|
||||
|
||||
// Prepare actions.
|
||||
$actions = [];
|
||||
ob_start();
|
||||
$this->table->out($this->table->get_requests_per_page(), true);
|
||||
$requests = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
// View action.
|
||||
$actionurl = new moodle_url('#');
|
||||
$actiondata = ['data-action' => 'view', 'data-requestid' => $requestid];
|
||||
$actiontext = get_string('viewrequest', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
|
||||
if ($status == api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
|
||||
// Approve.
|
||||
$actiondata['data-action'] = 'approve';
|
||||
$actiontext = get_string('approverequest', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
|
||||
// Deny.
|
||||
$actiondata['data-action'] = 'deny';
|
||||
$actiontext = get_string('denyrequest', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
}
|
||||
|
||||
$actionsmenu = new action_menu($actions);
|
||||
$actionsmenu->set_menu_trigger(get_string('actions'));
|
||||
$actionsmenu->set_owner_selector('request-actions-' . $requestid);
|
||||
$actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
|
||||
$item->actions = $actionsmenu->export_for_template($output);
|
||||
|
||||
$requests[] = $item;
|
||||
}
|
||||
$data->requests = $requests;
|
||||
$data->datarequests = $requests;
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Contains the class used for the displaying the data requests table.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Jun Pataleta <jun@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace tool_dataprivacy\output;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->libdir . '/tablelib.php');
|
||||
|
||||
use action_menu;
|
||||
use action_menu_link_secondary;
|
||||
use coding_exception;
|
||||
use dml_exception;
|
||||
use html_writer;
|
||||
use moodle_url;
|
||||
use stdClass;
|
||||
use table_sql;
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\external\data_request_exporter;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
/**
|
||||
* The class for displaying the data requests table.
|
||||
*
|
||||
* @copyright 2018 Jun Pataleta <jun@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class data_requests_table extends table_sql {
|
||||
|
||||
/** @var int The user ID. */
|
||||
protected $userid = 0;
|
||||
|
||||
/** @var int[] The status filters. */
|
||||
protected $statuses = [];
|
||||
|
||||
/** @var int[] The request type filters. */
|
||||
protected $types = [];
|
||||
|
||||
/** @var bool Whether this table is being rendered for managing data requests. */
|
||||
protected $manage = false;
|
||||
|
||||
/** @var \tool_dataprivacy\data_request[] Array of data request persistents. */
|
||||
protected $datarequests = [];
|
||||
|
||||
/** @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 = [], $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'),
|
||||
'requestedby' => get_string('requestby', 'tool_dataprivacy'),
|
||||
'status' => get_string('requeststatus', 'tool_dataprivacy'),
|
||||
'comments' => get_string('message', 'tool_dataprivacy'),
|
||||
'actions' => '',
|
||||
];
|
||||
|
||||
$this->define_columns(array_keys($columnheaders));
|
||||
$this->define_headers(array_values($columnheaders));
|
||||
$this->no_sorting('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)]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The type column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_type($data) {
|
||||
if ($this->manage) {
|
||||
return $data->typenameshort;
|
||||
}
|
||||
return $data->typename;
|
||||
}
|
||||
|
||||
/**
|
||||
* The user column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return mixed
|
||||
*/
|
||||
public function col_userid($data) {
|
||||
$user = $data->foruser;
|
||||
return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The context information column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_timecreated($data) {
|
||||
return userdate($data->timecreated);
|
||||
}
|
||||
|
||||
/**
|
||||
* The requesting user's column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return mixed
|
||||
*/
|
||||
public function col_requestedby($data) {
|
||||
$user = $data->requestedbyuser;
|
||||
return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The status column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return mixed
|
||||
*/
|
||||
public function col_status($data) {
|
||||
return html_writer::span($data->statuslabel, 'label ' . $data->statuslabelclass);
|
||||
}
|
||||
|
||||
/**
|
||||
* The comments column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_comments($data) {
|
||||
return shorten_text($data->comments, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* The actions column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_actions($data) {
|
||||
global $OUTPUT;
|
||||
|
||||
$requestid = $data->id;
|
||||
$status = $data->status;
|
||||
|
||||
// Prepare actions.
|
||||
$actions = [];
|
||||
|
||||
// View action.
|
||||
$actionurl = new moodle_url('#');
|
||||
$actiondata = ['data-action' => 'view', 'data-requestid' => $requestid];
|
||||
$actiontext = get_string('viewrequest', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
|
||||
switch ($status) {
|
||||
case api::DATAREQUEST_STATUS_PENDING:
|
||||
// Add action to mark a general enquiry request as complete.
|
||||
if ($data->type == api::DATAREQUEST_TYPE_OTHERS) {
|
||||
$actiondata['data-action'] = 'complete';
|
||||
$nameemail = (object)[
|
||||
'name' => $data->foruser->fullname,
|
||||
'email' => $data->foruser->email
|
||||
];
|
||||
$actiondata['data-requestid'] = $data->id;
|
||||
$actiondata['data-replytoemail'] = get_string('nameemail', 'tool_dataprivacy', $nameemail);
|
||||
$actiontext = get_string('markcomplete', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
}
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_AWAITING_APPROVAL:
|
||||
// Approve.
|
||||
$actiondata['data-action'] = 'approve';
|
||||
$actiontext = get_string('approverequest', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
|
||||
// Deny.
|
||||
$actiondata['data-action'] = 'deny';
|
||||
$actiontext = get_string('denyrequest', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
|
||||
$userid = $data->foruser->id;
|
||||
$usercontext = \context_user::instance($userid, IGNORE_MISSING);
|
||||
// If user has permission to view download link, show relevant action item.
|
||||
if ($usercontext && api::can_download_data_request_for_user($userid, $data->requestedbyuser->id)) {
|
||||
$actions[] = api::get_download_link($usercontext, $requestid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
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);
|
||||
$actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
|
||||
$actionsmenu->set_constraint('[data-region=data-requests-table] > .no-overflow');
|
||||
|
||||
return $OUTPUT->render($actionsmenu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the database for results to display in the table.
|
||||
*
|
||||
* @param int $pagesize size of page for paginated displayed table.
|
||||
* @param bool $useinitialsbar do you want to use the initials bar.
|
||||
* @throws dml_exception
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function query_db($pagesize, $useinitialsbar = true) {
|
||||
global $PAGE;
|
||||
|
||||
// Set dummy page total until we fetch full result set.
|
||||
$this->pagesize($pagesize, $pagesize + 1);
|
||||
|
||||
$sort = $this->get_sql_sort();
|
||||
|
||||
// Get data requests from the given conditions.
|
||||
$datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types,
|
||||
$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,
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override default implementation to display a more meaningful information to the user.
|
||||
*/
|
||||
public function print_nothing_to_display() {
|
||||
global $OUTPUT;
|
||||
echo $this->render_reset_button();
|
||||
$this->print_initials_bar();
|
||||
if (!empty($this->statuses) || !empty($this->types)) {
|
||||
$message = get_string('nodatarequestsmatchingfilter', 'tool_dataprivacy');
|
||||
} else {
|
||||
$message = get_string('nodatarequests', 'tool_dataprivacy');
|
||||
}
|
||||
echo $OUTPUT->notification($message, 'warning');
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the table's show_hide_link method to prevent the show/hide links from rendering.
|
||||
*
|
||||
* @param string $column the column name, index into various names.
|
||||
* @param int $index numerical index of the column.
|
||||
* @return string HTML fragment.
|
||||
*/
|
||||
protected function show_hide_link($column, $index) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ class my_data_requests_page implements renderable, templatable {
|
||||
$requestid = $request->get('id');
|
||||
$status = $request->get('status');
|
||||
$userid = $request->get('userid');
|
||||
$type = $request->get('type');
|
||||
|
||||
$usercontext = context_user::instance($userid, IGNORE_MISSING);
|
||||
if (!$usercontext) {
|
||||
@@ -94,7 +95,8 @@ class my_data_requests_page implements renderable, templatable {
|
||||
$requestexporter = new data_request_exporter($request, ['context' => $outputcontext]);
|
||||
$item = $requestexporter->export($output);
|
||||
|
||||
if ($request->get('userid') != $USER->id) {
|
||||
$self = $request->get('userid') == $USER->id;
|
||||
if (!$self) {
|
||||
// Append user name if it differs from $USER.
|
||||
$a = (object)['typename' => $item->typename, 'user' => $item->foruser->fullname];
|
||||
$item->typename = get_string('requesttypeuser', 'tool_dataprivacy', $a);
|
||||
@@ -107,7 +109,28 @@ class my_data_requests_page implements renderable, templatable {
|
||||
$item->statuslabelclass = 'label-success';
|
||||
$item->statuslabel = get_string('statuscomplete', 'tool_dataprivacy');
|
||||
$cancancel = false;
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
|
||||
$item->statuslabelclass = 'label-success';
|
||||
$item->statuslabel = get_string('statusready', 'tool_dataprivacy');
|
||||
$cancancel = false;
|
||||
$candownload = true;
|
||||
|
||||
if ($usercontext) {
|
||||
$candownload = api::can_download_data_request_for_user(
|
||||
$request->get('userid'), $request->get('requestedby'));
|
||||
}
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_DELETED:
|
||||
$item->statuslabelclass = 'label-success';
|
||||
$item->statuslabel = get_string('statusdeleted', 'tool_dataprivacy');
|
||||
$cancancel = false;
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_EXPIRED:
|
||||
$item->statuslabelclass = 'label-default';
|
||||
$item->statuslabel = get_string('statusexpired', 'tool_dataprivacy');
|
||||
$item->statuslabeltitle = get_string('downloadexpireduser', 'tool_dataprivacy');
|
||||
$cancancel = false;
|
||||
break;
|
||||
case api::DATAREQUEST_STATUS_CANCELLED:
|
||||
case api::DATAREQUEST_STATUS_REJECTED:
|
||||
@@ -124,10 +147,7 @@ class my_data_requests_page implements renderable, templatable {
|
||||
$actions[] = new action_menu_link_secondary($cancelurl, null, $canceltext, $canceldata);
|
||||
}
|
||||
if ($candownload && $usercontext) {
|
||||
$downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $requestid, '/',
|
||||
'export.zip', true);
|
||||
$downloadtext = get_string('download', 'tool_dataprivacy');
|
||||
$actions[] = new action_menu_link_secondary($downloadurl, null, $downloadtext);
|
||||
$actions[] = api::get_download_link($usercontext, $requestid);
|
||||
}
|
||||
if (!empty($actions)) {
|
||||
$actionsmenu = new action_menu($actions);
|
||||
|
||||
@@ -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,98 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Class containing the filter options data for rendering the autocomplete element for the data requests page.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace tool_dataprivacy\output;
|
||||
|
||||
use moodle_url;
|
||||
use renderable;
|
||||
use renderer_base;
|
||||
use stdClass;
|
||||
use templatable;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Class containing the filter options data for rendering the autocomplete element for the data requests page.
|
||||
*
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class request_filter implements renderable, templatable {
|
||||
|
||||
/** @var array $filteroptions The filter options. */
|
||||
protected $filteroptions;
|
||||
|
||||
/** @var array $selectedoptions The list of selected filter option values. */
|
||||
protected $selectedoptions;
|
||||
|
||||
/** @var moodle_url|string $baseurl The url with params needed to call up this page. */
|
||||
protected $baseurl;
|
||||
|
||||
/**
|
||||
* request_filter constructor.
|
||||
*
|
||||
* @param array $filteroptions The filter options.
|
||||
* @param array $selectedoptions The list of selected filter option values.
|
||||
* @param string|moodle_url $baseurl The url with params needed to call up this page.
|
||||
*/
|
||||
public function __construct($filteroptions, $selectedoptions, $baseurl = null) {
|
||||
$this->filteroptions = $filteroptions;
|
||||
$this->selectedoptions = $selectedoptions;
|
||||
if (!empty($baseurl)) {
|
||||
$this->baseurl = new moodle_url($baseurl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to export the renderer data in a format that is suitable for a mustache template.
|
||||
*
|
||||
* @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
|
||||
* @return stdClass|array
|
||||
*/
|
||||
public function export_for_template(renderer_base $output) {
|
||||
global $PAGE;
|
||||
$data = new stdClass();
|
||||
if (empty($this->baseurl)) {
|
||||
$this->baseurl = $PAGE->url;
|
||||
}
|
||||
$data->action = $this->baseurl->out(false);
|
||||
|
||||
foreach ($this->selectedoptions as $option) {
|
||||
if (!isset($this->filteroptions[$option])) {
|
||||
$this->filteroptions[$option] = $option;
|
||||
}
|
||||
}
|
||||
|
||||
$data->filteroptions = [];
|
||||
foreach ($this->filteroptions as $value => $label) {
|
||||
$selected = in_array($value, $this->selectedoptions);
|
||||
$filteroption = (object)[
|
||||
'value' => $value,
|
||||
'label' => $label
|
||||
];
|
||||
$filteroption->selected = $selected;
|
||||
$data->filteroptions[] = $filteroption;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -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,8 +52,14 @@ 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 {
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
|
||||
// This plugin has some sitewide user preferences to export.
|
||||
\core_privacy\local\request\user_preference_provider {
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
*
|
||||
@@ -70,6 +78,12 @@ class provider implements
|
||||
],
|
||||
'privacy:metadata:request'
|
||||
);
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -91,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.
|
||||
*
|
||||
@@ -127,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.
|
||||
@@ -162,4 +204,55 @@ 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.
|
||||
*
|
||||
* @param int $userid The userid of the user whose data is to be exported.
|
||||
*/
|
||||
public static function export_user_preferences(int $userid) {
|
||||
$preffilter = get_user_preferences(tool_helper::PREF_REQUEST_FILTERS, null, $userid);
|
||||
if ($preffilter !== null) {
|
||||
$filters = json_decode($preffilter);
|
||||
$descriptions = [];
|
||||
foreach ($filters as $filter) {
|
||||
list($category, $value) = explode(':', $filter);
|
||||
$option = new stdClass();
|
||||
switch($category) {
|
||||
case tool_helper::FILTER_TYPE:
|
||||
$option->category = get_string('requesttype', 'tool_dataprivacy');
|
||||
$option->name = tool_helper::get_shortened_request_type_string($value);
|
||||
break;
|
||||
case tool_helper::FILTER_STATUS:
|
||||
$option->category = get_string('requeststatus', 'tool_dataprivacy');
|
||||
$option->name = tool_helper::get_request_status_string($value);
|
||||
break;
|
||||
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);
|
||||
}
|
||||
// Export the filter preference as comma-separated values and text descriptions.
|
||||
$values = implode(', ', $filters);
|
||||
$descriptionstext = implode(', ', $descriptions);
|
||||
writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_FILTERS, $values, $descriptionstext);
|
||||
}
|
||||
|
||||
$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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class purpose extends \core\persistent {
|
||||
// Replicate self::read.
|
||||
$this->from_record($data);
|
||||
|
||||
// Using validate() as self::$validated is private.
|
||||
// Validate the purpose record.
|
||||
$this->validate();
|
||||
|
||||
// Now replicate the parent constructor.
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Scheduled task to delete files and update statuses of expired data requests.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Michael Hawkins
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_dataprivacy\task;
|
||||
|
||||
use coding_exception;
|
||||
use core\task\scheduled_task;
|
||||
use tool_dataprivacy\api;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
/**
|
||||
* Scheduled task to delete files and update request statuses once they expire.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Michael Hawkins
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class delete_expired_requests extends scheduled_task {
|
||||
|
||||
/**
|
||||
* Returns the task name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name() {
|
||||
return get_string('deleteexpireddatarequeststask', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the task to delete expired data request files and update request statuses.
|
||||
*
|
||||
*/
|
||||
public function execute() {
|
||||
$expiredrequests = \tool_dataprivacy\data_request::get_expired_requests();
|
||||
$deletecount = count($expiredrequests);
|
||||
|
||||
if ($deletecount > 0) {
|
||||
\tool_dataprivacy\data_request::expire($expiredrequests);
|
||||
|
||||
mtrace($deletecount . ' expired completed data requests have been deleted');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class initiate_data_request_task extends adhoc_task {
|
||||
public function execute() {
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/admin/tool/dataprivacy/lib.php');
|
||||
require_once($CFG->dirroot . "/{$CFG->admin}/tool/dataprivacy/lib.php");
|
||||
|
||||
if (!isset($this->get_custom_data()->requestid)) {
|
||||
throw new coding_exception('The custom data \'requestid\' is required.');
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class process_data_request_task extends adhoc_task {
|
||||
public function execute() {
|
||||
global $CFG, $PAGE, $SITE;
|
||||
|
||||
require_once($CFG->dirroot . '/admin/tool/dataprivacy/lib.php');
|
||||
require_once($CFG->dirroot . "/{$CFG->admin}/tool/dataprivacy/lib.php");
|
||||
|
||||
if (!isset($this->get_custom_data()->requestid)) {
|
||||
throw new coding_exception('The custom data \'requestid\' is required.');
|
||||
@@ -74,15 +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);
|
||||
|
||||
@@ -105,7 +119,7 @@ class process_data_request_task extends adhoc_task {
|
||||
$filerecord->author = fullname($foruser);
|
||||
// Save somewhere.
|
||||
$thing = $fs->create_file_from_pathname($filerecord, $exportedcontent);
|
||||
|
||||
$completestatus = api::DATAREQUEST_STATUS_DOWNLOAD_READY;
|
||||
} else if ($request->type == api::DATAREQUEST_TYPE_DELETE) {
|
||||
// Get the collection of approved_contextlist objects needed for core_privacy data deletion.
|
||||
$approvedclcollection = api::get_approved_contextlist_collection_for_request($requestpersistent);
|
||||
@@ -115,10 +129,12 @@ class process_data_request_task extends adhoc_task {
|
||||
$manager->set_observer(new \tool_dataprivacy\manager_observer());
|
||||
|
||||
$manager->delete_data_for_user($approvedclcollection);
|
||||
$completestatus = api::DATAREQUEST_STATUS_DELETED;
|
||||
$deleteuser = !$foruser->deleted;
|
||||
}
|
||||
|
||||
// When the preparation of the metadata finishes, update the request status to awaiting approval.
|
||||
api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE);
|
||||
api::update_request_status($requestid, $completestatus);
|
||||
mtrace('The processing of the user data request has been completed...');
|
||||
|
||||
// Create message to notify the user regarding the processing results.
|
||||
@@ -139,8 +155,17 @@ class process_data_request_task extends adhoc_task {
|
||||
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
$emailonly = false;
|
||||
$notifyuser = true;
|
||||
switch ($request->type) {
|
||||
case api::DATAREQUEST_TYPE_EXPORT:
|
||||
// Check if the user is allowed to download their own export. (This is for
|
||||
// institutions which centrally co-ordinate subject access request across many
|
||||
// systems, not just one Moodle instance, so we don't want every instance emailing
|
||||
// the user.)
|
||||
if (!api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->userid)) {
|
||||
$notifyuser = false;
|
||||
}
|
||||
|
||||
$typetext = get_string('requesttypeexport', 'tool_dataprivacy');
|
||||
// We want to notify the user in Moodle about the processing results.
|
||||
$message->notification = 1;
|
||||
@@ -179,18 +204,47 @@ class process_data_request_task extends adhoc_task {
|
||||
$message->fullmessagehtml = $messagehtml;
|
||||
|
||||
// Send message to the user involved.
|
||||
if ($emailonly) {
|
||||
email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
|
||||
} else {
|
||||
message_send($message);
|
||||
}
|
||||
mtrace('Message sent to user: ' . $messagetextdata['username']);
|
||||
if ($notifyuser) {
|
||||
$messagesent = false;
|
||||
if ($emailonly) {
|
||||
// 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 {
|
||||
$messagesent = message_send($message);
|
||||
}
|
||||
|
||||
if ($messagesent) {
|
||||
mtrace('Message sent to user: ' . $messagetextdata['username']);
|
||||
}
|
||||
}
|
||||
|
||||
// Send to requester as well in some circumstances.
|
||||
if ($foruser->id != $request->requestedby) {
|
||||
$sendtorequester = false;
|
||||
switch ($request->type) {
|
||||
case api::DATAREQUEST_TYPE_EXPORT:
|
||||
// Send to the requester as well if they can download it, unless they are the
|
||||
// DPO. If we didn't notify the user themselves (because they can't download)
|
||||
// then send to requester even if it is the DPO, as in that case the requester
|
||||
// needs to take some action.
|
||||
if (api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->requestedby)) {
|
||||
$sendtorequester = !$notifyuser || !api::is_site_dpo($request->requestedby);
|
||||
}
|
||||
break;
|
||||
case api::DATAREQUEST_TYPE_DELETE:
|
||||
// Send to the requester if they are not the DPO and if they are allowed to
|
||||
// create data requests for the user (e.g. Parent).
|
||||
$sendtorequester = !api::is_site_dpo($request->requestedby) &&
|
||||
api::can_create_data_request_for_user($request->userid, $request->requestedby);
|
||||
break;
|
||||
default:
|
||||
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
// Send to requester as well if this request was made on behalf of another user who's not a DPO,
|
||||
// and has the capability to make data requests for the user (e.g. Parent).
|
||||
if (!api::is_site_dpo($request->requestedby) && $foruser->id != $request->requestedby) {
|
||||
// Ensure the requester has the capability to make data requests for this user.
|
||||
if (api::can_create_data_request_for_user($request->userid, $request->requestedby)) {
|
||||
if ($sendtorequester) {
|
||||
$requestedby = core_user::get_user($request->requestedby);
|
||||
$message->userto = $requestedby;
|
||||
$messagetextdata['username'] = fullname($requestedby);
|
||||
@@ -209,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);
|
||||
|
||||
@@ -53,11 +53,13 @@ if ($manage) {
|
||||
$PAGE->set_context($context);
|
||||
|
||||
// If contactdataprotectionofficer is disabled, send the user back to the profile page, or the privacy policy page.
|
||||
if (!\tool_dataprivacy\api::can_contact_dpo()) {
|
||||
redirect($returnurl, get_string('contactdpoviaprivacypolicy', 'tool_dataprivacy'), \core\output\notification::NOTIFY_ERROR);
|
||||
// That is, unless you have sufficient capabilities to perform this on behalf of a user.
|
||||
if (!$manage && !\tool_dataprivacy\api::can_contact_dpo()) {
|
||||
redirect($returnurl, get_string('contactdpoviaprivacypolicy', 'tool_dataprivacy'), 0, \core\output\notification::NOTIFY_ERROR);
|
||||
}
|
||||
|
||||
$mform = new tool_dataprivacy_data_request_form($url->out(false), ['manage' => !empty($manage)]);
|
||||
$mform->set_data(['type' => $requesttype]);
|
||||
|
||||
// Data request cancelled.
|
||||
if ($mform->is_cancelled()) {
|
||||
@@ -66,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) {
|
||||
@@ -77,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);
|
||||
|
||||
@@ -44,19 +44,28 @@ class tool_dataprivacy_data_request_form extends moodleform {
|
||||
/**
|
||||
* Form definition.
|
||||
*
|
||||
* @throws HTML_QuickForm_Error
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public function definition() {
|
||||
global $DB, $USER;
|
||||
global $USER;
|
||||
$mform =& $this->_form;
|
||||
|
||||
$this->manage = $this->_customdata['manage'];
|
||||
if ($this->manage) {
|
||||
$options = [
|
||||
'ajax' => 'tool_dataprivacy/form-user-selector',
|
||||
'multiple' => false
|
||||
'valuehtmlcallback' => function($value) {
|
||||
global $OUTPUT;
|
||||
|
||||
$allusernames = get_all_user_name_fields(true);
|
||||
$fields = 'id, email, ' . $allusernames;
|
||||
$user = \core_user::get_user($value, $fields);
|
||||
$useroptiondata = [
|
||||
'fullname' => fullname($user),
|
||||
'email' => $user->email
|
||||
];
|
||||
return $OUTPUT->render_from_template('tool_dataprivacy/form-user-selector-suggestion', $useroptiondata);
|
||||
}
|
||||
];
|
||||
$mform->addElement('autocomplete', 'userid', get_string('requestfor', 'tool_dataprivacy'), [], $options);
|
||||
$mform->addRule('userid', null, 'required', null, 'client');
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$filter = optional_param('filter', CONTEXT_COURSE, PARAM_INT);
|
||||
|
||||
$url = new moodle_url('/admin/tool/dataprivacy/datadeletion.php');
|
||||
@@ -34,14 +36,21 @@ $title = get_string('datadeletion', 'tool_dataprivacy');
|
||||
\tool_dataprivacy\page_helper::setup($url, $title);
|
||||
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading($title);
|
||||
|
||||
$table = new \tool_dataprivacy\output\expired_contexts_table($filter);
|
||||
$table->baseurl = $url;
|
||||
$table->baseurl->param('filter', $filter);
|
||||
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
|
||||
$table = new \tool_dataprivacy\output\expired_contexts_table($filter);
|
||||
$table->baseurl = $url;
|
||||
$table->baseurl->param('filter', $filter);
|
||||
|
||||
$datadeletionpage = new \tool_dataprivacy\output\data_deletion_page($filter, $table);
|
||||
$datadeletionpage = new \tool_dataprivacy\output\data_deletion_page($filter, $table);
|
||||
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->render($datadeletionpage);
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->render($datadeletionpage);
|
||||
} else {
|
||||
$dponamestring = implode (',', tool_dataprivacy\api::get_dpo_role_names());
|
||||
$message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring);
|
||||
echo $OUTPUT->notification($message, 'error');
|
||||
}
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$contextlevel = optional_param('contextlevel', CONTEXT_SYSTEM, PARAM_INT);
|
||||
$contextid = optional_param('contextid', 0, PARAM_INT);
|
||||
|
||||
@@ -35,8 +37,14 @@ $title = get_string('dataregistry', 'tool_dataprivacy');
|
||||
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->header();
|
||||
echo $OUTPUT->heading($title);
|
||||
|
||||
$dataregistry = new tool_dataprivacy\output\data_registry_page($contextlevel, $contextid);
|
||||
|
||||
echo $output->render($dataregistry);
|
||||
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
|
||||
$dataregistry = new tool_dataprivacy\output\data_registry_page($contextlevel, $contextid);
|
||||
echo $output->render($dataregistry);
|
||||
} else {
|
||||
$dponamestring = implode (', ', tool_dataprivacy\api::get_dpo_role_names());
|
||||
$message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring);
|
||||
echo $OUTPUT->notification($message, 'error');
|
||||
}
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
require_once("../../../config.php");
|
||||
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');
|
||||
@@ -34,9 +38,60 @@ $title = get_string('datarequests', 'tool_dataprivacy');
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading($title);
|
||||
|
||||
$requests = tool_dataprivacy\api::get_data_requests();
|
||||
$requestlist = new tool_dataprivacy\output\data_requests_page($requests);
|
||||
$requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $requestlistoutput->render($requestlist);
|
||||
if (!\tool_dataprivacy\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);
|
||||
if ($filtersapplied === [-1]) {
|
||||
// If there are no filters submitted, check if there is a saved filters from the user preferences.
|
||||
$filterprefs = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, null);
|
||||
if ($filterprefs && empty($filterscleared)) {
|
||||
$filtersapplied = json_decode($filterprefs);
|
||||
} else {
|
||||
$filtersapplied = [];
|
||||
}
|
||||
}
|
||||
// Save the current applied filters to the user preferences.
|
||||
set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, json_encode($filtersapplied));
|
||||
|
||||
$types = [];
|
||||
$statuses = [];
|
||||
$creationmethods = [];
|
||||
foreach ($filtersapplied as $filter) {
|
||||
list($category, $value) = explode(':', $filter);
|
||||
switch($category) {
|
||||
case \tool_dataprivacy\local\helper::FILTER_TYPE:
|
||||
$types[] = $value;
|
||||
break;
|
||||
case \tool_dataprivacy\local\helper::FILTER_STATUS:
|
||||
$statuses[] = $value;
|
||||
break;
|
||||
case \tool_dataprivacy\local\helper::FILTER_CREATION:
|
||||
$creationmethods[] = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$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);
|
||||
$requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $requestlistoutput->render($requestlist);
|
||||
} else {
|
||||
$dponamestring = implode (', ', tool_dataprivacy\api::get_dpo_role_names());
|
||||
$message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring);
|
||||
echo $OUTPUT->notification($message, 'error');
|
||||
}
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
@@ -49,4 +49,22 @@ $capabilities = [
|
||||
'contextlevel' => CONTEXT_USER,
|
||||
'archetypes' => []
|
||||
],
|
||||
|
||||
// Capability for users to download the results of their own data request.
|
||||
'tool/dataprivacy:downloadownrequest' => [
|
||||
'riskbitmask' => 0,
|
||||
'captype' => 'read',
|
||||
'contextlevel' => CONTEXT_USER,
|
||||
'archetypes' => [
|
||||
'user' => CAP_ALLOW
|
||||
]
|
||||
],
|
||||
|
||||
// Capability for administrators to download other people's data requests.
|
||||
'tool/dataprivacy:downloadallrequests' => [
|
||||
'riskbitmask' => RISK_PERSONAL,
|
||||
'captype' => 'read',
|
||||
'contextlevel' => CONTEXT_USER,
|
||||
'archetypes' => []
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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="20180313" 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"
|
||||
>
|
||||
@@ -12,13 +12,14 @@
|
||||
<FIELD NAME="commentsformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID the request is being made for"/>
|
||||
<FIELD NAME="requestedby" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID of the one making the request"/>
|
||||
<FIELD NAME="status" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The current status of the data request"/>
|
||||
<FIELD NAME="status" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The current status of the data request"/>
|
||||
<FIELD NAME="dpo" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID of the Data Protection Officer who is reviewing th request"/>
|
||||
<FIELD NAME="dpocomment" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="DPO's comments (e.g. reason for rejecting the request, etc.)"/>
|
||||
<FIELD NAME="dpocommentformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<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,7 +99,10 @@
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="status" TYPE="int" LENGTH="2" DEFAULT="0" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="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"/>
|
||||
<FIELD NAME="timemodified" 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>
|
||||
</XMLDB>
|
||||
@@ -43,6 +43,16 @@ $functions = [
|
||||
'ajax' => true,
|
||||
'loginrequired' => true,
|
||||
],
|
||||
'tool_dataprivacy_mark_complete' => [
|
||||
'classname' => 'tool_dataprivacy\external',
|
||||
'methodname' => 'mark_complete',
|
||||
'classpath' => '',
|
||||
'description' => 'Mark a user\'s general enquiry as complete',
|
||||
'type' => 'write',
|
||||
'capabilities' => 'tool/dataprivacy:managedatarequests',
|
||||
'ajax' => true,
|
||||
'loginrequired' => true,
|
||||
],
|
||||
'tool_dataprivacy_get_data_request' => [
|
||||
'classname' => 'tool_dataprivacy\external',
|
||||
'methodname' => 'get_data_request',
|
||||
@@ -63,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',
|
||||
@@ -73,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',
|
||||
@@ -163,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,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -42,5 +42,22 @@ $tasks = array(
|
||||
'day' => '*',
|
||||
'dayofweek' => '*',
|
||||
'month' => '*'
|
||||
), array(
|
||||
'classname' => 'tool_dataprivacy\task\delete_expired_requests',
|
||||
'blocking' => 0,
|
||||
'minute' => 'R',
|
||||
'hour' => 'R',
|
||||
'day' => '*',
|
||||
'dayofweek' => '*',
|
||||
'month' => '*'
|
||||
), array(
|
||||
'classname' => 'tool_dataprivacy\task\delete_existing_deleted_users',
|
||||
'blocking' => 0,
|
||||
'minute' => 'R',
|
||||
'hour' => 'R',
|
||||
'day' => '*',
|
||||
'dayofweek' => '*',
|
||||
'month' => '*',
|
||||
'disabled' => true,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* tool_dataprivacy plugin upgrade code
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Jun Pataleta
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Function to upgrade tool_dataprivacy.
|
||||
*
|
||||
* @param int $oldversion the version we are upgrading from
|
||||
* @return bool result
|
||||
*/
|
||||
function xmldb_tool_dataprivacy_upgrade($oldversion) {
|
||||
global $CFG, $DB;
|
||||
|
||||
$dbman = $DB->get_manager();
|
||||
|
||||
if ($oldversion < 2018051405) {
|
||||
// Define table tool_dataprivacy_ctxexpired to be created.
|
||||
$table = new xmldb_table('tool_dataprivacy_ctxexpired');
|
||||
|
||||
// Adding fields to table tool_dataprivacy_ctxexpired.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('status', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
|
||||
$table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
|
||||
// Adding keys to table tool_dataprivacy_ctxexpired.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
|
||||
$table->add_key('contextid', XMLDB_KEY_FOREIGN_UNIQUE, array('contextid'), 'context', array('id'));
|
||||
|
||||
// Conditionally launch create table for tool_dataprivacy_ctxexpired.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Define table tool_dataprivacy_contextlist to be created.
|
||||
$table = new xmldb_table('tool_dataprivacy_contextlist');
|
||||
|
||||
// Adding fields to table tool_dataprivacy_contextlist.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('component', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
|
||||
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
|
||||
|
||||
// Adding keys to table tool_dataprivacy_contextlist.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
|
||||
|
||||
// Conditionally launch create table for tool_dataprivacy_contextlist.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Define table tool_dataprivacy_ctxlst_ctx to be created.
|
||||
$table = new xmldb_table('tool_dataprivacy_ctxlst_ctx');
|
||||
|
||||
// Adding fields to table tool_dataprivacy_ctxlst_ctx.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('contextlistid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('status', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
|
||||
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
|
||||
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
|
||||
|
||||
// Adding keys to table tool_dataprivacy_ctxlst_ctx.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
|
||||
$table->add_key('contextlistid', XMLDB_KEY_FOREIGN, array('contextlistid'), 'tool_dataprivacy_contextlist', array('id'));
|
||||
|
||||
// Conditionally launch create table for tool_dataprivacy_ctxlst_ctx.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Define table tool_dataprivacy_rqst_ctxlst to be created.
|
||||
$table = new xmldb_table('tool_dataprivacy_rqst_ctxlst');
|
||||
|
||||
// Adding fields to table tool_dataprivacy_rqst_ctxlst.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('requestid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('contextlistid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
|
||||
// Adding keys to table tool_dataprivacy_rqst_ctxlst.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
|
||||
$table->add_key('requestid', XMLDB_KEY_FOREIGN, array('requestid'), 'tool_dataprivacy_request', array('id'));
|
||||
$table->add_key('contextlistid', XMLDB_KEY_FOREIGN, array('contextlistid'), 'tool_dataprivacy_contextlist', array('id'));
|
||||
$table->add_key('request_contextlist', XMLDB_KEY_UNIQUE, array('requestid', 'contextlistid'));
|
||||
|
||||
// Conditionally launch create table for tool_dataprivacy_rqst_ctxlst.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Define field lawfulbases to be added to tool_dataprivacy_purpose.
|
||||
$table = new xmldb_table('tool_dataprivacy_purpose');
|
||||
|
||||
// It is a required field. We initially define and add it as null and later update it to XMLDB_NOTNULL.
|
||||
$field = new xmldb_field('lawfulbases', XMLDB_TYPE_TEXT, null, null, null, null, null, 'descriptionformat');
|
||||
|
||||
// Conditionally launch add field lawfulbases.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
|
||||
// Set a kind-of-random value to lawfulbasis field.
|
||||
$DB->set_field('tool_dataprivacy_purpose', 'lawfulbases', 'gdpr_art_6_1_a');
|
||||
|
||||
// We redefine it now as not null.
|
||||
$field = new xmldb_field('lawfulbases', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null, 'descriptionformat');
|
||||
|
||||
// Launch change of nullability for field lawfulbases.
|
||||
$dbman->change_field_notnull($table, $field);
|
||||
}
|
||||
|
||||
// Define field sensitivedatareasons to be added to tool_dataprivacy_purpose.
|
||||
$table = new xmldb_table('tool_dataprivacy_purpose');
|
||||
$field = new xmldb_field('sensitivedatareasons', XMLDB_TYPE_TEXT, null, null, null, null, null, 'lawfulbases');
|
||||
|
||||
// Conditionally launch add field sensitivedatareasons.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Dataprivacy savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2018051405, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
if ($oldversion < 2018051406) {
|
||||
// Update completed delete requests to new delete status.
|
||||
$query = "UPDATE {tool_dataprivacy_request}
|
||||
SET status = :setstatus
|
||||
WHERE type = :type
|
||||
AND status = :wherestatus";
|
||||
$params = array(
|
||||
'setstatus' => 10, // Request deleted.
|
||||
'type' => 2, // Delete type.
|
||||
'wherestatus' => 5, // Request completed.
|
||||
);
|
||||
|
||||
$DB->execute($query, $params);
|
||||
|
||||
// Update completed data export requests to new download ready status.
|
||||
$params = array(
|
||||
'setstatus' => 8, // Request download ready.
|
||||
'type' => 1, // export type.
|
||||
'wherestatus' => 5, // Request completed.
|
||||
);
|
||||
|
||||
$DB->execute($query, $params);
|
||||
|
||||
upgrade_plugin_savepoint(true, 2018051406, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
if ($oldversion < 2018051407) {
|
||||
|
||||
// Changing precision of field status on table tool_dataprivacy_request to (2).
|
||||
$table = new xmldb_table('tool_dataprivacy_request');
|
||||
$field = new xmldb_field('status', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'requestedby');
|
||||
|
||||
// Launch change of precision for field status.
|
||||
$dbman->change_field_precision($table, $field);
|
||||
|
||||
// Dataprivacy savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2018051407, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
if ($oldversion < 2018051411) {
|
||||
// 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, 2018051411, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
if ($oldversion < 2018051413) {
|
||||
// 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, 2018051413, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
if ($oldversion < 2018051415) {
|
||||
// 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, 2018051415, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -25,52 +25,47 @@
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$url = new \moodle_url('/admin/tool/dataprivacy/defaults.php');
|
||||
$title = get_string('setdefaults', 'tool_dataprivacy');
|
||||
|
||||
\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();
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$id = optional_param('id', 0, PARAM_INT);
|
||||
|
||||
$url = new \moodle_url('/admin/tool/dataprivacy/editcategory.php', array('id' => $id));
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
|
||||
require_login(null, false);
|
||||
|
||||
$id = optional_param('id', 0, PARAM_INT);
|
||||
|
||||
$url = new \moodle_url('/admin/tool/dataprivacy/editpurpose.php', array('id' => $id));
|
||||
@@ -42,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);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user