diff --git a/backend/Pipfile b/backend/Pipfile index 261640be..372cfc58 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -7,7 +7,7 @@ name = "pypi" # Before adding a new package, check it's not listed under [packages] at # https://github.com/ocadotechnology/codeforlife-package-python/blob/{ref}/Pipfile # Replace "{ref}" in the above URL with the ref set below. -codeforlife = {ref = "v0.13.9", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} +codeforlife = {ref = "v0.13.10", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} # TODO: check if we need the below packages whitenoise = "==6.5.0" django-pipeline = "==2.0.8" @@ -34,7 +34,7 @@ google-cloud-container = "==2.3.0" # Before adding a new package, check it's not listed under [dev-packages] at # https://github.com/ocadotechnology/codeforlife-package-python/blob/{ref}/Pipfile # Replace "{ref}" in the above URL with the ref set below. -codeforlife = {ref = "v0.13.9", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]} +codeforlife = {ref = "v0.13.10", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]} # TODO: check if we need the below packages django-selenium-clean = "==0.3.3" django-test-migrations = "==1.2.0" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index fbdefb16..5bba802a 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "133387961394ddba0202782e2bb4265b005cd74c06be1456cf8cae8c3df2a832" + "sha256": "c3b11b9d8699621ec2071fed98072adb143c11ae3dd29b642c565fb491add82d" }, "pipfile-spec": 6, "requires": { @@ -168,7 +168,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "cd70f4fc980eee04a029402029a1dfb52a0f1e3e" + "ref": "9f3c8292be7400cef9255cdf6283f9bb7edea955" }, "codeforlife-portal": { "hashes": [ @@ -501,62 +501,62 @@ }, "grpcio": { "hashes": [ - "sha256:0250a7a70b14000fa311de04b169cc7480be6c1a769b190769d347939d3232a8", - "sha256:069fe2aeee02dfd2135d562d0663fe70fbb69d5eed6eb3389042a7e963b54de8", - "sha256:082081e6a36b6eb5cf0fd9a897fe777dbb3802176ffd08e3ec6567edd85bc104", - "sha256:0c5807e9152eff15f1d48f6b9ad3749196f79a4a050469d99eecb679be592acc", - "sha256:14e8f2c84c0832773fb3958240c69def72357bc11392571f87b2d7b91e0bb092", - "sha256:2a6087f234cb570008a6041c8ffd1b7d657b397fdd6d26e83d72283dae3527b1", - "sha256:2bb2a2911b028f01c8c64d126f6b632fcd8a9ac975aa1b3855766c94e4107180", - "sha256:2f44c32aef186bbba254129cea1df08a20be414144ac3bdf0e84b24e3f3b2e05", - "sha256:30e980cd6db1088c144b92fe376747328d5554bc7960ce583ec7b7d81cd47287", - "sha256:33aed0a431f5befeffd9d346b0fa44b2c01aa4aeae5ea5b2c03d3e25e0071216", - "sha256:33bdea30dcfd4f87b045d404388469eb48a48c33a6195a043d116ed1b9a0196c", - "sha256:39aa848794b887120b1d35b1b994e445cc028ff602ef267f87c38122c1add50d", - "sha256:4216e67ad9a4769117433814956031cb300f85edc855252a645a9a724b3b6594", - "sha256:49c9b6a510e3ed8df5f6f4f3c34d7fbf2d2cae048ee90a45cd7415abab72912c", - "sha256:4eec8b8c1c2c9b7125508ff7c89d5701bf933c99d3910e446ed531cd16ad5d87", - "sha256:50d56280b482875d1f9128ce596e59031a226a8b84bec88cb2bf76c289f5d0de", - "sha256:53b69e79d00f78c81eecfb38f4516080dc7f36a198b6b37b928f1c13b3c063e9", - "sha256:55ccb7db5a665079d68b5c7c86359ebd5ebf31a19bc1a91c982fd622f1e31ff2", - "sha256:5a1ebbae7e2214f51b1f23b57bf98eeed2cf1ba84e4d523c48c36d5b2f8829ff", - "sha256:61b7199cd2a55e62e45bfb629a35b71fc2c0cb88f686a047f25b1112d3810904", - "sha256:660fc6b9c2a9ea3bb2a7e64ba878c98339abaf1811edca904ac85e9e662f1d73", - "sha256:6d140bdeb26cad8b93c1455fa00573c05592793c32053d6e0016ce05ba267549", - "sha256:6e490fa5f7f5326222cb9f0b78f207a2b218a14edf39602e083d5f617354306f", - "sha256:6ecf21d20d02d1733e9c820fb5c114c749d888704a7ec824b545c12e78734d1c", - "sha256:70c83bb530572917be20c21f3b6be92cd86b9aecb44b0c18b1d3b2cc3ae47df0", - "sha256:72153a0d2e425f45b884540a61c6639436ddafa1829a42056aa5764b84108b8e", - "sha256:73e14acd3d4247169955fae8fb103a2b900cfad21d0c35f0dcd0fdd54cd60367", - "sha256:76eaaba891083fcbe167aa0f03363311a9f12da975b025d30e94b93ac7a765fc", - "sha256:79ae0dc785504cb1e1788758c588c711f4e4a0195d70dff53db203c95a0bd303", - "sha256:7d142bcd604166417929b071cd396aa13c565749a4c840d6c702727a59d835eb", - "sha256:8c9554ca8e26241dabe7951aa1fa03a1ba0856688ecd7e7bdbdd286ebc272e4c", - "sha256:8d488fbdbf04283f0d20742b64968d44825617aa6717b07c006168ed16488804", - "sha256:91422ba785a8e7a18725b1dc40fbd88f08a5bb4c7f1b3e8739cab24b04fa8a03", - "sha256:9a66f4d2a005bc78e61d805ed95dedfcb35efa84b7bba0403c6d60d13a3de2d6", - "sha256:9b106bc52e7f28170e624ba61cc7dc6829566e535a6ec68528f8e1afbed1c41f", - "sha256:9b54577032d4f235452f77a83169b6527bf4b77d73aeada97d45b2aaf1bf5ce0", - "sha256:a09506eb48fa5493c58f946c46754ef22f3ec0df64f2b5149373ff31fb67f3dd", - "sha256:a212e5dea1a4182e40cd3e4067ee46be9d10418092ce3627475e995cca95de21", - "sha256:a731ac5cffc34dac62053e0da90f0c0b8560396a19f69d9703e88240c8f05858", - "sha256:af5ef6cfaf0d023c00002ba25d0751e5995fa0e4c9eec6cd263c30352662cbce", - "sha256:b58b855d0071575ea9c7bc0d84a06d2edfbfccec52e9657864386381a7ce1ae9", - "sha256:bc808924470643b82b14fe121923c30ec211d8c693e747eba8a7414bc4351a23", - "sha256:c557e94e91a983e5b1e9c60076a8fd79fea1e7e06848eb2e48d0ccfb30f6e073", - "sha256:c71be3f86d67d8d1311c6076a4ba3b75ba5703c0b856b4e691c9097f9b1e8bd2", - "sha256:c8754c75f55781515a3005063d9a05878b2cfb3cb7e41d5401ad0cf19de14872", - "sha256:cb0af13433dbbd1c806e671d81ec75bd324af6ef75171fd7815ca3074fe32bfe", - "sha256:cba6209c96828711cb7c8fcb45ecef8c8859238baf15119daa1bef0f6c84bfe7", - "sha256:cf77f8cf2a651fbd869fbdcb4a1931464189cd210abc4cfad357f1cacc8642a6", - "sha256:d7404cebcdb11bb5bd40bf94131faf7e9a7c10a6c60358580fe83913f360f929", - "sha256:dd1d3a8d1d2e50ad9b59e10aa7f07c7d1be2b367f3f2d33c5fade96ed5460962", - "sha256:e5d97c65ea7e097056f3d1ead77040ebc236feaf7f71489383d20f3b4c28412a", - "sha256:f1c3dc536b3ee124e8b24feb7533e5c70b9f2ef833e3b2e5513b2897fd46763a", - "sha256:f2212796593ad1d0235068c79836861f2201fc7137a99aa2fea7beeb3b101177", - "sha256:fead980fbc68512dfd4e0c7b1f5754c2a8e5015a04dea454b9cada54a8423525" - ], - "version": "==1.60.1" + "sha256:0b9179478b09ee22f4a36b40ca87ad43376acdccc816ce7c2193a9061bf35701", + "sha256:0d3dee701e48ee76b7d6fbbba18ba8bc142e5b231ef7d3d97065204702224e0e", + "sha256:0d7ae7fc7dbbf2d78d6323641ded767d9ec6d121aaf931ec4a5c50797b886532", + "sha256:0e97f37a3b7c89f9125b92d22e9c8323f4e76e7993ba7049b9f4ccbe8bae958a", + "sha256:136ffd79791b1eddda8d827b607a6285474ff8a1a5735c4947b58c481e5e4271", + "sha256:1bc8449084fe395575ed24809752e1dc4592bb70900a03ca42bf236ed5bf008f", + "sha256:1eda79574aec8ec4d00768dcb07daba60ed08ef32583b62b90bbf274b3c279f7", + "sha256:29cb592c4ce64a023712875368bcae13938c7f03e99f080407e20ffe0a9aa33b", + "sha256:2c1488b31a521fbba50ae86423f5306668d6f3a46d124f7819c603979fc538c4", + "sha256:2e84bfb2a734e4a234b116be208d6f0214e68dcf7804306f97962f93c22a1839", + "sha256:2f3d9a4d0abb57e5f49ed5039d3ed375826c2635751ab89dcc25932ff683bbb6", + "sha256:36df33080cd7897623feff57831eb83c98b84640b016ce443305977fac7566fb", + "sha256:38f69de9c28c1e7a8fd24e4af4264726637b72f27c2099eaea6e513e7142b47e", + "sha256:39cd45bd82a2e510e591ca2ddbe22352e8413378852ae814549c162cf3992a93", + "sha256:3fa15850a6aba230eed06b236287c50d65a98f05054a0f01ccedf8e1cc89d57f", + "sha256:4cd356211579043fce9f52acc861e519316fff93980a212c8109cca8f47366b6", + "sha256:56ca7ba0b51ed0de1646f1735154143dcbdf9ec2dbe8cc6645def299bb527ca1", + "sha256:5e709f7c8028ce0443bddc290fb9c967c1e0e9159ef7a030e8c21cac1feabd35", + "sha256:614c3ed234208e76991992342bab725f379cc81c7dd5035ee1de2f7e3f7a9842", + "sha256:62aa1659d8b6aad7329ede5d5b077e3d71bf488d85795db517118c390358d5f6", + "sha256:62ccb92f594d3d9fcd00064b149a0187c246b11e46ff1b7935191f169227f04c", + "sha256:662d3df5314ecde3184cf87ddd2c3a66095b3acbb2d57a8cada571747af03873", + "sha256:748496af9238ac78dcd98cce65421f1adce28c3979393e3609683fcd7f3880d7", + "sha256:77d48e5b1f8f4204889f1acf30bb57c30378e17c8d20df5acbe8029e985f735c", + "sha256:7a195531828b46ea9c4623c47e1dc45650fc7206f8a71825898dd4c9004b0928", + "sha256:7e1f51e2a460b7394670fdb615e26d31d3260015154ea4f1501a45047abe06c9", + "sha256:7eea57444a354ee217fda23f4b479a4cdfea35fb918ca0d8a0e73c271e52c09c", + "sha256:7f9d6c3223914abb51ac564dc9c3782d23ca445d2864321b9059d62d47144021", + "sha256:81531632f93fece32b2762247c4c169021177e58e725494f9a746ca62c83acaa", + "sha256:81d444e5e182be4c7856cd33a610154fe9ea1726bd071d07e7ba13fafd202e38", + "sha256:821a44bd63d0f04e33cf4ddf33c14cae176346486b0df08b41a6132b976de5fc", + "sha256:88f41f33da3840b4a9bbec68079096d4caf629e2c6ed3a72112159d570d98ebe", + "sha256:8aab8f90b2a41208c0a071ec39a6e5dbba16fd827455aaa070fec241624ccef8", + "sha256:921148f57c2e4b076af59a815467d399b7447f6e0ee10ef6d2601eb1e9c7f402", + "sha256:92cdb616be44c8ac23a57cce0243af0137a10aa82234f23cd46e69e115071388", + "sha256:95370c71b8c9062f9ea033a0867c4c73d6f0ff35113ebd2618171ec1f1e903e0", + "sha256:98d8f4eb91f1ce0735bf0b67c3b2a4fea68b52b2fd13dc4318583181f9219b4b", + "sha256:a33f2bfd8a58a02aab93f94f6c61279be0f48f99fcca20ebaee67576cd57307b", + "sha256:ab140a3542bbcea37162bdfc12ce0d47a3cda3f2d91b752a124cc9fe6776a9e2", + "sha256:b3d3d755cfa331d6090e13aac276d4a3fb828bf935449dc16c3d554bf366136b", + "sha256:b71c65427bf0ec6a8b48c68c17356cb9fbfc96b1130d20a07cb462f4e4dcdcd5", + "sha256:b7a6be562dd18e5d5bec146ae9537f20ae1253beb971c0164f1e8a2f5a27e829", + "sha256:bcff647e7fe25495e7719f779cc219bbb90b9e79fbd1ce5bda6aae2567f469f2", + "sha256:c912688acc05e4ff012c8891803659d6a8a8b5106f0f66e0aed3fb7e77898fa6", + "sha256:ce1aafdf8d3f58cb67664f42a617af0e34555fe955450d42c19e4a6ad41c84bd", + "sha256:d6a56ba703be6b6267bf19423d888600c3f574ac7c2cc5e6220af90662a4d6b0", + "sha256:e803e9b58d8f9b4ff0ea991611a8d51b31c68d2e24572cd1fe85e99e8cc1b4f8", + "sha256:eef1d16ac26c5325e7d39f5452ea98d6988c700c427c52cbc7ce3201e6d93334", + "sha256:f359d635ee9428f0294bea062bb60c478a8ddc44b0b6f8e1f42997e5dc12e2ee", + "sha256:f4c04fe33039b35b97c02d2901a164bbbb2f21fb9c4e2a45a959f0b044c3512c", + "sha256:f897b16190b46bc4d4aaf0a32a4b819d559a37a756d7c6b571e9562c360eed72", + "sha256:fbe0c20ce9a1cff75cfb828b21f08d0a1ca527b67f2443174af6626798a754a4", + "sha256:fc2836cb829895ee190813446dce63df67e6ed7b9bf76060262c55fcd097d270", + "sha256:fcc98cff4084467839d0a20d16abc2a76005f3d1b38062464d088c07f500d170" + ], + "version": "==1.62.0" }, "grpcio-status": { "hashes": [ @@ -1514,7 +1514,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "cd70f4fc980eee04a029402029a1dfb52a0f1e3e" + "ref": "9f3c8292be7400cef9255cdf6283f9bb7edea955" }, "codeforlife-portal": { "hashes": [ @@ -1528,61 +1528,61 @@ "toml" ], "hashes": [ - "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61", - "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1", - "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7", - "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7", - "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75", - "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd", - "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35", - "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04", - "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6", - "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042", - "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166", - "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1", - "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d", - "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c", - "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66", - "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70", - "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1", - "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676", - "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630", - "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a", - "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74", - "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad", - "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19", - "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6", - "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448", - "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018", - "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218", - "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756", - "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54", - "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45", - "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628", - "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968", - "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d", - "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25", - "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60", - "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950", - "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06", - "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295", - "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b", - "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c", - "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc", - "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74", - "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1", - "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee", - "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011", - "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156", - "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766", - "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5", - "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581", - "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016", - "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c", - "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3" + "sha256:006d220ba2e1a45f1de083d5022d4955abb0aedd78904cd5a779b955b019ec73", + "sha256:06fe398145a2e91edaf1ab4eee66149c6776c6b25b136f4a86fcbbb09512fd10", + "sha256:175f56572f25e1e1201d2b3e07b71ca4d201bf0b9cb8fad3f1dfae6a4188de86", + "sha256:18cac867950943fe93d6cd56a67eb7dcd2d4a781a40f4c1e25d6f1ed98721a55", + "sha256:1a5ee18e3a8d766075ce9314ed1cb695414bae67df6a4b0805f5137d93d6f1cb", + "sha256:20a875bfd8c282985c4720c32aa05056f77a68e6d8bbc5fe8632c5860ee0b49b", + "sha256:2412e98e70f16243be41d20836abd5f3f32edef07cbf8f407f1b6e1ceae783ac", + "sha256:2599972b21911111114100d362aea9e70a88b258400672626efa2b9e2179609c", + "sha256:2ed37e16cf35c8d6e0b430254574b8edd242a367a1b1531bd1adc99c6a5e00fe", + "sha256:32b4ab7e6c924f945cbae5392832e93e4ceb81483fd6dc4aa8fb1a97b9d3e0e1", + "sha256:34423abbaad70fea9d0164add189eabaea679068ebdf693baa5c02d03e7db244", + "sha256:3507427d83fa961cbd73f11140f4a5ce84208d31756f7238d6257b2d3d868405", + "sha256:3733545eb294e5ad274abe131d1e7e7de4ba17a144505c12feca48803fea5f64", + "sha256:3ff5bdb08d8938d336ce4088ca1a1e4b6c8cd3bef8bb3a4c0eb2f37406e49643", + "sha256:3ff7f92ae5a456101ca8f48387fd3c56eb96353588e686286f50633a611afc95", + "sha256:42a9e754aa250fe61f0f99986399cec086d7e7a01dd82fd863a20af34cbce962", + "sha256:51593a1f05c39332f623d64d910445fdec3d2ac2d96b37ce7f331882d5678ddf", + "sha256:5b11f9c6587668e495cc7365f85c93bed34c3a81f9f08b0920b87a89acc13469", + "sha256:69f1665165ba2fe7614e2f0c1aed71e14d83510bf67e2ee13df467d1c08bf1e8", + "sha256:78cdcbf7b9cb83fe047ee09298e25b1cd1636824067166dc97ad0543b079d22f", + "sha256:7df95fdd1432a5d2675ce630fef5f239939e2b3610fe2f2b5bf21fa505256fa3", + "sha256:81a5fb41b0d24447a47543b749adc34d45a2cf77b48ca74e5bf3de60a7bd9edc", + "sha256:840456cb1067dc350af9080298c7c2cfdddcedc1cb1e0b30dceecdaf7be1a2d3", + "sha256:8562ca91e8c40864942615b1d0b12289d3e745e6b2da901d133f52f2d510a1e3", + "sha256:861d75402269ffda0b33af94694b8e0703563116b04c681b1832903fac8fd647", + "sha256:8b98c89db1b150d851a7840142d60d01d07677a18f0f46836e691c38134ed18b", + "sha256:a178b7b1ac0f1530bb28d2e51f88c0bab3e5949835851a60dda80bff6052510c", + "sha256:a8ddbd158e069dded57738ea69b9744525181e99974c899b39f75b2b29a624e2", + "sha256:ac4bab32f396b03ebecfcf2971668da9275b3bb5f81b3b6ba96622f4ef3f6e17", + "sha256:ac9e95cefcf044c98d4e2c829cd0669918585755dd9a92e28a1a7012322d0a95", + "sha256:adbdfcda2469d188d79771d5696dc54fab98a16d2ef7e0875013b5f56a251047", + "sha256:b3c8bbb95a699c80a167478478efe5e09ad31680931ec280bf2087905e3b95ec", + "sha256:b3f2b1eb229f23c82898eedfc3296137cf1f16bb145ceab3edfd17cbde273fb7", + "sha256:b4ae777bebaed89e3a7e80c4a03fac434a98a8abb5251b2a957d38fe3fd30088", + "sha256:b953275d4edfab6cc0ed7139fa773dfb89e81fee1569a932f6020ce7c6da0e8f", + "sha256:bf54c3e089179d9d23900e3efc86d46e4431188d9a657f345410eecdd0151f50", + "sha256:bf711d517e21fb5bc429f5c4308fbc430a8585ff2a43e88540264ae87871e36a", + "sha256:c00e54f0bd258ab25e7f731ca1d5144b0bf7bec0051abccd2bdcff65fa3262c9", + "sha256:c11ca2df2206a4e3e4c4567f52594637392ed05d7c7fb73b4ea1c658ba560265", + "sha256:c5f9683be6a5b19cd776ee4e2f2ffb411424819c69afab6b2db3a0a364ec6642", + "sha256:cf89ab85027427d351f1de918aff4b43f4eb5f33aff6835ed30322a86ac29c9e", + "sha256:d1b750a8409bec61caa7824bfd64a8074b6d2d420433f64c161a8335796c7c6b", + "sha256:d779a48fac416387dd5673fc5b2d6bd903ed903faaa3247dc1865c65eaa5a93e", + "sha256:d9a1ef0f173e1a19738f154fb3644f90d0ada56fe6c9b422f992b04266c55d5a", + "sha256:ddb79414c15c6f03f56cc68fa06994f047cf20207c31b5dad3f6bab54a0f66ef", + "sha256:ef00d31b7569ed3cb2036f26565f1984b9fc08541731ce01012b02a4c238bf03", + "sha256:f40ac873045db4fd98a6f40387d242bde2708a3f8167bd967ccd43ad46394ba2", + "sha256:f593a4a90118d99014517c2679e04a4ef5aee2d81aa05c26c734d271065efcb6", + "sha256:f5df76c58977bc35a49515b2fbba84a1d952ff0ec784a4070334dfbec28a2def", + "sha256:f72cdd2586f9a769570d4b5714a3837b3a59a53b096bb954f1811f6a0afad305", + "sha256:f8e845d894e39fb53834da826078f6dc1a933b32b1478cf437007367efaf6f6a", + "sha256:fe6e43c8b510719b48af7db9631b5fbac910ade4bd90e6378c85ac5ac706382c" ], "markers": "python_version >= '3.8'", - "version": "==7.4.1" + "version": "==7.4.2" }, "defusedxml": { "hashes": [ @@ -2495,7 +2495,7 @@ "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.10'", "version": "==7.2.1" }, "pytest-cov": { diff --git a/backend/api/serializers/auth_factor.py b/backend/api/serializers/auth_factor.py index 09b5ad20..63def67b 100644 --- a/backend/api/serializers/auth_factor.py +++ b/backend/api/serializers/auth_factor.py @@ -24,7 +24,7 @@ class Meta: # pylint: disable-next=missing-function-docstring def validate_type(self, value: str): if AuthFactor.objects.filter( - user=self.request_user, type=value + user=self.request.auth_user, type=value ).exists(): raise serializers.ValidationError( "You already have this authentication factor enabled.", @@ -34,7 +34,7 @@ def validate_type(self, value: str): return value def create(self, validated_data): - user = self.request_user + user = self.request.auth_user if not user.userprofile.otp_secret: user.userprofile.otp_secret = pyotp.random_base32() user.userprofile.save() @@ -51,6 +51,6 @@ def to_representation(self, instance): ): representation[ "totp_provisioning_uri" - ] = self.request_user.totp_provisioning_uri + ] = self.request.auth_user.totp_provisioning_uri return representation diff --git a/backend/api/serializers/klass.py b/backend/api/serializers/klass.py index 8b758535..da97b09c 100644 --- a/backend/api/serializers/klass.py +++ b/backend/api/serializers/klass.py @@ -42,7 +42,7 @@ def validate_teacher(self, value: int): code="does_not_exist", ) - user = self.request_school_teacher_user + user = self.request.school_teacher_user if not queryset.filter(school=user.teacher.school_id).exists(): raise serializers.ValidationError( "This teacher is not in your school.", @@ -60,7 +60,7 @@ def validate_teacher(self, value: int): # pylint: disable-next=missing-function-docstring def validate_name(self, value: str): if Class.objects.filter( - teacher__school=self.request_school_teacher_user.teacher.school, + teacher__school=self.request.school_teacher_user.teacher.school, name=value, ).exists(): raise serializers.ValidationError( @@ -90,7 +90,7 @@ def create(self, validated_data): "teacher_id": ( validated_data["teacher"]["id"] if "teacher" in validated_data - else self.request_school_teacher_user.teacher.id + else self.request.school_teacher_user.teacher.id ), "classmates_data_viewable": validated_data[ "classmates_data_viewable" diff --git a/backend/api/serializers/school_teacher_invitation.py b/backend/api/serializers/school_teacher_invitation.py index 3f94a961..c2b20394 100644 --- a/backend/api/serializers/school_teacher_invitation.py +++ b/backend/api/serializers/school_teacher_invitation.py @@ -39,7 +39,7 @@ class Meta: } def create(self, validated_data): - user = self.request_admin_school_teacher_user + user = self.request.admin_school_teacher_user token = get_random_string(length=32) validated_data["token"] = make_password(token) diff --git a/backend/api/serializers/student.py b/backend/api/serializers/student.py index 4aaaeaaa..e8852ddf 100644 --- a/backend/api/serializers/student.py +++ b/backend/api/serializers/student.py @@ -20,7 +20,7 @@ class Meta(_StudentSerializer.Meta): # pylint: disable-next=missing-function-docstring def validate_klass(self, value: str): # Only teachers can manage students. - teacher = self.request_school_teacher_user.teacher + teacher = self.request.school_teacher_user.teacher try: klass = Class.objects.get(access_code=value) diff --git a/backend/api/serializers/teacher.py b/backend/api/serializers/teacher.py index ef868e69..c3d5e719 100644 --- a/backend/api/serializers/teacher.py +++ b/backend/api/serializers/teacher.py @@ -22,7 +22,7 @@ class Meta(_TeacherSerializer.Meta): def validate_is_admin(self, value: bool): instance = t.cast(t.Optional[User], self.parent.instance) if instance: - user = self.request_school_teacher_user + user = self.request.school_teacher_user if user.pk == instance.pk: raise serializers.ValidationError( "Cannot update own permissions.", diff --git a/backend/api/serializers/user.py b/backend/api/serializers/user.py index ef726f6f..5903d202 100644 --- a/backend/api/serializers/user.py +++ b/backend/api/serializers/user.py @@ -189,7 +189,7 @@ def to_representation(self, instance: User): # Return student's auto-generated password. if ( representation["student"] is not None - and self.request_user.teacher is not None + and self.request.auth_user.teacher is not None ): # pylint: disable-next=protected-access password = instance._password diff --git a/backend/api/tests/views/test_user.py b/backend/api/tests/views/test_user.py index 23caf27a..a09c88c8 100644 --- a/backend/api/tests/views/test_user.py +++ b/backend/api/tests/views/test_user.py @@ -11,15 +11,21 @@ from codeforlife.user.models import ( AdminSchoolTeacherUser, Class, + IndependentUser, + NonAdminSchoolTeacherUser, + NonSchoolTeacherUser, SchoolTeacherUser, + Student, StudentUser, + TypedUser, User, ) -from codeforlife.user.permissions import IsTeacher +from codeforlife.user.permissions import IsIndependent, IsTeacher from django.contrib.auth.tokens import ( PasswordResetTokenGenerator, default_token_generator, ) +from django.db.models.query import QuerySet from rest_framework import status from ...views import UserViewSet @@ -28,26 +34,23 @@ default_token_generator: PasswordResetTokenGenerator = default_token_generator -# pylint: disable-next=missing-class-docstring +# pylint: disable-next=missing-class-docstring,too-many-public-methods class TestUserViewSet(ModelViewSetTestCase[User]): basename = "user" model_view_set_class = UserViewSet fixtures = ["independent", "non_school_teacher", "school_1"] - non_school_teacher_email = "teacher@noschool.com" - school_teacher_email = "teacher@school1.com" - indy_email = "indy@man.com" - def setUp(self): + self.non_school_teacher_user = NonSchoolTeacherUser.objects.get( + email="teacher@noschool.com" + ) self.admin_school_teacher_user = AdminSchoolTeacherUser.objects.get( email="admin.teacher@school1.com" ) - - def _login_non_admin_school_teacher(self): - return self.client.login_non_admin_school_teacher( - email=self.school_teacher_email, - password="password", + self.non_admin_school_teacher_user = ( + NonAdminSchoolTeacherUser.objects.get(email="teacher@school1.com") ) + self.indy_user = IndependentUser.objects.get(email="indy@man.com") def _get_pk_and_token_for_user(self, email: str): user = User.objects.get(email__iexact=email) @@ -60,9 +63,7 @@ def _get_pk_and_token_for_user(self, email: str): def test_get_permissions__bulk(self): """Only admin-teachers or class-teachers can perform bulk actions.""" self.assert_get_permissions( - permissions=[ - OR(IsTeacher(is_admin=True), IsTeacher(in_class=True)) - ], + [OR(IsTeacher(is_admin=True), IsTeacher(in_class=True))], action="bulk", ) @@ -71,16 +72,14 @@ def test_get_permissions__students__reset_password(self): Only admin-teachers or class-teachers can reset students' passwords. """ self.assert_get_permissions( - permissions=[ - OR(IsTeacher(is_admin=True), IsTeacher(in_class=True)) - ], + [OR(IsTeacher(is_admin=True), IsTeacher(in_class=True))], action="students__reset_password", ) def test_get_permissions__partial_update__teacher(self): """Only admin-teachers can update a teacher.""" self.assert_get_permissions( - permissions=[IsTeacher(is_admin=True)], + [IsTeacher(is_admin=True)], action="partial_update", request=self.client.request_factory.patch(data={"teacher": {}}), ) @@ -88,16 +87,23 @@ def test_get_permissions__partial_update__teacher(self): def test_get_permissions__partial_update__student(self): """Only admin-teachers or class-teachers can update a student.""" self.assert_get_permissions( - permissions=[ - OR(IsTeacher(is_admin=True), IsTeacher(in_class=True)) - ], + [OR(IsTeacher(is_admin=True), IsTeacher(in_class=True))], action="partial_update", request=self.client.request_factory.patch(data={"student": {}}), ) + def test_get_permissions__destroy(self): + """Only independents or teachers can destroy a user.""" + self.assert_get_permissions( + [OR(IsTeacher(), IsIndependent())], + action="destroy", + ) + # test: get queryset - def _test_get_queryset(self, action: str, request_method: str): + def _test_get_queryset__student_users( + self, action: str, request_method: str + ): student_users = list( User.objects.filter( new_student__class_field__teacher__school=( @@ -115,25 +121,41 @@ def _test_get_queryset(self, action: str, request_method: str): def test_get_queryset__bulk__patch(self): """Bulk partial-update can only target student-users.""" - self._test_get_queryset(action="bulk", request_method="patch") + self._test_get_queryset__student_users( + action="bulk", request_method="patch" + ) def test_get_queryset__bulk__delete(self): """Bulk destroy can only target student-users.""" - self._test_get_queryset(action="bulk", request_method="delete") + self._test_get_queryset__student_users( + action="bulk", request_method="delete" + ) def test_get_queryset__students__reset_password(self): """Resetting student passwords can only target student-users.""" - self._test_get_queryset( + self._test_get_queryset__student_users( action="students__reset_password", request_method="patch" ) + def test_get_queryset__destroy(self): + """Destroying a user can only target the user making the request.""" + return self.assert_get_queryset( + [self.admin_school_teacher_user], + action="destroy", + request=self.client.request_factory.delete( + user=self.admin_school_teacher_user + ), + ) + # test: bulk actions def test_bulk_create__students(self): """Teacher can bulk create students.""" - user = self._login_non_admin_school_teacher() + self.client.login_as(self.non_admin_school_teacher_user) - klass: t.Optional[Class] = user.teacher.class_teacher.first() + klass: t.Optional[ + Class + ] = self.non_admin_school_teacher_user.teacher.class_teacher.first() assert klass is not None response = self.client.bulk_create( @@ -209,7 +231,7 @@ def test_request_password_reset__valid_email(self): viewname = self.reverse_action("request-password-reset") response = self.client.post( - viewname, data={"email": self.non_school_teacher_email} + viewname, data={"email": self.non_school_teacher_user.email} ) assert response.data["reset_password_url"] is not None @@ -219,7 +241,7 @@ def test_request_password_reset__valid_email(self): def test_reset_password__invalid_pk(self): """Reset password raises 400 on GET with invalid pk""" _, token = self._get_pk_and_token_for_user( - self.non_school_teacher_email + self.non_school_teacher_user.email ) viewname = self.reverse_action( @@ -236,7 +258,9 @@ def test_reset_password__invalid_pk(self): def test_reset_password__invalid_token(self): """Reset password raises 400 on GET with invalid token""" - pk, _ = self._get_pk_and_token_for_user(self.non_school_teacher_email) + pk, _ = self._get_pk_and_token_for_user( + self.non_school_teacher_user.email + ) viewname = self.reverse_action( "reset-password", kwargs={"pk": pk, "token": "whatever"} @@ -253,7 +277,7 @@ def test_reset_password__invalid_token(self): def test_reset_password__get(self): """Reset password GET succeeds.""" pk, token = self._get_pk_and_token_for_user( - self.non_school_teacher_email + self.non_school_teacher_user.email ) viewname = self.reverse_action( @@ -265,7 +289,7 @@ def test_reset_password__get(self): def test_reset_password__patch__teacher(self): """Teacher can successfully update password.""" pk, token = self._get_pk_and_token_for_user( - self.non_school_teacher_email + self.non_school_teacher_user.email ) viewname = self.reverse_action( @@ -273,14 +297,14 @@ def test_reset_password__patch__teacher(self): ) self.client.patch(viewname, data={"password": "N3wPassword!"}) - self.client.login( - email=self.non_school_teacher_email, + self.client.login_as( + self.non_school_teacher_user, password="N3wPassword!", ) def test_reset_password__patch__indy(self): """Indy can successfully update password.""" - pk, token = self._get_pk_and_token_for_user(self.indy_email) + pk, token = self._get_pk_and_token_for_user(self.indy_user.email) viewname = self.reverse_action( "reset-password", @@ -288,7 +312,7 @@ def test_reset_password__patch__indy(self): ) self.client.patch(viewname, data={"password": "N3wPassword"}) - self.client.login(email=self.indy_email, password="N3wPassword") + self.client.login_as(self.indy_user, password="N3wPassword") # test: students actions @@ -361,3 +385,106 @@ def test_partial_update__teacher(self): }, }, ) + + def assert_user_is_anonymized(self, user: User): + """Assert user has been anonymized. + + Args: + user: The user to assert. + """ + assert user.first_name == "" + assert user.last_name == "" + assert user.email == "" + assert not user.is_active + + def assert_classes_are_anonymized( + self, + school_teacher_user: SchoolTeacherUser, + class_names: t.Iterable[str], + ): + """Assert the classes and their students have been anonymized. + + Args: + school_teacher_user: The user the classes belong to. + class_names: The original class names. + """ + # TODO: remove when using new data strategy + queryset = QuerySet( + model=Class.objects.model, + using=Class.objects._db, + hints=Class.objects._hints, + ).filter(teacher=school_teacher_user.teacher) + + for klass, name in zip(queryset, class_names): + assert klass.name != name + assert klass.access_code == "" + assert not klass.is_active + + student: Student # TODO: delete in new data schema + for student in klass.students.all(): + self.assert_user_is_anonymized(student.new_user) + + def _test_destroy( + self, + user: TypedUser, + status_code_assertion: int = status.HTTP_204_NO_CONTENT, + ): + self.client.login_as(user) + self.client.destroy( + user, + status_code_assertion=status_code_assertion, + make_assertions=False, + ) + + def test_destroy__class_teacher(self): + """Class-teacher-users can anonymize themself and their classes.""" + user = self.non_admin_school_teacher_user + assert user.teacher.class_teacher.exists() + class_names = list( + user.teacher.class_teacher.values_list("name", flat=True) + ) + + self._test_destroy(user) + user.refresh_from_db() + self.assert_user_is_anonymized(user) + self.assert_classes_are_anonymized(user, class_names) + + def test_destroy__school_teacher__last_teacher(self): + """ + School-teacher-users can anonymize themself and their school if they are + the last teacher. + """ + user = self.admin_school_teacher_user + assert user.teacher.class_teacher.exists() + class_names = list( + user.teacher.class_teacher.values_list("name", flat=True) + ) + school_name = user.teacher.school.name + + SchoolTeacherUser.objects.filter( + new_teacher__school=user.teacher.school + ).exclude(pk=user.pk).delete() + + self._test_destroy(user) + user.refresh_from_db() + self.assert_user_is_anonymized(user) + self.assert_classes_are_anonymized(user, class_names) + assert user.teacher.school.name != school_name + assert not user.teacher.school.is_active + + def test_destroy__school_teacher__last_admin_teacher(self): + """ + School-teacher-users cannot anonymize themself if they are the last + admin teachers. + """ + self._test_destroy( + self.admin_school_teacher_user, + status_code_assertion=status.HTTP_409_CONFLICT, + ) + + def test_destroy__independent(self): + """Independent-users can anonymize themself.""" + user = self.indy_user + self._test_destroy(user) + user.refresh_from_db() + self.assert_user_is_anonymized(user) diff --git a/backend/api/views/auth_factor.py b/backend/api/views/auth_factor.py index f417efb4..1c813cf3 100644 --- a/backend/api/views/auth_factor.py +++ b/backend/api/views/auth_factor.py @@ -17,7 +17,7 @@ class AuthFactorViewSet(ModelViewSet[AuthFactor]): serializer_class = AuthFactorSerializer def get_queryset(self): - return AuthFactor.objects.filter(user=self.request_user) + return AuthFactor.objects.filter(user=self.request.auth_user) def get_permissions(self): if self.action in ["retrieve", "bulk"]: diff --git a/backend/api/views/otp_bypass_token.py b/backend/api/views/otp_bypass_token.py index c5d50d7a..ae5c9559 100644 --- a/backend/api/views/otp_bypass_token.py +++ b/backend/api/views/otp_bypass_token.py @@ -3,11 +3,9 @@ Created on 23/01/2024 at 17:54:08(+00:00). """ -import typing as t - from codeforlife.permissions import AllowNone from codeforlife.request import Request -from codeforlife.user.models import OtpBypassToken, User +from codeforlife.user.models import OtpBypassToken from codeforlife.user.permissions import IsTeacher from codeforlife.views import ModelViewSet from rest_framework import status @@ -20,8 +18,7 @@ class OtpBypassTokenViewSet(ModelViewSet[OtpBypassToken]): http_method_names = ["post"] def get_queryset(self): - user: User = self.request.user # type: ignore[assignment] - return OtpBypassToken.objects.filter(user=user) + return OtpBypassToken.objects.filter(user=self.request.teacher_user) def get_permissions(self): if self.action == "create": @@ -33,7 +30,7 @@ def get_permissions(self): def generate(self, request: Request): """Generates some OTP bypass tokens for a user.""" - user = t.cast(User, request.user) + user = request.auth_user OtpBypassToken.objects.filter(user=user).delete() diff --git a/backend/api/views/school_teacher_invitation.py b/backend/api/views/school_teacher_invitation.py index bff91cb3..1d9c2ed2 100644 --- a/backend/api/views/school_teacher_invitation.py +++ b/backend/api/views/school_teacher_invitation.py @@ -43,7 +43,7 @@ def get_queryset(self): return queryset return queryset.filter( - school=self.request_admin_school_teacher_user.teacher.school + school=self.request.admin_school_teacher_user.teacher.school ) @action( diff --git a/backend/api/views/user.py b/backend/api/views/user.py index 907c6212..6b4f9883 100644 --- a/backend/api/views/user.py +++ b/backend/api/views/user.py @@ -8,8 +8,8 @@ from codeforlife.permissions import OR from codeforlife.request import Request from codeforlife.types import DataDict -from codeforlife.user.models import StudentUser, User -from codeforlife.user.permissions import IsTeacher +from codeforlife.user.models import Class, SchoolTeacher, StudentUser, User +from codeforlife.user.permissions import IsIndependent, IsTeacher from codeforlife.user.views import UserViewSet as _UserViewSet from django.contrib.auth.tokens import ( PasswordResetTokenGenerator, @@ -32,6 +32,8 @@ class UserViewSet(_UserViewSet): serializer_class = UserSerializer def get_permissions(self): + if self.action == "destroy": + return [OR(IsTeacher(), IsIndependent())] if self.action in ["bulk", "students__reset_password"]: return [OR(IsTeacher(is_admin=True), IsTeacher(in_class=True))] if self.action == "partial_update": @@ -44,7 +46,9 @@ def get_permissions(self): def get_queryset(self): queryset = super().get_queryset() - if ( + if self.action == "destroy": + queryset = queryset.filter(pk=self.request.auth_user.pk) + elif ( self.action == "bulk" and self.request.method in ["PATCH", "DELETE"] ) or self.action == "students__reset_password": queryset = queryset.filter( @@ -56,6 +60,43 @@ def get_queryset(self): def perform_bulk_destroy(self, queryset): queryset.update(first_name="", is_active=False) + def destroy(self, request, *args, **kwargs): + user = self.get_object() + + def anonymize_user(user: User): + user.first_name = "" + user.last_name = "" + user.email = "" + user.is_active = False + user.save() + + if user.teacher: + if user.teacher.school: + other_school_teachers = SchoolTeacher.objects.filter( + school=user.teacher.school + ).exclude(pk=user.teacher.pk) + + if not other_school_teachers.exists(): + user.teacher.school.anonymise() + elif ( + user.teacher.is_admin + and not other_school_teachers.filter(is_admin=True).exists() + ): + return Response(status=status.HTTP_409_CONFLICT) + + klass: Class # TODO: delete in new data schema + for klass in user.teacher.class_teacher.all(): + for student_user in StudentUser.objects.filter( + new_student__class_field=klass + ): + anonymize_user(student_user) + + klass.anonymise() + + anonymize_user(user) + + return Response(status=status.HTTP_204_NO_CONTENT) + @action( detail=True, methods=["get", "patch"],