From 4e8e8b8a5ad6783333cc57feac650951bd1b0a46 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 5 Feb 2024 14:46:41 +0000 Subject: [PATCH 1/8] create school --- backend/Pipfile | 12 +- backend/Pipfile.lock | 350 +++++++-------- backend/api/serializers/school.py | 413 +++++++++++++++++- backend/api/tests/serializers/test_school.py | 44 ++ .../api/tests/views/test_otp_bypass_token.py | 2 +- backend/api/tests/views/test_school.py | 87 ++++ backend/api/views/school.py | 17 + 7 files changed, 743 insertions(+), 182 deletions(-) create mode 100644 backend/api/tests/serializers/test_school.py create mode 100644 backend/api/tests/views/test_school.py diff --git a/backend/Pipfile b/backend/Pipfile index 1cef8113..c6005685 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -28,15 +28,15 @@ google-cloud-logging = "==1.*" google-auth = "==2.*" google-cloud-container = "==2.3.0" # "django-anymail[amazon_ses]" = "==7.0.*" -codeforlife = {ref = "v0.11.2", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} -django = "==3.2.20" +codeforlife = {ref = "v0.12.4", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} +django = "==3.2.23" djangorestframework = "==3.13.1" django-cors-headers = "==4.1.0" # https://pypi.org/user/codeforlife/ -cfl-common = "==6.37.1" # TODO: remove -codeforlife-portal = "==6.37.1" # TODO: remove -aimmo = "==2.10.6" # TODO: remove -rapid-router = "==5.11.3" # TODO: remove +cfl-common = "==6.40.0" # TODO: remove +codeforlife-portal = "==6.40.0" # TODO: remove +aimmo = "==2.11.0" # TODO: remove +rapid-router = "==5.16.12" # TODO: remove phonenumbers = "==8.12.12" # TODO: remove [dev-packages] diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 5a78e12d..941ca572 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d31045f406dc9ec7fe3710a0225aba8dd242965211101e99514f68dd1a2c1076" + "sha256": "6ef3be9e49bc02002dd44d1f3b17d6822f638ed1afe1408dcda45de95fa4b043" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "aimmo": { "hashes": [ - "sha256:b89f83586412320b147ea61b4277599732c10e7668fba5b2d0a383db6a173145", - "sha256:bd2841b24d7830096b7cc81bdf7548377d30602f1a1b3d9e9084a58b11557413" + "sha256:1ef1e0e4148641526f2f5a10d5ee00f3aeae9227099ad5a012c05b5c637bc75f", + "sha256:959ec4d7215fc0b0f6943ce42ff7b1bfbe74eca531407eb8ce84d03912c11a20" ], "index": "pypi", - "version": "==2.10.6" + "version": "==2.11.0" }, "asgiref": { "hashes": [ @@ -50,19 +50,19 @@ }, "certifi": { "hashes": [ - "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", - "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" ], "markers": "python_version >= '3.6'", - "version": "==2023.11.17" + "version": "==2024.2.2" }, "cfl-common": { "hashes": [ - "sha256:24045d5550c741249a1d466cfb90149883454833accb48bbdb1aceec51e885c1", - "sha256:e28553af70dc4388fc73a6dfb4ece08e8518f21531d63213b448d24a5c0abef3" + "sha256:35b71e2a7eea91f8f12f186c40a609b01c05ee966c620ac810ad311c8279a2dd", + "sha256:9f661373c4b5968ed0eedb6e6de2eb43240932b52a9aa9a9698956639f6064aa" ], "index": "pypi", - "version": "==6.37.1" + "version": "==6.40.0" }, "charset-normalizer": { "hashes": [ @@ -170,15 +170,15 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "279ff34c1f467d729d014c8b1b49dab30372674c" + "ref": "8afb1bb8d20aac1d091886dadaed906b52a1907a" }, "codeforlife-portal": { "hashes": [ - "sha256:3c31ac0135af0cd78ec39e1b3e32ba98e514c05d944447e504002c000ce6b334", - "sha256:63a234390da9728139de7fbe8a4da9f4ca4b4b24d37c4bed248e5d49af563e53" + "sha256:75c186cd8708c86509658de00f2ad1e2771d4e5b379139143a6a50ede4879816", + "sha256:a48ca554543b6bb952490ef944ae94cc3b4ad0623e4ac7a47dbe006e0032ac1f" ], "index": "pypi", - "version": "==6.37.1" + "version": "==6.40.0" }, "defusedxml": { "hashes": [ @@ -198,12 +198,12 @@ }, "django": { "hashes": [ - "sha256:a477ab326ae7d8807dc25c186b951ab8c7648a3a23f9497763c37307a2b5ef87", - "sha256:dec2a116787b8e14962014bf78e120bba454135108e1af9e9b91ade7b2964c40" + "sha256:82968f3640e29ef4a773af2c28448f5f7a08d001c6ac05b32d02aeee6509508b", + "sha256:d48608d5f62f2c1e260986835db089fa3b79d6f58510881d316b8d88345ae6e1" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==3.2.20" + "version": "==3.2.23" }, "django-classy-tags": { "hashes": [ @@ -256,12 +256,12 @@ }, "django-import-export": { "hashes": [ - "sha256:12edb7ba1f7f9b392d0257a2ae68086020e53ab2c7bd2b21a56ec17fbb83826b", - "sha256:b1b7385627ed61063cd9764e8c19cce3ce8945626f7953262df8162b0feec376" + "sha256:39a4216c26a2dba6429b64c68b3fe282a6279bb71afb4015c13df0696bdbb4cd", + "sha256:dffedd53bed33cfcceb3b2f13d4fd93a21826f9a2ae37b9926a1e1f4be24bcb9" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==3.3.6" + "version": "==3.3.7" }, "django-js-reverse": { "hashes": [ @@ -392,12 +392,12 @@ }, "google-auth": { "hashes": [ - "sha256:3f445c8ce9b61ed6459aad86d8ccdba4a9afed841b2d1451a11ef4db08957424", - "sha256:97327dbbf58cccb58fc5a1712bba403ae76668e64814eb30f7316f7e27126b81" + "sha256:8e4bad367015430ff253fe49d500fdc3396c1a434db5740828c728e45bcce245", + "sha256:e863a56ccc2d8efa83df7a80272601e43487fa9a728a376205c86c26aaefa821" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.26.2" + "version": "==2.27.0" }, "google-cloud-container": { "hashes": [ @@ -510,62 +510,62 @@ }, "grpcio": { "hashes": [ - "sha256:073f959c6f570797272f4ee9464a9997eaf1e98c27cb680225b82b53390d61e6", - "sha256:0fd3b3968ffe7643144580f260f04d39d869fcc2cddb745deef078b09fd2b328", - "sha256:1434ca77d6fed4ea312901122dc8da6c4389738bf5788f43efb19a838ac03ead", - "sha256:1c30bb23a41df95109db130a6cc1b974844300ae2e5d68dd4947aacba5985aa5", - "sha256:20e7a4f7ded59097c84059d28230907cd97130fa74f4a8bfd1d8e5ba18c81491", - "sha256:2199165a1affb666aa24adf0c97436686d0a61bc5fc113c037701fb7c7fceb96", - "sha256:297eef542156d6b15174a1231c2493ea9ea54af8d016b8ca7d5d9cc65cfcc444", - "sha256:2aef56e85901c2397bd557c5ba514f84de1f0ae5dd132f5d5fed042858115951", - "sha256:30943b9530fe3620e3b195c03130396cd0ee3a0d10a66c1bee715d1819001eaf", - "sha256:3b36a2c6d4920ba88fa98075fdd58ff94ebeb8acc1215ae07d01a418af4c0253", - "sha256:428d699c8553c27e98f4d29fdc0f0edc50e9a8a7590bfd294d2edb0da7be3629", - "sha256:43e636dc2ce9ece583b3e2ca41df5c983f4302eabc6d5f9cd04f0562ee8ec1ae", - "sha256:452ca5b4afed30e7274445dd9b441a35ece656ec1600b77fff8c216fdf07df43", - "sha256:467a7d31554892eed2aa6c2d47ded1079fc40ea0b9601d9f79204afa8902274b", - "sha256:4b44d7e39964e808b071714666a812049765b26b3ea48c4434a3b317bac82f14", - "sha256:4c86343cf9ff7b2514dd229bdd88ebba760bd8973dac192ae687ff75e39ebfab", - "sha256:5208a57eae445ae84a219dfd8b56e04313445d146873117b5fa75f3245bc1390", - "sha256:5ff21e000ff2f658430bde5288cb1ac440ff15c0d7d18b5fb222f941b46cb0d2", - "sha256:675997222f2e2f22928fbba640824aebd43791116034f62006e19730715166c0", - "sha256:676e4a44e740deaba0f4d95ba1d8c5c89a2fcc43d02c39f69450b1fa19d39590", - "sha256:6e306b97966369b889985a562ede9d99180def39ad42c8014628dd3cc343f508", - "sha256:6fd9584bf1bccdfff1512719316efa77be235469e1e3295dce64538c4773840b", - "sha256:705a68a973c4c76db5d369ed573fec3367d7d196673fa86614b33d8c8e9ebb08", - "sha256:74d7d9fa97809c5b892449b28a65ec2bfa458a4735ddad46074f9f7d9550ad13", - "sha256:77c8a317f0fd5a0a2be8ed5cbe5341537d5c00bb79b3bb27ba7c5378ba77dbca", - "sha256:79a050889eb8d57a93ed21d9585bb63fca881666fc709f5d9f7f9372f5e7fd03", - "sha256:7db16dd4ea1b05ada504f08d0dca1cd9b926bed3770f50e715d087c6f00ad748", - "sha256:83f2292ae292ed5a47cdcb9821039ca8e88902923198f2193f13959360c01860", - "sha256:87c9224acba0ad8bacddf427a1c2772e17ce50b3042a789547af27099c5f751d", - "sha256:8a97a681e82bc11a42d4372fe57898d270a2707f36c45c6676e49ce0d5c41353", - "sha256:9073513ec380434eb8d21970e1ab3161041de121f4018bbed3146839451a6d8e", - "sha256:90bdd76b3f04bdb21de5398b8a7c629676c81dfac290f5f19883857e9371d28c", - "sha256:91229d7203f1ef0ab420c9b53fe2ca5c1fbeb34f69b3bc1b5089466237a4a134", - "sha256:92f88ca1b956eb8427a11bb8b4a0c0b2b03377235fc5102cb05e533b8693a415", - "sha256:95ae3e8e2c1b9bf671817f86f155c5da7d49a2289c5cf27a319458c3e025c320", - "sha256:9e30be89a75ee66aec7f9e60086fadb37ff8c0ba49a022887c28c134341f7179", - "sha256:a48edde788b99214613e440fce495bbe2b1e142a7f214cce9e0832146c41e324", - "sha256:a7152fa6e597c20cb97923407cf0934e14224af42c2b8d915f48bc3ad2d9ac18", - "sha256:a9c7b71211f066908e518a2ef7a5e211670761651039f0d6a80d8d40054047df", - "sha256:b0571a5aef36ba9177e262dc88a9240c866d903a62799e44fd4aae3f9a2ec17e", - "sha256:b0fb2d4801546598ac5cd18e3ec79c1a9af8b8f2a86283c55a5337c5aeca4b1b", - "sha256:b10241250cb77657ab315270b064a6c7f1add58af94befa20687e7c8d8603ae6", - "sha256:b87efe4a380887425bb15f220079aa8336276398dc33fce38c64d278164f963d", - "sha256:b98f43fcdb16172dec5f4b49f2fece4b16a99fd284d81c6bbac1b3b69fcbe0ff", - "sha256:c193109ca4070cdcaa6eff00fdb5a56233dc7610216d58fb81638f89f02e4968", - "sha256:c826f93050c73e7769806f92e601e0efdb83ec8d7c76ddf45d514fee54e8e619", - "sha256:d020cfa595d1f8f5c6b343530cd3ca16ae5aefdd1e832b777f9f0eb105f5b139", - "sha256:d6a478581b1a1a8fdf3318ecb5f4d0cda41cacdffe2b527c23707c9c1b8fdb55", - "sha256:de2ad69c9a094bf37c1102b5744c9aec6cf74d2b635558b779085d0263166454", - "sha256:e278eafb406f7e1b1b637c2cf51d3ad45883bb5bd1ca56bc05e4fc135dfdaa65", - "sha256:e381fe0c2aa6c03b056ad8f52f8efca7be29fb4d9ae2f8873520843b6039612a", - "sha256:e61e76020e0c332a98290323ecfec721c9544f5b739fab925b6e8cbe1944cf19", - "sha256:f897c3b127532e6befdcf961c415c97f320d45614daf84deba0a54e64ea2457b", - "sha256:fb464479934778d7cc5baf463d959d361954d6533ad34c3a4f1d267e86ee25fd" - ], - "version": "==1.60.0" + "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" }, "grpcio-status": { "hashes": [ @@ -681,69 +681,69 @@ }, "markupsafe": { "hashes": [ - "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", - "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", - "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", - "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", - "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", - "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", - "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", - "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", - "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", - "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", - "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", - "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", - "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", - "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", - "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", - "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", - "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", - "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", - "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", - "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", - "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", - "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", - "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", - "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", - "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", - "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", - "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", - "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", - "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", - "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", - "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", - "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", - "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", - "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", - "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", - "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", - "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", - "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", - "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", - "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", - "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", - "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", - "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", - "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", - "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", - "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", - "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", - "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", - "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", - "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", - "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", - "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", - "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", - "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", - "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", - "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", - "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", - "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", - "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2", - "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11" + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" ], "markers": "python_version >= '3.7'", - "version": "==2.1.3" + "version": "==2.1.5" }, "more-itertools": { "hashes": [ @@ -816,6 +816,7 @@ "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" ], + "markers": "python_version >= '3.6'", "version": "==3.1.2" }, "pandas": { @@ -1074,10 +1075,10 @@ }, "pytz": { "hashes": [ - "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b", - "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7" + "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", + "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" ], - "version": "==2023.3.post1" + "version": "==2024.1" }, "pyyaml": { "hashes": [ @@ -1125,11 +1126,11 @@ }, "rapid-router": { "hashes": [ - "sha256:b3b0b9dd449775aaac8b6dcc05601f8e0d3d1805100ba80a086cc8a3c2661526", - "sha256:bb88bb75e0f743ebedcfd44fbbfba5e1f63e5464c2d7d7bd02d3c838486f78ac" + "sha256:ad30357c65aaae03e06485aa99f4d42b1b3cc86d767082da79fbf65f9acda094", + "sha256:ffb1d8ab7e686c41aed199d9323be3c63078b345ce710f096f35e892ed4a6e0a" ], "index": "pypi", - "version": "==5.11.3" + "version": "==5.16.12" }, "reportlab": { "hashes": [ @@ -1210,11 +1211,11 @@ }, "setuptools": { "hashes": [ - "sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8", - "sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592" + "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31", + "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f" ], "markers": "python_version >= '3.7'", - "version": "==62.1.0" + "version": "==65.5.1" }, "six": { "hashes": [ @@ -1280,11 +1281,11 @@ }, "urllib3": { "hashes": [ - "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", - "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" + "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20", + "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224" ], "markers": "python_version >= '3.8'", - "version": "==2.1.0" + "version": "==2.2.0" }, "websocket-client": { "hashes": [ @@ -1316,6 +1317,7 @@ "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.0.1" }, "xlwt": { @@ -1345,11 +1347,11 @@ }, "astroid": { "hashes": [ - "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91", - "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c" + "sha256:4148645659b08b70d72460ed1921158027a9e53ae8b7234149b1400eddacbb93", + "sha256:92fcf218b89f449cdf9f7b39a269f8d5d617b27be68434912e11e79203963a17" ], "markers": "python_full_version >= '3.8.0'", - "version": "==3.0.2" + "version": "==3.0.3" }, "black": { "hashes": [ @@ -1382,11 +1384,11 @@ }, "certifi": { "hashes": [ - "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", - "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" ], "markers": "python_version >= '3.6'", - "version": "==2023.11.17" + "version": "==2024.2.2" }, "charset-normalizer": { "hashes": [ @@ -1579,21 +1581,21 @@ }, "django": { "hashes": [ - "sha256:a477ab326ae7d8807dc25c186b951ab8c7648a3a23f9497763c37307a2b5ef87", - "sha256:dec2a116787b8e14962014bf78e120bba454135108e1af9e9b91ade7b2964c40" + "sha256:82968f3640e29ef4a773af2c28448f5f7a08d001c6ac05b32d02aeee6509508b", + "sha256:d48608d5f62f2c1e260986835db089fa3b79d6f58510881d316b8d88345ae6e1" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==3.2.20" + "version": "==3.2.23" }, "django-import-export": { "hashes": [ - "sha256:12edb7ba1f7f9b392d0257a2ae68086020e53ab2c7bd2b21a56ec17fbb83826b", - "sha256:b1b7385627ed61063cd9764e8c19cce3ce8945626f7953262df8162b0feec376" + "sha256:39a4216c26a2dba6429b64c68b3fe282a6279bb71afb4015c13df0696bdbb4cd", + "sha256:dffedd53bed33cfcceb3b2f13d4fd93a21826f9a2ae37b9926a1e1f4be24bcb9" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==3.3.6" + "version": "==3.3.7" }, "django-selenium-clean": { "hashes": [ @@ -1766,6 +1768,7 @@ "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" ], + "markers": "python_version >= '3.6'", "version": "==3.1.2" }, "packaging": { @@ -1899,10 +1902,10 @@ }, "pytz": { "hashes": [ - "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b", - "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7" + "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", + "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" ], - "version": "==2023.3.post1" + "version": "==2024.1" }, "pyvirtualdisplay": { "hashes": [ @@ -2040,11 +2043,11 @@ }, "types-pytz": { "hashes": [ - "sha256:33676a90bf04b19f92c33eec8581136bea2f35ddd12759e579a624a006fd387a", - "sha256:6ce76a9f8fd22bd39b01a59c35bfa2db39b60d11a2f77145e97b730de7e64fe0" + "sha256:9679eef0365db3af91ef7722c199dbb75ee5c1b67e3c4dd7bfbeb1b8a71c21a3", + "sha256:c93751ee20dfc6e054a0148f8f5227b9a00b79c90a4d3c9f464711a73179c89e" ], "markers": "python_version >= '3.8'", - "version": "==2023.4.0.20240130" + "version": "==2024.1.0.20240203" }, "types-pyyaml": { "hashes": [ @@ -2071,11 +2074,11 @@ }, "urllib3": { "hashes": [ - "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", - "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" + "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20", + "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224" ], "markers": "python_version >= '3.8'", - "version": "==2.1.0" + "version": "==2.2.0" }, "wasmer": { "hashes": [ @@ -2120,6 +2123,7 @@ "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.0.1" }, "xlwt": { diff --git a/backend/api/serializers/school.py b/backend/api/serializers/school.py index 1930bbe5..132c48d5 100644 --- a/backend/api/serializers/school.py +++ b/backend/api/serializers/school.py @@ -3,10 +3,419 @@ Created on 23/01/2024 at 11:05:41(+00:00). """ +from codeforlife.user.models import School from codeforlife.user.serializers import SchoolSerializer as _SchoolSerializer +from rest_framework import serializers -# pylint: disable-next=missing-class-docstring +# pylint: disable-next=missing-class-docstring,too-many-ancestors class SchoolSerializer(_SchoolSerializer): + country = serializers.ChoiceField( # type: ignore[assignment] + choices=[ + "AF", + "AX", + "AL", + "DZ", + "AS", + "AD", + "AO", + "AI", + "AQ", + "AG", + "AR", + "AM", + "AW", + "AU", + "AT", + "AZ", + "BS", + "BH", + "BD", + "BB", + "BY", + "BE", + "BZ", + "BJ", + "BM", + "BT", + "BO", + "BQ", + "BA", + "BW", + "BV", + "BR", + "IO", + "BN", + "BG", + "BF", + "BI", + "KH", + "CM", + "CA", + "CV", + "KY", + "CF", + "TD", + "CL", + "CN", + "CX", + "CC", + "CO", + "KM", + "CG", + "CD", + "CK", + "CR", + "CI", + "HR", + "CU", + "CW", + "CY", + "CZ", + "DK", + "DJ", + "DM", + "DO", + "EC", + "EG", + "SV", + "GQ", + "ER", + "EE", + "ET", + "FK", + "FO", + "FJ", + "FI", + "FR", + "GF", + "PF", + "TF", + "GA", + "GM", + "GE", + "DE", + "GH", + "GI", + "GR", + "GL", + "GD", + "GP", + "GU", + "GT", + "GG", + "GN", + "GW", + "GY", + "HT", + "HM", + "VA", + "HN", + "HK", + "HU", + "IS", + "IN", + "ID", + "IR", + "IQ", + "IE", + "IM", + "IL", + "IT", + "JM", + "JP", + "JE", + "JO", + "KZ", + "KE", + "KI", + "KP", + "KR", + "KW", + "KG", + "LA", + "LV", + "LB", + "LS", + "LR", + "LY", + "LI", + "LT", + "LU", + "MO", + "MK", + "MG", + "MW", + "MY", + "MV", + "ML", + "MT", + "MH", + "MQ", + "MR", + "MU", + "YT", + "MX", + "FM", + "MD", + "MC", + "MN", + "ME", + "MS", + "MA", + "MZ", + "MM", + "NA", + "NR", + "NP", + "NL", + "NC", + "NZ", + "NI", + "NE", + "NG", + "NU", + "NF", + "MP", + "NO", + "OM", + "PK", + "PW", + "PS", + "PA", + "PG", + "PY", + "PE", + "PH", + "PN", + "PL", + "PT", + "PR", + "QA", + "RE", + "RO", + "RU", + "RW", + "BL", + "SH", + "KN", + "LC", + "MF", + "PM", + "VC", + "WS", + "SM", + "ST", + "SA", + "SN", + "RS", + "SC", + "SL", + "SG", + "SX", + "SK", + "SI", + "SB", + "SO", + "ZA", + "GS", + "SS", + "ES", + "LK", + "SD", + "SR", + "SJ", + "SZ", + "SE", + "CH", + "SY", + "TW", + "TJ", + "TZ", + "TH", + "TL", + "TG", + "TK", + "TO", + "TT", + "TN", + "TR", + "TM", + "TC", + "TV", + "UG", + "UA", + "AE", + "GB", + "US", + "UM", + "UY", + "UZ", + "VU", + "VE", + "VN", + "VG", + "VI", + "WF", + "EH", + "YE", + "ZM", + "ZW", + ], + ) + + uk_county = serializers.ChoiceField( # type: ignore[assignment] + source="county", + default="", + choices=[ + "Aberdeen City", + "Aberdeenshire", + "Angus", + "Argyll and Bute", + "Bedfordshire", + "Belfast", + "Belfast Greater", + "Berkshire", + "Blaenau Gwent", + "Bridgend", + "Buckinghamshire", + "Caerphilly", + "Cambridgeshire", + "Cardiff", + "Carmarthenshire", + "Ceredigion", + "Channel Islands", + "Cheshire", + "City of Edinburgh", + "Clackmannanshire", + "Conwy", + "Cornwall", + "County Antrim", + "County Armagh", + "County Down", + "County Fermanagh", + "County Londonderry", + "County Tyrone", + "County of Bristol", + "Cumbria", + "Denbighshire", + "Derbyshire", + "Devon", + "Dorset", + "Dumfries and Galloway", + "Dunbartonshire", + "Dundee City", + "Durham", + "East Ayrshire", + "East Dunbartonshire", + "East Lothian", + "East Renfrewshire", + "East Riding of Yorkshire", + "East Sussex", + "Essex", + "Falkirk", + "Fife", + "Flintshire", + "Glasgow City", + "Gloucestershire", + "Greater London", + "Greater Manchester", + "Guernsey Channel Islands", + "Gwynedd", + "Hampshire", + "Hereford and Worcester", + "Herefordshire", + "Hertfordshire", + "Highland", + "Inverclyde", + "Inverness", + "Isle of Anglesey", + "Isle of Barra", + "Isle of Man", + "Isle of Wight", + "Jersey Channel Islands", + "Kent", + "Lancashire", + "Leicestershire", + "Lincolnshire", + "Merseyside", + "Merthyr Tydfil", + "Midlothian", + "Monmouthshire", + "Moray", + "Neath Port Talbot", + "Newport", + "Norfolk", + "North Ayrshire", + "North Lanarkshire", + "North Yorkshire", + "Northamptonshire", + "Northumberland", + "Nottinghamshire", + "Orkney", + "Orkney Islands", + "Oxfordshire", + "Pembrokeshire", + "Perth and Kinross", + "Powys", + "Renfrewshire", + "Rhondda Cynon Taff", + "Rutland", + "Scottish Borders", + "Shetland Islands", + "Shropshire", + "Somerset", + "South Ayrshire", + "South Lanarkshire", + "South Yorkshire", + "Staffordshire", + "Stirling", + "Suffolk", + "Surrey", + "Swansea", + "Torfaen", + "Tyne and Wear", + "Vale of Glamorgan", + "Warwickshire", + "West Dunbart", + "West Lothian", + "West Midlands", + "West Sussex", + "West Yorkshire", + "Western Isles", + "Wiltshire", + "Worcestershire", + "Wrexham", + ], + ) + class Meta(_SchoolSerializer.Meta): - pass + extra_kwargs = { + **_SchoolSerializer.Meta.extra_kwargs, + "name": {"read_only": False}, + } + + # TODO: set unique=True for model name field in new models. + # pylint: disable-next=missing-function-docstring + def validate_name(self, value: str): + if School.objects.filter(name=value).exists(): + raise serializers.ValidationError( + "Name already taken.", + code="name_not_unique", + ) + + return value + + def validate(self, attrs): + if "uk_county" in attrs: + country = attrs.get( + "country", + self.instance.country if self.view.action != "create" else None, + ) + if country != "GB": + raise serializers.ValidationError( + "Cannot set the UK county when country is not set to GB.", + code="country_ne_gb", + ) + + return attrs diff --git a/backend/api/tests/serializers/test_school.py b/backend/api/tests/serializers/test_school.py new file mode 100644 index 00000000..495454af --- /dev/null +++ b/backend/api/tests/serializers/test_school.py @@ -0,0 +1,44 @@ +""" +© Ocado Group +Created on 02/02/2024 at 15:38:51(+00:00). +""" + +from codeforlife.tests import ModelSerializerTestCase +from codeforlife.user.models import School + +from ...serializers import SchoolSerializer +from ...views import SchoolViewSet + + +# pylint: disable-next=missing-class-docstring +class SchoolSerializerTestCase(ModelSerializerTestCase[School]): + model_serializer_class = SchoolSerializer + fixtures = ["school_1"] + + def setUp(self): + self.school_1 = School.objects.get(pk=2) + + def test_validate__country_ne_gb(self): + """ + Setting a UK county raises an error if the country does not equal GB. + """ + + self.assert_validate( + attrs={ + "uk_county": "Surrey", + "country": "AF", + }, + error_code="country_ne_gb", + context={"view": SchoolViewSet(action="create")}, + ) + + def test_validate_name__name_not_unique(self): + """ + School names must be unique. + """ + + self.assert_validate_field( + name="name", + value=self.school_1.name, + error_code="name_not_unique", + ) diff --git a/backend/api/tests/views/test_otp_bypass_token.py b/backend/api/tests/views/test_otp_bypass_token.py index abf0d81d..161d12d7 100644 --- a/backend/api/tests/views/test_otp_bypass_token.py +++ b/backend/api/tests/views/test_otp_bypass_token.py @@ -57,7 +57,7 @@ def test_generate(self): OtpBypassToken, "generate_tokens", return_value=tokens ) as generate_tokens: response = self.client.post( - self.client.reverse("generate"), + self.reverse_action("generate"), status_code_assertion=status.HTTP_201_CREATED, ) diff --git a/backend/api/tests/views/test_school.py b/backend/api/tests/views/test_school.py new file mode 100644 index 00000000..d02d28da --- /dev/null +++ b/backend/api/tests/views/test_school.py @@ -0,0 +1,87 @@ +""" +© Ocado Group +Created on 02/02/2024 at 15:31:21(+00:00). +""" + +from codeforlife.permissions import NOT, AllowNone +from codeforlife.tests import ModelViewSetTestCase +from codeforlife.user.models import School +from codeforlife.user.permissions import InSchool, IsTeacher + +from ...views import SchoolViewSet + + +# pylint: disable-next=missing-class-docstring +class TestSchoolViewSet(ModelViewSetTestCase[School]): + basename = "school" + model_view_set_class = SchoolViewSet + fixtures = ["non_school_teacher"] + + def test_get_permissions__bulk(self): + """ + No one is allowed to perform bulk actions. + """ + + self.assert_get_permissions( + permissions=[AllowNone()], + action="bulk", + ) + + def test_get_permissions__list(self): + """ + No one is allowed to list schools. + """ + + self.assert_get_permissions( + permissions=[AllowNone()], + action="list", + ) + + def test_get_permissions__create(self): + """ + Only teachers not in a school can create a school. + """ + + self.assert_get_permissions( + permissions=[IsTeacher(), NOT(InSchool())], + action="create", + ) + + def test_get_permissions__update(self): + """ + Only admin-teachers in a school can update a school. + """ + + self.assert_get_permissions( + permissions=[IsTeacher(is_admin=True), InSchool()], + action="update", + ) + + def test_get_permissions__retrieve(self): + """ + Anyone in a school can retrieve a school. + """ + + self.assert_get_permissions( + permissions=[InSchool()], + action="retrieve", + ) + + def test_create(self): + """ + Can successfully create a school. + """ + + self.client.login_non_school_teacher( + email="teacher@noschool.com", + password="password", + in_school=False, + ) + + self.client.create( + { + "name": "ExampleSchool", + "uk_county": "Surrey", + "country": "GB", + }, + ) diff --git a/backend/api/views/school.py b/backend/api/views/school.py index 191ab540..54e52d83 100644 --- a/backend/api/views/school.py +++ b/backend/api/views/school.py @@ -3,6 +3,10 @@ Created on 23/01/2024 at 17:53:50(+00:00). """ +import typing as t + +from codeforlife.permissions import NOT, AllowNone, Permission +from codeforlife.user.permissions import InSchool, IsTeacher from codeforlife.user.views import SchoolViewSet as _SchoolViewSet from ..serializers import SchoolSerializer @@ -12,3 +16,16 @@ class SchoolViewSet(_SchoolViewSet): http_method_names = ["get", "post", "patch"] serializer_class = SchoolSerializer + + def get_permissions(self): + # Bulk actions not allowed for schools. + if self.action in ["bulk", "list"]: + return [AllowNone()] + # Only teachers not in a school can create a school. + if self.action == "create": + return [IsTeacher(), NOT(InSchool())] + # Only admin-teachers in a school can update a school. + if self.action == "update": + return [IsTeacher(is_admin=True), InSchool()] + + return [InSchool()] From a8f0740ff1e66ebda1d8c01c8b9abfb846e23896 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 5 Feb 2024 14:48:52 +0000 Subject: [PATCH 2/8] remove is_unique_email action --- backend/api/tests/views/test_user.py | 37 +++------------------------- backend/api/views/user.py | 15 ----------- 2 files changed, 4 insertions(+), 48 deletions(-) diff --git a/backend/api/tests/views/test_user.py b/backend/api/tests/views/test_user.py index f0bf8385..595580b6 100644 --- a/backend/api/tests/views/test_user.py +++ b/backend/api/tests/views/test_user.py @@ -11,53 +11,24 @@ from ...views import UserViewSet +# pylint: disable-next=missing-class-docstring class TestUserViewSet(ModelViewSetTestCase[User]): - """ - Base naming convention: - test_{action} - - action: The view set action. - https://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions - """ - basename = "user" model_view_set_class = UserViewSet - def _login_teacher(self): - return self.client.login_teacher( + def _login_school_teacher(self): + return self.client.login_school_teacher( email="maxplanck@codeforlife.com", password="Password1", is_admin=False, ) - def test_is_unique_email(self): - """ - Check email is unique. - """ - - user = User.objects.first() - assert user is not None - - viewname = self.client.reverse("is-unique-email") - - response = self.client.post(viewname, data={"email": user.email}) - - self.assertFalse(response.json()) - - response = self.client.post( - viewname, - data={"email": "unique.email@codeforlife.com"}, - ) - - self.assertTrue(response.json()) - def test_bulk_create__students(self): """ Teacher can bulk create students. """ - user = self._login_teacher() - assert user.teacher.school is not None + user = self._login_school_teacher() klass: t.Optional[Class] = user.teacher.class_teacher.first() assert klass is not None diff --git a/backend/api/views/user.py b/backend/api/views/user.py index 813fd35d..5f8dc831 100644 --- a/backend/api/views/user.py +++ b/backend/api/views/user.py @@ -3,14 +3,8 @@ Created on 23/01/2024 at 17:53:44(+00:00). """ -import typing as t - -from codeforlife.user.models import User from codeforlife.user.permissions import IsTeacher from codeforlife.user.views import UserViewSet as _UserViewSet -from rest_framework.decorators import action -from rest_framework.permissions import AllowAny -from rest_framework.response import Response from ..serializers import UserSerializer @@ -25,12 +19,3 @@ def get_permissions(self): return [IsTeacher()] return super().get_permissions() - - @action(detail=False, methods=["post"], permission_classes=[AllowAny]) - def is_unique_email(self, request): - """Checks if an email is unique.""" - - email: t.Optional[str] = request.data.get("email") - return Response( - email and not User.objects.filter(email__iexact=email).exists() - ) From 8498d4c9ed8419eddc8ef0de5f91f0a04b57fd4d Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 5 Feb 2024 15:02:54 +0000 Subject: [PATCH 3/8] fix unit tests --- backend/api/tests/views/test_user.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/backend/api/tests/views/test_user.py b/backend/api/tests/views/test_user.py index e3106901..1fb4dfcc 100644 --- a/backend/api/tests/views/test_user.py +++ b/backend/api/tests/views/test_user.py @@ -7,11 +7,17 @@ from codeforlife.tests import ModelViewSetTestCase from codeforlife.user.models import Class, User -from django.contrib.auth.tokens import default_token_generator +from django.contrib.auth.tokens import ( + PasswordResetTokenGenerator, + default_token_generator, +) from rest_framework import status from ...views import UserViewSet +# NOTE: type hint to help Intellisense. +default_token_generator: PasswordResetTokenGenerator = default_token_generator + # pylint: disable-next=missing-class-docstring class TestUserViewSet(ModelViewSetTestCase[User]): @@ -61,7 +67,7 @@ def test_request_password_reset__invalid_email(self): Request password reset doesn't generate reset password URL if email is invalid but still returns a 200. """ - viewname = self.client.reverse("request-password-reset") + viewname = self.reverse_action("request-password-reset") response = self.client.post( viewname, @@ -73,7 +79,7 @@ def test_request_password_reset__invalid_email(self): def test_request_password_reset__empty_email(self): """Email field is required.""" - viewname = self.client.reverse("request-password-reset") + viewname = self.reverse_action("request-password-reset") response = self.client.post( viewname, status_code_assertion=status.HTTP_400_BAD_REQUEST @@ -85,7 +91,7 @@ def test_request_password_reset__valid_email(self): """ Request password reset generates reset password URL for valid email. """ - viewname = self.client.reverse("request-password-reset") + viewname = self.reverse_action("request-password-reset") response = self.client.post( viewname, data={"email": self.non_school_teacher_email} @@ -101,7 +107,7 @@ def test_reset_password__invalid_pk(self): self.non_school_teacher_email ) - viewname = self.client.reverse( + viewname = self.reverse_action( "reset-password", kwargs={"pk": "whatever", "token": token}, ) @@ -118,7 +124,7 @@ 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) - viewname = self.client.reverse( + viewname = self.reverse_action( "reset-password", kwargs={"pk": pk, "token": "whatever"}, ) @@ -137,7 +143,7 @@ def test_reset_password__get(self): self.non_school_teacher_email ) - viewname = self.client.reverse( + viewname = self.reverse_action( "reset-password", kwargs={"pk": pk, "token": token}, ) @@ -150,7 +156,7 @@ def test_reset_password__patch__teacher(self): self.non_school_teacher_email ) - viewname = self.client.reverse( + viewname = self.reverse_action( "reset-password", kwargs={"pk": pk, "token": token}, ) @@ -165,7 +171,7 @@ def test_reset_password__patch__indy(self): """Indy can successfully update password.""" pk, token = self._get_pk_and_token_for_user(self.indy_email) - viewname = self.client.reverse( + viewname = self.reverse_action( "reset-password", kwargs={"pk": pk, "token": token}, ) From 484bef041b6a61892ba684fee5551413738cf2e5 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 6 Feb 2024 22:54:44 +0000 Subject: [PATCH 4/8] everything but the serializer --- backend/Pipfile | 2 +- backend/Pipfile.lock | 16 ++-- backend/api/serializers/school.py | 1 - backend/api/tests/serializers/test_klass.py | 51 ++++++++++ backend/api/tests/views/test_klass.py | 101 ++++++++++++++++++++ backend/api/tests/views/test_school.py | 1 - backend/api/tests/views/test_user.py | 7 +- backend/api/views/klass.py | 11 +++ backend/api/views/school.py | 8 +- 9 files changed, 178 insertions(+), 20 deletions(-) create mode 100644 backend/api/tests/serializers/test_klass.py create mode 100644 backend/api/tests/views/test_klass.py diff --git a/backend/Pipfile b/backend/Pipfile index c6005685..9ea4246b 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -28,7 +28,7 @@ google-cloud-logging = "==1.*" google-auth = "==2.*" google-cloud-container = "==2.3.0" # "django-anymail[amazon_ses]" = "==7.0.*" -codeforlife = {ref = "v0.12.4", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} +codeforlife = {ref = "create_class", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} django = "==3.2.23" djangorestframework = "==3.13.1" django-cors-headers = "==4.1.0" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index c7954a72..15f28600 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6ef3be9e49bc02002dd44d1f3b17d6822f638ed1afe1408dcda45de95fa4b043" + "sha256": "cf8a311c51e5d2bbff8df51377979e752f5193526cd931f4158b0b4ff4760990" }, "pipfile-spec": 6, "requires": { @@ -170,7 +170,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "8afb1bb8d20aac1d091886dadaed906b52a1907a" + "ref": "e1cf1fc703d2015897555a5497ea9bf034a50d60" }, "codeforlife-portal": { "hashes": [ @@ -816,7 +816,6 @@ "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" ], - "markers": "python_version >= '3.6'", "version": "==3.1.2" }, "pandas": { @@ -1070,7 +1069,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.2" }, "pytz": { @@ -1222,7 +1221,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sortedcontainers": { @@ -1317,7 +1316,6 @@ "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.0.1" }, "xlwt": { @@ -1768,7 +1766,6 @@ "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" ], - "markers": "python_version >= '3.6'", "version": "==3.1.2" }, "packaging": { @@ -1982,7 +1979,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "snapshottest": { @@ -2123,7 +2120,6 @@ "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.0.1" }, "xlwt": { @@ -2134,4 +2130,4 @@ "version": "==1.3.0" } } -} \ No newline at end of file +} diff --git a/backend/api/serializers/school.py b/backend/api/serializers/school.py index 132c48d5..23909a67 100644 --- a/backend/api/serializers/school.py +++ b/backend/api/serializers/school.py @@ -266,7 +266,6 @@ class SchoolSerializer(_SchoolSerializer): uk_county = serializers.ChoiceField( # type: ignore[assignment] source="county", - default="", choices=[ "Aberdeen City", "Aberdeenshire", diff --git a/backend/api/tests/serializers/test_klass.py b/backend/api/tests/serializers/test_klass.py new file mode 100644 index 00000000..9d49a92b --- /dev/null +++ b/backend/api/tests/serializers/test_klass.py @@ -0,0 +1,51 @@ +""" +© Ocado Group +Created on 05/02/2024 at 15:31:59(+00:00). +""" + +from codeforlife.tests import ModelSerializerTestCase +from codeforlife.user.models import Class, SchoolTeacherUser + +from ...serializers import ClassSerializer + + +# pylint: disable-next=missing-class-docstring +class ClassSerializerTestCase(ModelSerializerTestCase[Class]): + model_serializer_class = ClassSerializer + fixtures = ["school_1"] + + def setUp(self): + self.school_teacher_user = SchoolTeacherUser.objects.get( + email="teacher@school1.com" + ) + self.class_1 = Class.objects.get(name="Class 1 @ School 1") + + def test_validate_name__name_not_unique(self): + """ + Class names must be unique per school. + """ + + self.assert_validate_field( + name="name", + value=self.class_1.name, + error_code="name_not_unique", + user=self.school_teacher_user, + ) + + def test_create(self): + """ + Creates an instance of the Class model. + """ + + self.assert_create( + { + "name": "ExampleClass", + "teacher": { + "id": self.school_teacher_user.teacher.id, + "school": { + "id": self.school_teacher_user.teacher.school.id, + }, + }, + "classmates_data_viewable": False, + } + ) diff --git a/backend/api/tests/views/test_klass.py b/backend/api/tests/views/test_klass.py new file mode 100644 index 00000000..40202d5b --- /dev/null +++ b/backend/api/tests/views/test_klass.py @@ -0,0 +1,101 @@ +""" +© Ocado Group +Created on 05/02/2024 at 16:13:46(+00:00). +""" + +from datetime import timedelta + +from codeforlife.permissions import AllowNone +from codeforlife.tests import ModelViewSetTestCase +from codeforlife.user.models import Class +from codeforlife.user.permissions import InSchool, IsTeacher +from django.utils import timezone + +from ...views import ClassViewSet + + +# pylint: disable-next=missing-class-docstring +class TestClassViewSet(ModelViewSetTestCase[Class]): + basename = "class" + model_view_set_class = ClassViewSet + fixtures = ["school_1"] + + def test_get_permissions__bulk(self): + """ + No one is allowed to perform bulk actions. + """ + + self.assert_get_permissions( + permissions=[AllowNone()], + action="bulk", + ) + + def test_get_permissions__create(self): + """ + Only a school-teacher can create a class. + """ + + self.assert_get_permissions( + permissions=[IsTeacher(), InSchool()], + action="create", + ) + + def test_get_permissions__update(self): + """ + Only a school-teacher can update a class. + """ + + self.assert_get_permissions( + permissions=[IsTeacher(), InSchool()], + action="update", + ) + + def test_get_permissions__destroy(self): + """ + Only a school-teacher can destroy a class. + """ + + self.assert_get_permissions( + permissions=[IsTeacher(), InSchool()], + action="destroy", + ) + + def test_get_permissions__list(self): + """ + Only a school-teacher can list classes. + """ + + self.assert_get_permissions( + permissions=[IsTeacher(), InSchool()], + action="list", + ) + + def test_get_permissions__retrieve(self): + """ + Any school-user can retrieve a class. + """ + + self.assert_get_permissions( + permissions=[InSchool()], + action="retrieve", + ) + + def test_create(self): + """ + Create a new class. + """ + + user = self.client.login_school_teacher( + email="teacher@school1.com", + password="password", + ) + + self.client.create( + { + "name": "ExampleClass", + "school": user.teacher.school.id, + "read_classmates_data": False, + "teacher": user.teacher.id, + "receive_requests_until": timezone.now() + timedelta(days=1), + }, + ) diff --git a/backend/api/tests/views/test_school.py b/backend/api/tests/views/test_school.py index d02d28da..a1e82c7d 100644 --- a/backend/api/tests/views/test_school.py +++ b/backend/api/tests/views/test_school.py @@ -75,7 +75,6 @@ def test_create(self): self.client.login_non_school_teacher( email="teacher@noschool.com", password="password", - in_school=False, ) self.client.create( diff --git a/backend/api/tests/views/test_user.py b/backend/api/tests/views/test_user.py index 1fb4dfcc..f5e068b4 100644 --- a/backend/api/tests/views/test_user.py +++ b/backend/api/tests/views/test_user.py @@ -49,7 +49,7 @@ def test_bulk_create__students(self): klass: t.Optional[Class] = user.teacher.class_teacher.first() assert klass is not None - self.client.bulk_create( + response = self.client.bulk_create( [ { "first_name": "Peter", @@ -59,9 +59,12 @@ def test_bulk_create__students(self): "first_name": "Mary", "student": {"klass": klass.access_code}, }, - ] + ], + make_assertions=False, ) + response.json() # TODO: make custom assertions and check for password + def test_request_password_reset__invalid_email(self): """ Request password reset doesn't generate reset password URL if email diff --git a/backend/api/views/klass.py b/backend/api/views/klass.py index 052109fd..17e5d1ed 100644 --- a/backend/api/views/klass.py +++ b/backend/api/views/klass.py @@ -3,6 +3,8 @@ Created on 23/01/2024 at 17:53:37(+00:00). """ +from codeforlife.permissions import AllowNone +from codeforlife.user.permissions import InSchool, IsTeacher from codeforlife.user.views import ClassViewSet as _ClassViewSet from ..serializers import ClassSerializer @@ -12,3 +14,12 @@ class ClassViewSet(_ClassViewSet): http_method_names = ["get", "post", "patch", "delete"] serializer_class = ClassSerializer + + def get_permissions(self): + # Bulk actions not allowed for classes. + if self.action == "bulk": + return [AllowNone()] + if self.action in ["create", "update", "destroy"]: + return [IsTeacher(), InSchool()] + + return super().get_permissions() diff --git a/backend/api/views/school.py b/backend/api/views/school.py index 54e52d83..30df17f5 100644 --- a/backend/api/views/school.py +++ b/backend/api/views/school.py @@ -3,9 +3,7 @@ Created on 23/01/2024 at 17:53:50(+00:00). """ -import typing as t - -from codeforlife.permissions import NOT, AllowNone, Permission +from codeforlife.permissions import NOT, AllowNone from codeforlife.user.permissions import InSchool, IsTeacher from codeforlife.user.views import SchoolViewSet as _SchoolViewSet @@ -19,7 +17,7 @@ class SchoolViewSet(_SchoolViewSet): def get_permissions(self): # Bulk actions not allowed for schools. - if self.action in ["bulk", "list"]: + if self.action == "bulk": return [AllowNone()] # Only teachers not in a school can create a school. if self.action == "create": @@ -28,4 +26,4 @@ def get_permissions(self): if self.action == "update": return [IsTeacher(is_admin=True), InSchool()] - return [InSchool()] + return super().get_permissions() From 91e54bc047ade33ab15fe43019e6154dfd7ed89f Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 7 Feb 2024 09:32:04 +0000 Subject: [PATCH 5/8] fix: create class --- backend/api/serializers/klass.py | 69 +++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/backend/api/serializers/klass.py b/backend/api/serializers/klass.py index 26caabb3..2c7461a1 100644 --- a/backend/api/serializers/klass.py +++ b/backend/api/serializers/klass.py @@ -3,10 +3,75 @@ Created on 24/01/2024 at 12:14:21(+00:00). """ +import string + +from codeforlife.user.models import Class from codeforlife.user.serializers import ClassSerializer as _ClassSerializer +from django.utils.crypto import get_random_string +from rest_framework import serializers -# pylint: disable-next=missing-class-docstring +# pylint: disable-next=missing-class-docstring,too-many-ancestors class ClassSerializer(_ClassSerializer): + teacher = serializers.IntegerField( + source="teacher.id", + ) + + school = serializers.IntegerField( + source="teacher.school.id", + ) + + read_classmates_data = serializers.BooleanField( + source="classmates_data_viewable", + ) + + receive_requests_until = serializers.DateTimeField( + source="accept_requests_until", + required=False, + ) + class Meta(_ClassSerializer.Meta): - pass + extra_kwargs = { + **_ClassSerializer.Meta.extra_kwargs, + "name": {"read_only": False}, + } + + # TODO: set unique_together=("name", "school") for in new Class model. + # 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, + name=value, + ).exists(): + raise serializers.ValidationError( + "Name already taken.", + code="name_not_unique", + ) + + return value + + def create(self, validated_data): + # TODO: move generation logic to new Class model. + access_code = None + while ( + access_code is None + or Class.objects.filter(access_code=access_code).exists() + ): + access_code = get_random_string( + length=5, + allowed_chars=string.ascii_uppercase + string.digits, + ) + + return super().create( + { + "access_code": access_code, + "name": validated_data["name"], + "teacher_id": validated_data["teacher"]["id"], + "classmates_data_viewable": validated_data[ + "classmates_data_viewable" + ], + "accept_requests_until": validated_data.get( + "accept_requests_until" + ), + } + ) From 07866e3fe61fb39808962d7780a415d238960793 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 7 Feb 2024 17:34:17 +0000 Subject: [PATCH 6/8] fix feedback --- backend/Pipfile | 2 +- backend/Pipfile.lock | 4 +- backend/api/serializers/klass.py | 39 +++++++-- backend/api/tests/serializers/test_klass.py | 86 +++++++++++++++++-- backend/api/tests/serializers/test_student.py | 8 +- backend/api/tests/views/test_klass.py | 35 +++++++- 6 files changed, 149 insertions(+), 25 deletions(-) diff --git a/backend/Pipfile b/backend/Pipfile index 9ea4246b..c71ec6cd 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -28,7 +28,7 @@ google-cloud-logging = "==1.*" google-auth = "==2.*" google-cloud-container = "==2.3.0" # "django-anymail[amazon_ses]" = "==7.0.*" -codeforlife = {ref = "create_class", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} +codeforlife = {ref = "create_class_2", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} django = "==3.2.23" djangorestframework = "==3.13.1" django-cors-headers = "==4.1.0" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 15f28600..c7bf2fc6 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "cf8a311c51e5d2bbff8df51377979e752f5193526cd931f4158b0b4ff4760990" + "sha256": "33ab0343f6ada0199044c9038dddbb3f787ed7eb27ca6ca6bcba3e3be13946a7" }, "pipfile-spec": 6, "requires": { @@ -170,7 +170,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "e1cf1fc703d2015897555a5497ea9bf034a50d60" + "ref": "3c43adbc6acacbbaf7b108d6a7a4c52d0cd40fb4" }, "codeforlife-portal": { "hashes": [ diff --git a/backend/api/serializers/klass.py b/backend/api/serializers/klass.py index 2c7461a1..8b758535 100644 --- a/backend/api/serializers/klass.py +++ b/backend/api/serializers/klass.py @@ -5,7 +5,7 @@ import string -from codeforlife.user.models import Class +from codeforlife.user.models import Class, Teacher from codeforlife.user.serializers import ClassSerializer as _ClassSerializer from django.utils.crypto import get_random_string from rest_framework import serializers @@ -15,10 +15,7 @@ class ClassSerializer(_ClassSerializer): teacher = serializers.IntegerField( source="teacher.id", - ) - - school = serializers.IntegerField( - source="teacher.school.id", + required=False, ) read_classmates_data = serializers.BooleanField( @@ -36,6 +33,29 @@ class Meta(_ClassSerializer.Meta): "name": {"read_only": False}, } + # pylint: disable-next=missing-function-docstring + def validate_teacher(self, value: int): + queryset = Teacher.objects.filter(id=value) + if not queryset.exists(): + raise serializers.ValidationError( + "This teacher does not exist.", + code="does_not_exist", + ) + + 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.", + code="not_in_school", + ) + if value != user.teacher.id and not user.teacher.is_admin: + raise serializers.ValidationError( + "Cannot assign another teacher if you're not admin.", + code="not_admin", + ) + + return value + # TODO: set unique_together=("name", "school") for in new Class model. # pylint: disable-next=missing-function-docstring def validate_name(self, value: str): @@ -59,14 +79,19 @@ def create(self, validated_data): ): access_code = get_random_string( length=5, - allowed_chars=string.ascii_uppercase + string.digits, + allowed_chars=string.ascii_uppercase, ) + # TODO: set school to teacher's school on new Class model. return super().create( { "access_code": access_code, "name": validated_data["name"], - "teacher_id": validated_data["teacher"]["id"], + "teacher_id": ( + validated_data["teacher"]["id"] + if "teacher" in validated_data + else self.request_school_teacher_user.teacher.id + ), "classmates_data_viewable": validated_data[ "classmates_data_viewable" ], diff --git a/backend/api/tests/serializers/test_klass.py b/backend/api/tests/serializers/test_klass.py index 9d49a92b..65e6e973 100644 --- a/backend/api/tests/serializers/test_klass.py +++ b/backend/api/tests/serializers/test_klass.py @@ -4,7 +4,7 @@ """ from codeforlife.tests import ModelSerializerTestCase -from codeforlife.user.models import Class, SchoolTeacherUser +from codeforlife.user.models import Class, SchoolTeacherUser, Teacher from ...serializers import ClassSerializer @@ -20,6 +20,61 @@ def setUp(self): ) self.class_1 = Class.objects.get(name="Class 1 @ School 1") + def test_validate_teacher__does_not_exist(self): + """ + Teacher must exist. + """ + + self.assert_validate_field( + name="teacher", + value=-1, + error_code="does_not_exist", + ) + + def test_validate_teacher__not_in_school(self): + """ + Teacher must be in school. + """ + + teacher = Teacher.objects.exclude( + school=self.school_teacher_user.teacher.school + ).first() + assert teacher + + self.assert_validate_field( + name="teacher", + value=teacher.id, + error_code="not_in_school", + context={ + "request": self.init_request("POST", self.school_teacher_user) + }, + ) + + def test_validate_teacher__not_admin(self): + """ + Teacher cannot assign another teacher if they're not an admin. + """ + + assert not self.school_teacher_user.teacher.is_admin + + teacher = ( + Teacher.objects.filter( + school=self.school_teacher_user.teacher.school + ) + .exclude(pk=self.school_teacher_user.teacher.pk) + .first() + ) + assert teacher + + self.assert_validate_field( + name="teacher", + value=teacher.id, + error_code="not_admin", + context={ + "request": self.init_request("POST", self.school_teacher_user) + }, + ) + def test_validate_name__name_not_unique(self): """ Class names must be unique per school. @@ -29,12 +84,14 @@ def test_validate_name__name_not_unique(self): name="name", value=self.class_1.name, error_code="name_not_unique", - user=self.school_teacher_user, + context={ + "request": self.init_request("POST", self.school_teacher_user) + }, ) - def test_create(self): + def test_create__teacher(self): """ - Creates an instance of the Class model. + Can successfully create with setting the teacher field. """ self.assert_create( @@ -42,10 +99,25 @@ def test_create(self): "name": "ExampleClass", "teacher": { "id": self.school_teacher_user.teacher.id, - "school": { - "id": self.school_teacher_user.teacher.school.id, - }, }, "classmates_data_viewable": False, } ) + + def test_create__no_teacher(self): + """ + Can successfully create without setting the teacher field. + """ + + self.assert_create( + { + "name": "ExampleClass", + "classmates_data_viewable": False, + }, + new_data={ + "teacher": self.school_teacher_user.teacher.id, + }, + context={ + "request": self.init_request("POST", self.school_teacher_user), + }, + ) diff --git a/backend/api/tests/serializers/test_student.py b/backend/api/tests/serializers/test_student.py index c6ba9053..57d6c27a 100644 --- a/backend/api/tests/serializers/test_student.py +++ b/backend/api/tests/serializers/test_student.py @@ -31,7 +31,7 @@ def test_validate_klass__teacher_not_in_school(self): name="klass", value="", error_code="teacher_not_in_school", - user=user, + context={"request": self.init_request("POST", user)}, ) def test_validate_klass__does_not_exist(self): @@ -47,7 +47,7 @@ def test_validate_klass__does_not_exist(self): name="klass", value="", error_code="does_not_exist", - user=user, + context={"request": self.init_request("POST", user)}, ) def test_validate_klass__teacher_not_in_same_school(self): @@ -68,7 +68,7 @@ def test_validate_klass__teacher_not_in_same_school(self): name="klass", value=klass.access_code, error_code="teacher_not_in_same_school", - user=user, + context={"request": self.init_request("POST", user)}, ) def test_validate_klass__teacher_not_admin_or_class_owner(self): @@ -93,5 +93,5 @@ def test_validate_klass__teacher_not_admin_or_class_owner(self): name="klass", value=klass.access_code, error_code="teacher_not_admin_or_class_owner", - user=user, + context={"request": self.init_request("POST", user)}, ) diff --git a/backend/api/tests/views/test_klass.py b/backend/api/tests/views/test_klass.py index 40202d5b..27ed6817 100644 --- a/backend/api/tests/views/test_klass.py +++ b/backend/api/tests/views/test_klass.py @@ -7,7 +7,7 @@ from codeforlife.permissions import AllowNone from codeforlife.tests import ModelViewSetTestCase -from codeforlife.user.models import Class +from codeforlife.user.models import Class, Teacher from codeforlife.user.permissions import InSchool, IsTeacher from django.utils import timezone @@ -80,9 +80,9 @@ def test_get_permissions__retrieve(self): action="retrieve", ) - def test_create(self): + def test_create__self(self): """ - Create a new class. + Teacher can create a class with their self as the class owner. """ user = self.client.login_school_teacher( @@ -95,7 +95,34 @@ def test_create(self): "name": "ExampleClass", "school": user.teacher.school.id, "read_classmates_data": False, - "teacher": user.teacher.id, "receive_requests_until": timezone.now() + timedelta(days=1), }, ) + + def test_create__other(self): + """ + Teacher can create a class with another teacher as the class owner. + """ + + user = self.client.login_school_teacher( + email="admin.teacher@school1.com", + password="password", + is_admin=True, + ) + + teacher = ( + Teacher.objects.filter(school=user.teacher.school) + .exclude(pk=user.teacher.pk) + .first() + ) + assert teacher + + self.client.create( + { + "name": "ExampleClass", + "school": user.teacher.school.id, + "read_classmates_data": False, + "receive_requests_until": timezone.now() + timedelta(days=1), + "teacher": teacher.id, + }, + ) From 37fe76ccbfa984c36699f22fa12164d077a57d0d Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 8 Feb 2024 09:17:25 +0000 Subject: [PATCH 7/8] use py package v0.12.8 --- backend/Pipfile | 2 +- backend/Pipfile.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/Pipfile b/backend/Pipfile index c71ec6cd..eb003004 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -28,7 +28,7 @@ google-cloud-logging = "==1.*" google-auth = "==2.*" google-cloud-container = "==2.3.0" # "django-anymail[amazon_ses]" = "==7.0.*" -codeforlife = {ref = "create_class_2", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} +codeforlife = {ref = "v0.12.8", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} django = "==3.2.23" djangorestframework = "==3.13.1" django-cors-headers = "==4.1.0" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index c7bf2fc6..b0811cad 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "33ab0343f6ada0199044c9038dddbb3f787ed7eb27ca6ca6bcba3e3be13946a7" + "sha256": "eaaeae63a1ea6566c6008ffc0f3ecd38cbb4a3c8e36db2ce148bec03125b0442" }, "pipfile-spec": 6, "requires": { @@ -170,7 +170,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "3c43adbc6acacbbaf7b108d6a7a4c52d0cd40fb4" + "ref": "8e078930e3f1e6796d939cde3bf847711140916d" }, "codeforlife-portal": { "hashes": [ @@ -1069,7 +1069,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, "pytz": { @@ -1221,7 +1221,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "sortedcontainers": { @@ -1979,7 +1979,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "snapshottest": { From 27adc252194141133d2fb3afb96d61ba811e524b Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 8 Feb 2024 10:39:46 +0000 Subject: [PATCH 8/8] feedback --- backend/api/tests/views/test_klass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/api/tests/views/test_klass.py b/backend/api/tests/views/test_klass.py index 27ed6817..550a8c84 100644 --- a/backend/api/tests/views/test_klass.py +++ b/backend/api/tests/views/test_klass.py @@ -82,7 +82,7 @@ def test_get_permissions__retrieve(self): def test_create__self(self): """ - Teacher can create a class with their self as the class owner. + Teacher can create a class with themself as the class owner. """ user = self.client.login_school_teacher(