From b9945a1232c40ae5ff33496955fae595f3861c1d Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 10 Feb 2026 14:37:51 +0000 Subject: [PATCH 01/25] initial --- Pipfile | 7 - Pipfile.lock | 946 ++++-------------- codeforlife/settings/django.py | 8 +- codeforlife/user/fixtures/google_users.json | 6 +- codeforlife/user/fixtures/independent.json | 12 +- .../user/fixtures/non_school_teacher.json | 14 +- codeforlife/user/fixtures/school_1.json | 32 +- codeforlife/user/fixtures/school_2.json | 20 +- codeforlife/user/fixtures/school_3.json | 16 +- codeforlife/user/migrations/0001_initial.py | 627 ++++++++++-- codeforlife/user/models/__init__.py | 1 + codeforlife/user/models/klass.py | 111 +- codeforlife/user/models/other.py | 91 ++ codeforlife/user/models/school.py | 55 +- codeforlife/user/models/student.py | 73 +- codeforlife/user/models/teacher.py | 92 +- codeforlife/user/models/user/google.py | 3 +- codeforlife/user/models/user/independent.py | 8 +- codeforlife/user/models/user/student.py | 11 +- codeforlife/user/models/user/teacher.py | 8 +- codeforlife/user/models/user/user.py | 56 +- settings.py | 3 - 22 files changed, 1274 insertions(+), 926 deletions(-) create mode 100644 codeforlife/user/models/other.py diff --git a/Pipfile b/Pipfile index 2e0a87d9..e43b8293 100644 --- a/Pipfile +++ b/Pipfile @@ -26,13 +26,6 @@ gunicorn = "==23.0.0" uvicorn-worker = "==0.2.0" pyjwt = "==2.6.0" # TODO: upgrade to latest version. psutil = "==7.0.0" -importlib-metadata = "==4.13.0" # TODO: remove. needed by old portal -django-formtools = "==2.5.1" # TODO: remove. needed by old portal -# https://pypi.org/user/codeforlife/ -cfl-common = "==8.9.19" # TODO: remove -codeforlife-portal = "==8.9.19" # TODO: remove -rapid-router = "==7.6.18" # TODO: remove -phonenumbers = "==8.12.12" # TODO: remove google-auth = "==2.40.3" google-cloud-bigquery = "==3.38.0" diff --git a/Pipfile.lock b/Pipfile.lock index 84e2fb5c..0c7242d3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e7f16b2d14b73ca8af83232206b594726dcda0856f51945cdf2410edc86f4472" + "sha256": "89ff1d169339b93c750837aed9cf2af6808f5fd41db2092b6c926b57228205e4" }, "pipfile-spec": 6, "requires": { @@ -26,19 +26,11 @@ }, "asgiref": { "hashes": [ - "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", - "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d" + "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", + "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133" ], "markers": "python_version >= '3.9'", - "version": "==3.11.0" - }, - "asttokens": { - "hashes": [ - "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", - "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7" - ], - "markers": "python_version >= '3.8'", - "version": "==3.0.1" + "version": "==3.11.1" }, "billiard": { "hashes": [ @@ -182,14 +174,6 @@ "markers": "python_version >= '3.9'", "version": "==2.0.0" }, - "cfl-common": { - "hashes": [ - "sha256:02a7f5d44cd8495c7b252bb6980896d45b413f645da42f805f17e7851e1d51db", - "sha256:eba4f98e6b569f2851fda9bde0ca61a46585112494f2319ed1503b47a4de2b9e" - ], - "index": "pypi", - "version": "==8.9.19" - }, "charset-normalizer": { "hashes": [ "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", @@ -340,14 +324,6 @@ "markers": "python_version >= '3.6'", "version": "==0.3.0" }, - "codeforlife-portal": { - "hashes": [ - "sha256:6a181a6742ef189da84fe66a13c01d4136130a0b57c9c90ddcff444c7f9e7fb6", - "sha256:ab894bd62d9b6cb05207f4661fdb78eac0c07384b72c6c83e9a9d00f94aa563f" - ], - "index": "pypi", - "version": "==8.9.19" - }, "cryptography": { "hashes": [ "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", @@ -386,14 +362,6 @@ "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'", "version": "==44.0.1" }, - "decorator": { - "hashes": [ - "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", - "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a" - ], - "markers": "python_version >= '3.8'", - "version": "==5.2.1" - }, "diff-match-patch": { "hashes": [ "sha256:93cea333fb8b2bc0d181b0de5e16df50dd344ce64828226bda07728818936782", @@ -411,14 +379,6 @@ "markers": "python_version >= '3.10'", "version": "==5.1.15" }, - "django-classy-tags": { - "hashes": [ - "sha256:1c784cf1bac49c20a77b8f7d1541867c64076642a160a847ff449588d4e01e55", - "sha256:c8d9d1aa2fa6e71c4d866df4dd11d23a69b8d25bbb750b2490a17b161774ee59" - ], - "markers": "python_version >= '3.8'", - "version": "==4.1.0" - }, "django-cors-headers": { "hashes": [ "sha256:6fdf31bf9c6d6448ba09ef57157db2268d515d94fc5c89a0a1028e1fc03ee52b", @@ -458,7 +418,6 @@ "sha256:47cb34552c6efca088863d693284d04fc36eaaf350eb21e1a1d935e0df523c93", "sha256:bce9b64eda52cc1eef6961cc649cf75aacd1a707c2fff08d6c3efcbc8e7e761a" ], - "index": "pypi", "markers": "python_version >= '3.8'", "version": "==2.5.1" }, @@ -487,51 +446,6 @@ "markers": "python_version >= '3.10'", "version": "==8.4.0" }, - "django-pipeline": { - "hashes": [ - "sha256:0ab1190d9dc64e2f7b72be3f7b023c06aca7cc1cc61e7dc9f0343838e29bbc88", - "sha256:90e50c15387a6e051ee1a6ce2aaca333823ccfb23695028790f74412bde7d7db" - ], - "markers": "python_version >= '3.9'", - "version": "==4.0.0" - }, - "django-preventconcurrentlogins": { - "hashes": [ - "sha256:9cb45fcd63edeec55e5ac29bbd2ee96974dc2a72d74ab88088dbf6a1f52978e9" - ], - "version": "==0.8.2" - }, - "django-ratelimit": { - "hashes": [ - "sha256:73223d860abd5c5d7b9a807fabb39a6220068129b514be8d78044b52607ab154", - "sha256:857e797f23de948b204a31dba9d88aea3ce731b7a5d926d0240c772e19b5486f" - ], - "markers": "python_version >= '3.4'", - "version": "==3.0.1" - }, - "django-recaptcha": { - "hashes": [ - "sha256:0d912d5c7c009df4e47accd25029133d47a74342dbd2a8edc2877b6bffa971a3", - "sha256:5316438f97700c431d65351470d1255047e3f2cd9af0f2f13592b637dad9213e" - ], - "version": "==4.0.0" - }, - "django-reverse-js": { - "hashes": [ - "sha256:42739b2d955704cb723467655c67278602695d2aae6e7595ff80db323c73e958", - "sha256:e9e604aaf8c5cda7c1b1cb9a1a78cff395f7937935085d22bcd484016512a069" - ], - "markers": "python_version >= '3.10'", - "version": "==0.1.8" - }, - "django-sekizai": { - "hashes": [ - "sha256:2aca36cbae0b5c0cefed9565416ec442335767fb3145bff11e58622fc653cdad", - "sha256:aa12e66ba0335fbe726b7d74cf4e8716b89a0be99a1304a9b9e8b191229e2e4a" - ], - "markers": "python_version >= '3.8'", - "version": "==4.1.0" - }, "django-storages": { "extras": [ "s3" @@ -543,14 +457,6 @@ "markers": "python_version >= '3.7'", "version": "==1.14.6" }, - "django-treebeard": { - "hashes": [ - "sha256:846e462904b437155f76e04907ba4e48480716855f88b898df4122bdcfbd6e98", - "sha256:995c7120153ab999898fe3043bbdcd8a0fc77cc106eb94de7350e9d02c885135" - ], - "markers": "python_version >= '3.8'", - "version": "==4.7.1" - }, "django-two-factor-auth": { "hashes": [ "sha256:622e78b0d6cf12eeafa239665d99c1221c399228f2f902fe478aea7759995e0e", @@ -569,14 +475,6 @@ "markers": "python_version >= '3.9'", "version": "==3.16.0" }, - "executing": { - "hashes": [ - "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", - "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017" - ], - "markers": "python_version >= '3.8'", - "version": "==2.2.1" - }, "google-api-core": { "extras": [ "grpc" @@ -671,78 +569,78 @@ }, "grpcio": { "hashes": [ - "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", - "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", - "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", - "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", - "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", - "sha256:0aaa82d0813fd4c8e589fac9b65d7dd88702555f702fb10417f96e2a2a6d4c0f", - "sha256:0b7604868b38c1bfd5cf72d768aedd7db41d78cb6a4a18585e33fb0f9f2363fd", - "sha256:0c37db8606c258e2ee0c56b78c62fc9dee0e901b5dbdcf816c2dd4ad652b8b0c", - "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", - "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", - "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", - "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", - "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", - "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", - "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", - "sha256:3bf0f392c0b806905ed174dcd8bdd5e418a40d5567a05615a030a5aeddea692d", - "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", - "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", - "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", - "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", - "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", - "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", - "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", - "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", - "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", - "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", - "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", - "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", - "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", - "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", - "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", - "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", - "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", - "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", - "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", - "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", - "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", - "sha256:8ebe63ee5f8fa4296b1b8cfc743f870d10e902ca18afc65c68cf46fd39bb0783", - "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", - "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", - "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", - "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", - "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", - "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", - "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", - "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", - "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", - "sha256:acab0277c40eff7143c2323190ea57b9ee5fd353d8190ee9652369fae735668a", - "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", - "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", - "sha256:d099566accf23d21037f18a2a63d323075bebace807742e4b0ac210971d4dd70", - "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", - "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", - "sha256:e6d1db20594d9daba22f90da738b1a0441a7427552cc6e2e3d1297aeddc00378", - "sha256:ebea5cc3aa8ea72e04df9913492f9a96d9348db876f9dda3ad729cfedf7ac416", - "sha256:ebebf83299b0cb1721a8859ea98f3a77811e35dce7609c5c963b9ad90728f886", - "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", - "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", - "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", - "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", - "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62" + "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", + "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", + "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", + "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", + "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", + "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", + "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", + "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", + "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", + "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", + "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", + "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", + "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", + "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", + "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", + "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", + "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", + "sha256:5361a0630a7fdb58a6a97638ab70e1dae2893c4d08d7aba64ded28bb9e7a29df", + "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", + "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", + "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", + "sha256:684083fd383e9dc04c794adb838d4faea08b291ce81f64ecd08e4577c7398adf", + "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", + "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", + "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", + "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", + "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", + "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", + "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", + "sha256:86ce2371bfd7f212cf60d8517e5e854475c2c43ce14aa910e136ace72c6db6c1", + "sha256:86f85dd7c947baa707078a236288a289044836d4b640962018ceb9cd1f899af5", + "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", + "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", + "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", + "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", + "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", + "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", + "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", + "sha256:ab399ef5e3cd2a721b1038a0f3021001f19c5ab279f145e1146bb0b9f1b2b12c", + "sha256:b0c689c02947d636bc7fab3e30cc3a3445cca99c834dfb77cd4a6cabfc1c5597", + "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", + "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", + "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", + "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", + "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", + "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", + "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", + "sha256:ce7599575eeb25c0f4dc1be59cada6219f3b56176f799627f44088b21381a28a", + "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", + "sha256:de8cb00d1483a412a06394b8303feec5dcb3b55f81d83aa216dbb6a0b86a94f5", + "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", + "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", + "sha256:e888474dee2f59ff68130f8a397792d8cb8e17e6b3434339657ba4ee90845a8c", + "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", + "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", + "sha256:f3d6379493e18ad4d39537a82371c5281e153e963cecb13f953ebac155756525", + "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", + "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", + "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", + "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", + "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7" ], "markers": "python_version >= '3.9'", - "version": "==1.76.0" + "version": "==1.78.0" }, "grpcio-status": { "hashes": [ - "sha256:25fcbfec74c15d1a1cb5da3fab8ee9672852dc16a5a9eeb5baf7d7a9952943cd", - "sha256:380568794055a8efbbd8871162df92012e0228a5f6dffaf57f2a00c534103b18" + "sha256:a34cfd28101bfea84b5aa0f936b4b423019e9213882907166af6b3bddc59e189", + "sha256:b492b693d4bf27b47a6c32590701724f1d3b9444b36491878fb71f6208857f34" ], "markers": "python_version >= '3.9'", - "version": "==1.76.0" + "version": "==1.78.0" }, "gunicorn": { "hashes": [ @@ -880,39 +778,6 @@ "markers": "python_version >= '3.8'", "version": "==3.11" }, - "importlib-metadata": { - "hashes": [ - "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116", - "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==4.13.0" - }, - "ipython": { - "hashes": [ - "sha256:48fbed1b2de5e2c7177eefa144aba7fcb82dac514f09b57e2ac9da34ddb54220", - "sha256:b457fe9165df2b84e8ec909a97abcf2ed88f565970efba16b1f7229c283d252b" - ], - "markers": "python_version >= '3.11'", - "version": "==9.9.0" - }, - "ipython-pygments-lexers": { - "hashes": [ - "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", - "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c" - ], - "markers": "python_version >= '3.8'", - "version": "==1.1.1" - }, - "jedi": { - "hashes": [ - "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", - "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9" - ], - "markers": "python_version >= '3.6'", - "version": "==0.19.2" - }, "jmespath": { "hashes": [ "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", @@ -932,112 +797,6 @@ "markers": "python_version >= '3.9'", "version": "==5.6.2" }, - "libsass": { - "hashes": [ - "sha256:31e86d92a5c7a551df844b72d83fc2b5e50abc6fbbb31e296f7bebd6489ed1b4", - "sha256:34cae047cbbfc4ffa832a61cbb110f3c95f5471c6170c842d3fed161e40814dc", - "sha256:4a218406d605f325d234e4678bd57126a66a88841cb95bee2caeafdc6f138306", - "sha256:6f209955ede26684e76912caf329f4ccb57e4a043fd77fe0e7348dd9574f1880", - "sha256:a2ec85d819f353cbe807432d7275d653710d12b08ec7ef61c124a580a8352f3c", - "sha256:ea97d1b45cdc2fc3590cb9d7b60f1d8915d3ce17a98c1f2d4dd47ee0d9c68ce6" - ], - "markers": "python_version >= '3.8'", - "version": "==0.23.0" - }, - "matplotlib-inline": { - "hashes": [ - "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", - "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe" - ], - "markers": "python_version >= '3.9'", - "version": "==0.2.1" - }, - "more-itertools": { - "hashes": [ - "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced", - "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713" - ], - "markers": "python_version >= '3.5'", - "version": "==8.7.0" - }, - "numpy": { - "hashes": [ - "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", - "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", - "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", - "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", - "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", - "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", - "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", - "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", - "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", - "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", - "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", - "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", - "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", - "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", - "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", - "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", - "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", - "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", - "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", - "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", - "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", - "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", - "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", - "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", - "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", - "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", - "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", - "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", - "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", - "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", - "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", - "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", - "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", - "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", - "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", - "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", - "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", - "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", - "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", - "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", - "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", - "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", - "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", - "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", - "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", - "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", - "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", - "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", - "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", - "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", - "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", - "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", - "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", - "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", - "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", - "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", - "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", - "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", - "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", - "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", - "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", - "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", - "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", - "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", - "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", - "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", - "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", - "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", - "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", - "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", - "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", - "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c" - ], - "markers": "python_version >= '3.11'", - "version": "==2.4.1" - }, "packaging": { "hashes": [ "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", @@ -1046,189 +805,6 @@ "markers": "python_version >= '3.8'", "version": "==26.0" }, - "pandas": { - "hashes": [ - "sha256:0192fee1f1a8e743b464a6607858ee4b071deb0b118eb143d71c2a1d170996d5", - "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", - "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", - "sha256:113b4cca2614ff7e5b9fee9b6f066618fe73c5a83e99d721ffc41217b2bf57dd", - "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", - "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", - "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", - "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", - "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", - "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", - "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", - "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", - "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", - "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", - "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", - "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", - "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", - "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", - "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", - "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", - "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", - "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2", - "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", - "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", - "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", - "sha256:707a9a877a876c326ae2cb640fbdc4ef63b0a7b9e2ef55c6df9942dcee8e2af9", - "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", - "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", - "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", - "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", - "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", - "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", - "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", - "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", - "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", - "sha256:afd0aa3d0b5cda6e0b8ffc10dbcca3b09ef3cbcd3fe2b27364f85fdc04e1989d", - "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", - "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", - "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", - "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", - "sha256:c14837eba8e99a8da1527c0280bba29b0eb842f64aa94982c5e21227966e164b", - "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", - "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850", - "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", - "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", - "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", - "sha256:f0b853319dec8d5e0c8b875374c078ef17f2269986a78168d9bd57e49bf650ae", - "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d" - ], - "markers": "python_version >= '3.11'", - "version": "==3.0.0" - }, - "parso": { - "hashes": [ - "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", - "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887" - ], - "markers": "python_version >= '3.6'", - "version": "==0.8.5" - }, - "pexpect": { - "hashes": [ - "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", - "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f" - ], - "markers": "sys_platform != 'win32' and sys_platform != 'emscripten'", - "version": "==4.9.0" - }, - "pgeocode": { - "hashes": [ - "sha256:07995d4cd2d7fec1f82afb14d6025e83bbc156b6f225fa3e0b3417da2ec020c8", - "sha256:60fc2bad60aa161c3cf46ace4fde607b77e016b1e2a25470534163305499e55e" - ], - "markers": "python_version >= '3.8'", - "version": "==0.4.0" - }, - "phonenumbers": { - "hashes": [ - "sha256:23944f9e628f32a975d3b221b6d76e6ba8ae618d53cb3d82fc23d9e100a59b29", - "sha256:70aa98a50ba7bc7f6bf17851f806c927107e7c44e7d21eb46bdbec07b99d23ae" - ], - "index": "pypi", - "version": "==8.12.12" - }, - "pillow": { - "hashes": [ - "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", - "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", - "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", - "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", - "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", - "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", - "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", - "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", - "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", - "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", - "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", - "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", - "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", - "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", - "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", - "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", - "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", - "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", - "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", - "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", - "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", - "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", - "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", - "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", - "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", - "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", - "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", - "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", - "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", - "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", - "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", - "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", - "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", - "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", - "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", - "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", - "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", - "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", - "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", - "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", - "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", - "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", - "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", - "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", - "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", - "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", - "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", - "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", - "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", - "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", - "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", - "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", - "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", - "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", - "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", - "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", - "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", - "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", - "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", - "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", - "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", - "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", - "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", - "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", - "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", - "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", - "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", - "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", - "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", - "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", - "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", - "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", - "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", - "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", - "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", - "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", - "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", - "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", - "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", - "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", - "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", - "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", - "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", - "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", - "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", - "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", - "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", - "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", - "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", - "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", - "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd" - ], - "markers": "python_version >= '3.10'", - "version": "==12.1.0" - }, "prompt-toolkit": { "hashes": [ "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", @@ -1239,11 +815,11 @@ }, "proto-plus": { "hashes": [ - "sha256:1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82", - "sha256:873af56dd0d7e91836aee871e5799e1c6f1bda86ac9a983e0bb9f0c266a568c4" + "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147", + "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc" ], "markers": "python_version >= '3.7'", - "version": "==1.27.0" + "version": "==1.27.1" }, "protobuf": { "hashes": [ @@ -1357,20 +933,6 @@ "markers": "python_version >= '3.7'", "version": "==2.9.9" }, - "ptyprocess": { - "hashes": [ - "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", - "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" - ], - "version": "==0.7.0" - }, - "pure-eval": { - "hashes": [ - "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", - "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42" - ], - "version": "==0.2.3" - }, "pyasn1": { "hashes": [ "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", @@ -1444,22 +1006,6 @@ "markers": "python_version >= '3.5'", "version": "==7.45.7" }, - "pygments": { - "hashes": [ - "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", - "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" - ], - "markers": "python_version >= '3.8'", - "version": "==2.19.2" - }, - "pyhamcrest": { - "hashes": [ - "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", - "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" - ], - "markers": "python_version >= '3.5'", - "version": "==2.0.2" - }, "pyjwt": { "hashes": [ "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd", @@ -1502,65 +1048,6 @@ "markers": "python_version >= '3.8'", "version": "==1.0.1" }, - "pyyaml": { - "hashes": [ - "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", - "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", - "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", - "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", - "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", - "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", - "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", - "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", - "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", - "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", - "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", - "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", - "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", - "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", - "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", - "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", - "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", - "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", - "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", - "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", - "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", - "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", - "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", - "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", - "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", - "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", - "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", - "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", - "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", - "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", - "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", - "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", - "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", - "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", - "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", - "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", - "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", - "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", - "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", - "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", - "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", - "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", - "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", - "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", - "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", - "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", - "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", - "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", - "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", - "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", - "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", - "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", - "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" - ], - "markers": "python_version >= '3.8'", - "version": "==6.0.2" - }, "qrcode": { "hashes": [ "sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a", @@ -1569,14 +1056,6 @@ "markers": "python_version >= '3.7'", "version": "==7.4.2" }, - "rapid-router": { - "hashes": [ - "sha256:736a37ef2a9a2592add7ba0a885c08228d81fe15a773a194ecb93cece793c26c", - "sha256:b18545684c1707c9889c8e1bdaf68db84b9a7364da6273c4de5e04d30116ce37" - ], - "index": "pypi", - "version": "==7.6.18" - }, "redis": { "extras": [ "hiredis" @@ -1689,14 +1168,6 @@ "markers": "python_version >= '3.8'", "version": "==2024.11.6" }, - "reportlab": { - "hashes": [ - "sha256:58e11be387457928707c12153b7e41e52533a5da3f587b15ba8f8fd0805c6ee2", - "sha256:fc6283048ddd0781a9db1d671715990e6aa059c8d40ec9baf34294c4bd583a36" - ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==4.4.2" - }, "requests": { "hashes": [ "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", @@ -1706,14 +1177,6 @@ "markers": "python_version >= '3.9'", "version": "==2.32.5" }, - "requests-toolbelt": { - "hashes": [ - "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", - "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.0.0" - }, "rsa": { "hashes": [ "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", @@ -1730,14 +1193,6 @@ "markers": "python_version >= '3.8'", "version": "==0.11.3" }, - "setuptools": { - "hashes": [ - "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", - "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173" - ], - "markers": "python_version >= '3.9'", - "version": "==80.10.2" - }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", @@ -1754,13 +1209,6 @@ "markers": "python_version >= '3.8'", "version": "==0.5.5" }, - "stack-data": { - "hashes": [ - "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", - "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695" - ], - "version": "==0.6.3" - }, "tablib": { "hashes": [ "sha256:9a6930037cfe0f782377963ca3f2b1dae3fd4cdbf0883848f22f1447e7bb718b", @@ -1769,14 +1217,6 @@ "markers": "python_version >= '3.9'", "version": "==3.7.0" }, - "traitlets": { - "hashes": [ - "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", - "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f" - ], - "markers": "python_version >= '3.8'", - "version": "==5.14.3" - }, "typing-extensions": { "hashes": [ "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", @@ -1828,45 +1268,21 @@ }, "wcwidth": { "hashes": [ - "sha256:1efe1361b83b0ff7877b81ba57c8562c99cf812158b778988ce17ec061095695", - "sha256:f89c103c949a693bf563377b2153082bf58e309919dfb7f27b04d862a0089333" + "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", + "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159" ], "markers": "python_version >= '3.8'", - "version": "==0.5.0" - }, - "wheel": { - "hashes": [ - "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d", - "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803" - ], - "markers": "python_version >= '3.9'", - "version": "==0.46.3" - }, - "whitenoise": { - "hashes": [ - "sha256:8c4a7c9d384694990c26f3047e118c691557481d624f069b7f7752a2f735d609", - "sha256:c8a489049b7ee9889617bb4c274a153f3d979e8f51d2efd0f5b403caf41c57df" - ], - "markers": "python_version >= '3.9'", - "version": "==6.9.0" - }, - "zipp": { - "hashes": [ - "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", - "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" - ], - "markers": "python_version >= '3.9'", - "version": "==3.23.0" + "version": "==0.6.0" } }, "develop": { "asgiref": { "hashes": [ - "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", - "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d" + "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", + "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133" ], "markers": "python_version >= '3.9'", - "version": "==3.11.0" + "version": "==3.11.1" }, "astroid": { "hashes": [ @@ -1918,11 +1334,11 @@ }, "botocore-stubs": { "hashes": [ - "sha256:96cf63b6949264f078f5009fa72932c6fd06a88c7bde4e21ed96df6c37e8edac", - "sha256:e21dadac08ee134cc0713884545c01b8eb1552c77e0d0c55c05e9584590e2b28" + "sha256:9423110fb0e391834bd2ed44ae5f879d8cb370a444703d966d30842ce2bcb5f0", + "sha256:dbeac2f744df6b814ce83ec3f3777b299a015cbea57a2efc41c33b8c38265825" ], "markers": "python_version >= '3.9'", - "version": "==1.42.38" + "version": "==1.42.41" }, "celery-types": { "hashes": [ @@ -2073,101 +1489,115 @@ "toml" ], "hashes": [ - "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", - "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", - "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", - "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", - "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", - "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", - "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", - "sha256:1574983178b35b9af4db4a9f7328a18a14a0a0ce76ffaa1c1bacb4cc82089a7c", - "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", - "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", - "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", - "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", - "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", - "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", - "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", - "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", - "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2", - "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", - "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", - "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", - "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", - "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", - "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f", - "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", - "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", - "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", - "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", - "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5", - "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", - "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", - "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", - "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", - "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", - "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", - "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", - "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", - "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad", - "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", - "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", - "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", - "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31", - "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", - "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", - "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", - "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", - "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", - "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", - "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", - "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", - "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", - "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", - "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", - "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", - "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", - "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc", - "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896", - "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", - "sha256:a360a8baeb038928ceb996f5623a4cd508728f8f13e08d4e96ce161702f3dd99", - "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", - "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", - "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", - "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", - "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", - "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", - "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", - "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", - "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", - "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", - "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", - "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", - "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", - "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c", - "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", - "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", - "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", - "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", - "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", - "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", - "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", - "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8", - "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", - "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", - "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", - "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", - "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", - "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", - "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b", - "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", - "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", - "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", - "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", - "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99" + "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", + "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", + "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", + "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", + "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", + "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", + "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", + "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", + "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", + "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", + "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", + "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", + "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", + "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", + "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", + "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", + "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", + "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", + "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", + "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", + "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", + "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", + "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", + "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", + "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", + "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", + "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", + "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", + "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", + "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", + "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", + "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", + "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", + "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", + "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", + "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", + "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", + "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", + "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", + "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", + "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", + "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", + "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", + "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", + "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", + "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", + "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", + "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", + "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", + "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", + "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", + "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", + "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", + "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", + "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", + "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", + "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", + "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", + "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", + "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", + "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", + "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", + "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", + "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", + "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", + "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", + "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", + "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", + "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", + "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", + "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", + "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", + "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", + "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", + "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", + "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", + "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", + "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", + "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", + "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", + "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", + "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", + "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", + "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", + "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", + "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", + "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", + "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", + "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", + "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", + "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", + "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", + "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", + "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", + "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", + "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", + "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", + "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", + "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", + "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", + "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", + "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", + "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", + "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", + "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", + "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0" ], "markers": "python_version >= '3.10'", - "version": "==7.13.2" + "version": "==7.13.4" }, "dill": { "hashes": [ diff --git a/codeforlife/settings/django.py b/codeforlife/settings/django.py index ff4ee774..330c0d77 100644 --- a/codeforlife/settings/django.py +++ b/codeforlife/settings/django.py @@ -235,6 +235,11 @@ def get_databases(): WSGI_APPLICATION = "application.django_wsgi" +# Custom user model +# https://docs.djangoproject.com/en/6.0/topics/auth/customizing/#auth-custom-user + +AUTH_USER_MODEL = "user.User" + # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators @@ -275,9 +280,6 @@ def get_databases(): "django.contrib.messages", "django.contrib.sites", "django.contrib.staticfiles", - "game", # TODO: remove - "portal", # TODO: remove - "common", # TODO: remove "src", "codeforlife.user", "corsheaders", diff --git a/codeforlife/user/fixtures/google_users.json b/codeforlife/user/fixtures/google_users.json index 6a075828..f56b4119 100644 --- a/codeforlife/user/fixtures/google_users.json +++ b/codeforlife/user/fixtures/google_users.json @@ -1,6 +1,6 @@ [ { - "model": "auth.user", + "model": "user.user", "pk": 34, "fields": { "first_name": "Google", @@ -11,7 +11,7 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 34, "fields": { "user": 34, @@ -21,7 +21,7 @@ } }, { - "model": "common.teacher", + "model": "user.teacher", "pk": 13, "fields": { "user": 34, diff --git a/codeforlife/user/fixtures/independent.json b/codeforlife/user/fixtures/independent.json index 00e5c7bd..71d2f958 100644 --- a/codeforlife/user/fixtures/independent.json +++ b/codeforlife/user/fixtures/independent.json @@ -1,6 +1,6 @@ [ { - "model": "auth.user", + "model": "user.user", "pk": 28, "fields": { "first_name": "Indy", @@ -11,7 +11,7 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 28, "fields": { "user": 28, @@ -19,7 +19,7 @@ } }, { - "model": "common.student", + "model": "user.student", "pk": 18, "fields": { "user": 28, @@ -28,7 +28,7 @@ } }, { - "model": "auth.user", + "model": "user.user", "pk": 30, "fields": { "first_name": "Indy", @@ -39,7 +39,7 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 30, "fields": { "user": 30, @@ -47,7 +47,7 @@ } }, { - "model": "common.student", + "model": "user.student", "pk": 20, "fields": { "user": 30, diff --git a/codeforlife/user/fixtures/non_school_teacher.json b/codeforlife/user/fixtures/non_school_teacher.json index 55f015c4..1bcc2b7b 100644 --- a/codeforlife/user/fixtures/non_school_teacher.json +++ b/codeforlife/user/fixtures/non_school_teacher.json @@ -1,6 +1,6 @@ [ { - "model": "auth.user", + "model": "user.user", "pk": 22, "fields": { "first_name": "John", @@ -11,7 +11,7 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 22, "fields": { "user": 22, @@ -19,7 +19,7 @@ } }, { - "model": "common.teacher", + "model": "user.teacher", "pk": 5, "fields": { "user": 22, @@ -27,7 +27,7 @@ } }, { - "model": "auth.user", + "model": "user.user", "pk": 33, "fields": { "first_name": "Unverified", @@ -38,7 +38,7 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 33, "fields": { "user": 33, @@ -46,11 +46,11 @@ } }, { - "model": "common.teacher", + "model": "user.teacher", "pk": 12, "fields": { "user": 33, "new_user": 33 } } -] \ No newline at end of file +] diff --git a/codeforlife/user/fixtures/school_1.json b/codeforlife/user/fixtures/school_1.json index 9955f700..444fa023 100644 --- a/codeforlife/user/fixtures/school_1.json +++ b/codeforlife/user/fixtures/school_1.json @@ -1,6 +1,6 @@ [ { - "model": "common.school", + "model": "user.school", "pk": 2, "fields": { "name": "School 1", @@ -9,7 +9,7 @@ } }, { - "model": "auth.user", + "model": "user.user", "pk": 23, "fields": { "first_name": "John", @@ -20,7 +20,7 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 23, "fields": { "user": 23, @@ -28,7 +28,7 @@ } }, { - "model": "common.teacher", + "model": "user.teacher", "pk": 6, "fields": { "user": 23, @@ -37,7 +37,7 @@ } }, { - "model": "common.class", + "model": "user.class", "pk": 6, "fields": { "name": "Class 1 @ School 1", @@ -47,7 +47,7 @@ } }, { - "model": "auth.user", + "model": "user.user", "pk": 27, "fields": { "first_name": "Student1", @@ -56,7 +56,7 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 27, "fields": { "user": 27, @@ -64,7 +64,7 @@ } }, { - "model": "common.student", + "model": "user.student", "pk": 17, "fields": { "user": 27, @@ -73,7 +73,7 @@ } }, { - "model": "auth.user", + "model": "user.user", "pk": 24, "fields": { "first_name": "Jane", @@ -84,7 +84,7 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 24, "fields": { "user": 24, @@ -92,7 +92,7 @@ } }, { - "model": "common.teacher", + "model": "user.teacher", "pk": 7, "fields": { "user": 24, @@ -102,7 +102,7 @@ } }, { - "model": "common.class", + "model": "user.class", "pk": 7, "fields": { "name": "Class 2 @ School 1", @@ -111,7 +111,7 @@ } }, { - "model": "auth.user", + "model": "user.user", "pk": 29, "fields": { "first_name": "Student2", @@ -120,7 +120,7 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 29, "fields": { "user": 29, @@ -128,7 +128,7 @@ } }, { - "model": "common.student", + "model": "user.student", "pk": 19, "fields": { "user": 29, @@ -137,7 +137,7 @@ } }, { - "model": "common.class", + "model": "user.class", "pk": 10, "fields": { "name": "Class 3 @ School 1", diff --git a/codeforlife/user/fixtures/school_2.json b/codeforlife/user/fixtures/school_2.json index 2fd9b02a..9711cadf 100644 --- a/codeforlife/user/fixtures/school_2.json +++ b/codeforlife/user/fixtures/school_2.json @@ -1,6 +1,6 @@ [ { - "model": "common.school", + "model": "user.school", "pk": 3, "fields": { "name": "School 2", @@ -9,7 +9,7 @@ } }, { - "model": "auth.user", + "model": "user.user", "pk": 25, "fields": { "first_name": "John", @@ -20,7 +20,7 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 25, "fields": { "user": 25, @@ -117,7 +117,7 @@ } }, { - "model": "common.teacher", + "model": "user.teacher", "pk": 8, "fields": { "user": 25, @@ -126,7 +126,7 @@ } }, { - "model": "common.class", + "model": "user.class", "pk": 8, "fields": { "name": "Class 1 @ School 2", @@ -135,7 +135,7 @@ } }, { - "model": "auth.user", + "model": "user.user", "pk": 26, "fields": { "first_name": "Jane", @@ -146,7 +146,7 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 26, "fields": { "user": 26, @@ -163,7 +163,7 @@ } }, { - "model": "common.teacher", + "model": "user.teacher", "pk": 9, "fields": { "user": 26, @@ -173,7 +173,7 @@ } }, { - "model": "common.class", + "model": "user.class", "pk": 9, "fields": { "name": "Class 2 @ School 2", @@ -181,4 +181,4 @@ "teacher": 9 } } -] \ No newline at end of file +] diff --git a/codeforlife/user/fixtures/school_3.json b/codeforlife/user/fixtures/school_3.json index ea2c1173..bbd1ec10 100644 --- a/codeforlife/user/fixtures/school_3.json +++ b/codeforlife/user/fixtures/school_3.json @@ -1,6 +1,6 @@ [ { - "model": "common.school", + "model": "user.school", "pk": 4, "fields": { "name": "School 3", @@ -9,7 +9,7 @@ } }, { - "model": "auth.user", + "model": "user.user", "pk": 31, "fields": { "first_name": "Peter", @@ -20,7 +20,7 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 31, "fields": { "user": 31, @@ -28,7 +28,7 @@ } }, { - "model": "common.teacher", + "model": "user.teacher", "pk": 10, "fields": { "user": 31, @@ -38,7 +38,7 @@ } }, { - "model": "auth.user", + "model": "user.user", "pk": 32, "fields": { "first_name": "Doctor", @@ -49,7 +49,7 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 32, "fields": { "user": 32, @@ -57,7 +57,7 @@ } }, { - "model": "common.teacher", + "model": "user.teacher", "pk": 11, "fields": { "user": 32, @@ -65,4 +65,4 @@ "school": 4 } } -] \ No newline at end of file +] diff --git a/codeforlife/user/migrations/0001_initial.py b/codeforlife/user/migrations/0001_initial.py index c48171a9..ca28de0e 100644 --- a/codeforlife/user/migrations/0001_initial.py +++ b/codeforlife/user/migrations/0001_initial.py @@ -1,9 +1,21 @@ -# Generated by Django 5.1.10 on 2025-08-12 10:29 +# Generated by Django 5.1.15 on 2026-02-10 14:27 import codeforlife.models.encrypted_char_field -import codeforlife.user.models.user +import codeforlife.user.models.user.admin_school_teacher +import codeforlife.user.models.user.contactable +import codeforlife.user.models.user.google +import codeforlife.user.models.user.independent +import codeforlife.user.models.user.non_admin_school_teacher +import codeforlife.user.models.user.non_school_teacher +import codeforlife.user.models.user.school_teacher +import codeforlife.user.models.user.student +import codeforlife.user.models.user.teacher import django.contrib.auth.models +import django.contrib.auth.validators import django.db.models.deletion +import django.utils.timezone +import django_countries.fields +from django.conf import settings from django.db import migrations, models @@ -13,52 +25,327 @@ class Migration(migrations.Migration): dependencies = [ ("auth", "0012_alter_user_first_name_max_length"), - ("common", "0057_teacher_teacher__is_admin"), ] operations = [ + migrations.CreateModel( + name="Class", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=200)), + ("access_code", models.CharField(max_length=5, null=True)), + ("classmates_data_viewable", models.BooleanField(default=False)), + ("always_accept_requests", models.BooleanField(default=False)), + ("accept_requests_until", models.DateTimeField(null=True)), + ( + "creation_time", + models.DateTimeField(default=django.utils.timezone.now, null=True), + ), + ("is_active", models.BooleanField(default=True)), + ], + options={ + "verbose_name_plural": "classes", + }, + ), + migrations.CreateModel( + name="DailyActivity", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField(default=django.utils.timezone.now)), + ("csv_click_count", models.PositiveIntegerField(default=0)), + ("login_cards_click_count", models.PositiveIntegerField(default=0)), + ( + "primary_coding_club_downloads", + models.PositiveIntegerField(default=0), + ), + ( + "python_coding_club_downloads", + models.PositiveIntegerField(default=0), + ), + ("level_control_submits", models.PositiveBigIntegerField(default=0)), + ("teacher_lockout_resets", models.PositiveIntegerField(default=0)), + ("indy_lockout_resets", models.PositiveIntegerField(default=0)), + ( + "school_student_lockout_resets", + models.PositiveIntegerField(default=0), + ), + ( + "anonymised_unverified_teachers", + models.PositiveIntegerField(default=0), + ), + ( + "anonymised_unverified_independents", + models.PositiveIntegerField(default=0), + ), + ], + options={ + "verbose_name_plural": "Daily activities", + }, + ), + migrations.CreateModel( + name="School", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=200, unique=True)), + ( + "country", + django_countries.fields.CountryField( + blank=True, max_length=2, null=True + ), + ), + ("county", models.CharField(blank=True, max_length=50, null=True)), + ( + "creation_time", + models.DateTimeField(default=django.utils.timezone.now, null=True), + ), + ("is_active", models.BooleanField(default=True)), + ], + ), + migrations.CreateModel( + name="TotalActivity", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("teacher_registrations", models.PositiveIntegerField(default=0)), + ("student_registrations", models.PositiveIntegerField(default=0)), + ("independent_registrations", models.PositiveIntegerField(default=0)), + ( + "anonymised_unverified_teachers", + models.PositiveIntegerField(default=0), + ), + ( + "anonymised_unverified_independents", + models.PositiveIntegerField(default=0), + ), + ], + options={ + "verbose_name_plural": "Total activity", + }, + ), migrations.CreateModel( name="User", - fields=[], + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], options={ - "proxy": True, - "indexes": [], - "constraints": [], + "abstract": False, }, - bases=("auth.user", models.Model), managers=[ ("objects", django.contrib.auth.models.UserManager()), ], ), migrations.CreateModel( - name="SchoolTeacher", + name="ContactableUser", fields=[], options={ "proxy": True, "indexes": [], "constraints": [], }, - bases=("common.teacher",), + bases=("user.user",), + managers=[ + ( + "objects", + codeforlife.user.models.user.contactable.ContactableUserManager(), + ), + ], ), migrations.CreateModel( - name="Independent", + name="StudentUser", fields=[], options={ "proxy": True, "indexes": [], "constraints": [], }, - bases=("common.student",), + bases=("user.user",), + managers=[ + ("objects", codeforlife.user.models.user.student.StudentUserManager()), + ], ), migrations.CreateModel( - name="NonSchoolTeacher", - fields=[], + name="AuthFactor", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("type", models.TextField(choices=[("otp", "one-time password")])), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="auth_factors", + to=settings.AUTH_USER_MODEL, + ), + ), + ], options={ - "proxy": True, - "indexes": [], - "constraints": [], + "unique_together": {("user", "type")}, + }, + ), + migrations.CreateModel( + name="OtpBypassToken", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "token", + codeforlife.models.encrypted_char_field.EncryptedCharField( + help_text="The encrypted equivalent of the token.", + max_length=108, + verbose_name="token", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="otp_bypass_tokens", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "OTP bypass token", + "verbose_name_plural": "OTP bypass tokens", }, - bases=("common.teacher",), ), migrations.CreateModel( name="Session", @@ -83,7 +370,7 @@ class Migration(migrations.Migration): blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, - to="user.user", + to=settings.AUTH_USER_MODEL, ), ), ], @@ -94,7 +381,7 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name="OtpBypassToken", + name="Student", fields=[ ( "id", @@ -105,30 +392,79 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), + ("login_id", models.CharField(max_length=64, null=True)), + ("blocked_time", models.DateTimeField(blank=True, null=True)), ( - "token", - codeforlife.models.encrypted_char_field.EncryptedCharField( - help_text="The encrypted equivalent of the token.", - max_length=104, - verbose_name="token", + "class_field", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="students", + to="user.class", ), ), ( - "user", + "new_user", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="new_student", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "pending_class_request", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="class_request", + to="user.class", + ), + ), + ], + ), + migrations.CreateModel( + name="JoinReleaseStudent", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("action_type", models.CharField(max_length=64)), + ( + "action_time", + models.DateTimeField(default=django.utils.timezone.now), + ), + ( + "student", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - related_name="otp_bypass_tokens", - to="user.user", + related_name="student", + to="user.student", ), ), ], + ), + migrations.CreateModel( + name="Independent", + fields=[], options={ - "verbose_name": "OTP bypass token", - "verbose_name_plural": "OTP bypass tokens", + "proxy": True, + "indexes": [], + "constraints": [], }, + bases=("user.student",), ), migrations.CreateModel( - name="AuthFactor", + name="Teacher", fields=[ ( "id", @@ -139,65 +475,214 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("type", models.TextField(choices=[("otp", "one-time password")])), + ("is_admin", models.BooleanField(default=False)), + ("blocked_time", models.DateTimeField(blank=True, null=True)), ( - "user", + "invited_by", models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="invited_teachers", + to="user.teacher", + ), + ), + ( + "new_user", + models.OneToOneField( + blank=True, + null=True, on_delete=django.db.models.deletion.CASCADE, - related_name="auth_factors", - to="user.user", + related_name="new_teacher", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "school", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="teacher_school", + to="user.school", ), ), ], + ), + migrations.AddField( + model_name="class", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="created_classes", + to="user.teacher", + ), + ), + migrations.AddField( + model_name="class", + name="teacher", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="class_teacher", + to="user.teacher", + ), + ), + migrations.CreateModel( + name="NonSchoolTeacher", + fields=[], options={ - "unique_together": {("user", "type")}, + "proxy": True, + "indexes": [], + "constraints": [], }, + bases=("user.teacher",), ), migrations.CreateModel( - name="ContactableUser", + name="SchoolTeacher", fields=[], options={ "proxy": True, "indexes": [], "constraints": [], }, - bases=("user.user",), - managers=[ - ("objects", codeforlife.user.models.user.ContactableUserManager()), + bases=("user.teacher",), + ), + migrations.CreateModel( + name="UserProfile", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("otp_secret", models.CharField(blank=True, max_length=40, null=True)), + ("last_otp_for_time", models.DateTimeField(blank=True, null=True)), + ("developer", models.BooleanField(default=False)), + ("is_verified", models.BooleanField(default=False)), + ("first_name", models.CharField(blank=True, max_length=200, null=True)), + ("_first_name", models.BinaryField(blank=True, null=True)), + ("last_name", models.CharField(blank=True, max_length=200, null=True)), + ("_last_name", models.BinaryField(blank=True, null=True)), + ("email", models.CharField(blank=True, max_length=200, null=True)), + ("_email", models.BinaryField(blank=True, null=True)), + ("username", models.CharField(blank=True, max_length=200, null=True)), + ("_username", models.BinaryField(blank=True, null=True)), + ( + "google_refresh_token", + codeforlife.models.encrypted_char_field.EncryptedCharField( + blank=True, max_length=1012, null=True + ), + ), + ("google_sub", models.CharField(blank=True, max_length=255, null=True)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), + migrations.AddField( + model_name="teacher", + name="user", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="user.userprofile" + ), + ), + migrations.AddField( + model_name="student", + name="user", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="user.userprofile" + ), + ), migrations.CreateModel( - name="StudentUser", + name="UserSession", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("login_time", models.DateTimeField(default=django.utils.timezone.now)), + ("login_type", models.CharField(max_length=100, null=True)), + ( + "class_field", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="user.class", + ), + ), + ( + "school", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="user.school", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="GoogleUser", fields=[], options={ "proxy": True, "indexes": [], "constraints": [], }, - bases=("user.user",), + bases=("user.contactableuser",), managers=[ - ("objects", codeforlife.user.models.user.StudentUserManager()), + ("objects", codeforlife.user.models.user.google.GoogleUserManager()), ], ), migrations.CreateModel( - name="AdminSchoolTeacher", + name="IndependentUser", fields=[], options={ "proxy": True, "indexes": [], "constraints": [], }, - bases=("user.schoolteacher",), + bases=("user.contactableuser",), + managers=[ + ( + "objects", + codeforlife.user.models.user.independent.IndependentUserManager(), + ), + ], ), migrations.CreateModel( - name="NonAdminSchoolTeacher", + name="TeacherUser", fields=[], options={ "proxy": True, "indexes": [], "constraints": [], }, - bases=("user.schoolteacher",), + bases=("user.contactableuser",), + managers=[ + ("objects", codeforlife.user.models.user.teacher.TeacherUserManager()), + ], ), migrations.CreateModel( name="SessionAuthFactor", @@ -233,43 +718,33 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name="GoogleUser", + name="AdminSchoolTeacher", fields=[], options={ "proxy": True, "indexes": [], "constraints": [], }, - bases=("user.contactableuser",), - managers=[ - ("objects", codeforlife.user.models.user.GoogleUserManager()), - ], + bases=("user.schoolteacher",), ), migrations.CreateModel( - name="IndependentUser", + name="NonAdminSchoolTeacher", fields=[], options={ "proxy": True, "indexes": [], "constraints": [], }, - bases=("user.contactableuser",), - managers=[ - ("objects", codeforlife.user.models.user.IndependentUserManager()), - ], + bases=("user.schoolteacher",), ), - migrations.CreateModel( - name="TeacherUser", - fields=[], - options={ - "proxy": True, - "indexes": [], - "constraints": [], - }, - bases=("user.contactableuser",), - managers=[ - ("objects", codeforlife.user.models.user.TeacherUserManager()), - ], + migrations.AddConstraint( + model_name="teacher", + constraint=models.CheckConstraint( + condition=models.Q( + ("is_admin", True), ("school__isnull", True), _negated=True + ), + name="teacher__is_admin", + ), ), migrations.CreateModel( name="NonSchoolTeacherUser", @@ -281,7 +756,10 @@ class Migration(migrations.Migration): }, bases=("user.teacheruser",), managers=[ - ("objects", codeforlife.user.models.user.NonSchoolTeacherUserManager()), + ( + "objects", + codeforlife.user.models.user.non_school_teacher.NonSchoolTeacherUserManager(), + ), ], ), migrations.CreateModel( @@ -294,7 +772,10 @@ class Migration(migrations.Migration): }, bases=("user.teacheruser",), managers=[ - ("objects", codeforlife.user.models.user.SchoolTeacherUserManager()), + ( + "objects", + codeforlife.user.models.user.school_teacher.SchoolTeacherUserManager(), + ), ], ), migrations.CreateModel( @@ -309,7 +790,7 @@ class Migration(migrations.Migration): managers=[ ( "objects", - codeforlife.user.models.user.AdminSchoolTeacherUserManager(), + codeforlife.user.models.user.admin_school_teacher.AdminSchoolTeacherUserManager(), ), ], ), @@ -325,7 +806,7 @@ class Migration(migrations.Migration): managers=[ ( "objects", - codeforlife.user.models.user.NonAdminSchoolTeacherUserManager(), + codeforlife.user.models.user.non_admin_school_teacher.NonAdminSchoolTeacherUserManager(), ), ], ), diff --git a/codeforlife/user/models/__init__.py b/codeforlife/user/models/__init__.py index f68e4461..fe9765ab 100644 --- a/codeforlife/user/models/__init__.py +++ b/codeforlife/user/models/__init__.py @@ -5,6 +5,7 @@ from .auth_factor import AuthFactor from .klass import Class, class_name_validators +from .other import DailyActivity, JoinReleaseStudent, TotalActivity, UserSession from .otp_bypass_token import OtpBypassToken from .school import School, school_name_validators from .session import Session diff --git a/codeforlife/user/models/klass.py b/codeforlife/user/models/klass.py index 46f6dc21..e1f8ac61 100644 --- a/codeforlife/user/models/klass.py +++ b/codeforlife/user/models/klass.py @@ -3,15 +3,24 @@ Created on 19/02/2024 at 21:54:04(+00:00). """ -# pylint: disable-next=unused-import -from common.models import Class # type: ignore[import-untyped] +import typing as t +from datetime import timedelta +from uuid import uuid4 + from django.core.validators import MaxLengthValidator, MinLengthValidator +from django.db import models +from django.utils import timezone from ...types import Validators from ...validators import ( UnicodeAlphanumericCharSetValidator, UppercaseAsciiAlphanumericCharSetValidator, ) +from .teacher import Teacher + +if t.TYPE_CHECKING: + from django.db.models import ManyToManyField + from game.models import Worksheet class_access_code_validators: Validators = [ MinLengthValidator(5), @@ -25,3 +34,101 @@ special_chars="-_", ) ] + + +class ClassModelManager(models.Manager): + def all_members(self, user): + members = [] + if hasattr(user, "teacher"): + members.append(user.teacher) + if user.teacher.has_school(): + classes = user.teacher.class_teacher.all() + for c in classes: + members.extend(c.students.all()) + else: + c = user.student.class_field + members.append(c.teacher) + members.extend(c.students.all()) + return members + + def get_original_queryset(self): + return super().get_queryset() + + # Filter out non active classes by default + def get_queryset(self): + return super().get_queryset().filter(is_active=True) + + +class Class(models.Model): + locked_worksheets: "ManyToManyField[Worksheet]" + + name = models.CharField(max_length=200) + teacher = models.ForeignKey( + Teacher, related_name="class_teacher", on_delete=models.CASCADE + ) + access_code = models.CharField(max_length=5, null=True) + classmates_data_viewable = models.BooleanField(default=False) + always_accept_requests = models.BooleanField(default=False) + accept_requests_until = models.DateTimeField(null=True) + creation_time = models.DateTimeField(default=timezone.now, null=True) + is_active = models.BooleanField(default=True) + created_by = models.ForeignKey( + Teacher, + null=True, + blank=True, + related_name="created_classes", + on_delete=models.SET_NULL, + ) + + objects = ClassModelManager() + + def __str__(self): + return self.name + + @property + def active_game(self): + games = self.game_set.filter(game_class=self, is_archived=False) + if len(games) >= 1: + assert ( + len(games) == 1 + ) # there should NOT be more than one active game + return games[0] + return None + + def has_students(self): + students = self.students.all() + return students.count() != 0 + + def get_requests_message(self): + if self.always_accept_requests: + external_requests_message = ( + "This class is currently set to always accept requests." + ) + elif ( + self.accept_requests_until is not None + and (self.accept_requests_until - timezone.now()) >= timedelta() + ): + external_requests_message = ( + "This class is accepting external requests until " + + self.accept_requests_until.strftime("%d-%m-%Y %H:%M") + + " " + + timezone.get_current_timezone_name() + ) + else: + external_requests_message = ( + "This class is not currently accepting external requests." + ) + + return external_requests_message + + def anonymise(self): + self.name = uuid4().hex + self.access_code = "" + self.is_active = False + self.save() + + # Remove independent students' requests to join this class + self.class_request.clear() + + class Meta(object): + verbose_name_plural = "classes" diff --git a/codeforlife/user/models/other.py b/codeforlife/user/models/other.py new file mode 100644 index 00000000..58697a5f --- /dev/null +++ b/codeforlife/user/models/other.py @@ -0,0 +1,91 @@ +""" +© Ocado Group +Created on 10/02/2026 at 14:00:56(+00:00). + +Models that have been carried over from the old schema but are not yet fully +integrated into the new schema. These models are expected to be refactored and +integrated or removed in the new schema in the future. +""" + +from django.db import models +from django.utils import timezone + +from .klass import Class +from .school import School +from .student import Student +from .user import User + + +class UserSession(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + login_time = models.DateTimeField(default=timezone.now) + school = models.ForeignKey(School, null=True, on_delete=models.SET_NULL) + class_field = models.ForeignKey(Class, null=True, on_delete=models.SET_NULL) + login_type = models.CharField( + max_length=100, null=True + ) # for student login + + def __str__(self): + return f"{self.user} login: {self.login_time} type: {self.login_type}" + + +class JoinReleaseStudent(models.Model): + """ + To keep track when a student is released to be independent student or + joins a class to be a school student. + """ + + JOIN = "join" + RELEASE = "release" + + student = models.ForeignKey( + Student, related_name="student", on_delete=models.CASCADE + ) + # either "release" or "join" + action_type = models.CharField(max_length=64) + action_time = models.DateTimeField(default=timezone.now) + + +class TotalActivity(models.Model): + """ + A model to record total activity. Meant to only have one entry which + records all total activity. An example of this is total ever registrations. + """ + + teacher_registrations = models.PositiveIntegerField(default=0) + student_registrations = models.PositiveIntegerField(default=0) + independent_registrations = models.PositiveIntegerField(default=0) + anonymised_unverified_teachers = models.PositiveIntegerField(default=0) + anonymised_unverified_independents = models.PositiveIntegerField(default=0) + + class Meta: + verbose_name_plural = "Total activity" + + def __str__(self): + return "Total activity" + + +class DailyActivity(models.Model): + """ + A model to record sets of daily activity. Currently used to record the + amount of student details download clicks, through the CSV and login + cards methods, per day. + """ + + date = models.DateField(default=timezone.now) + csv_click_count = models.PositiveIntegerField(default=0) + login_cards_click_count = models.PositiveIntegerField(default=0) + primary_coding_club_downloads = models.PositiveIntegerField(default=0) + python_coding_club_downloads = models.PositiveIntegerField(default=0) + level_control_submits = models.PositiveBigIntegerField(default=0) + teacher_lockout_resets = models.PositiveIntegerField(default=0) + indy_lockout_resets = models.PositiveIntegerField(default=0) + school_student_lockout_resets = models.PositiveIntegerField(default=0) + anonymised_unverified_teachers = models.PositiveIntegerField(default=0) + anonymised_unverified_independents = models.PositiveIntegerField(default=0) + + class Meta: + verbose_name_plural = "Daily activities" + + def __str__(self): + return f"Activity on {self.date}: CSV clicks: {self.csv_click_count}, login cards clicks: {self.login_cards_click_count}, primary pack downloads: {self.primary_coding_club_downloads}, python pack downloads: {self.python_coding_club_downloads}, level control submits: {self.level_control_submits}, teacher lockout resets: {self.teacher_lockout_resets}, indy lockout resets: {self.indy_lockout_resets}, school student lockout resets: {self.school_student_lockout_resets}, unverified teachers anonymised: {self.anonymised_unverified_teachers}, unverified independents anonymised: {self.anonymised_unverified_independents}" diff --git a/codeforlife/user/models/school.py b/codeforlife/user/models/school.py index a934b0d4..283db471 100644 --- a/codeforlife/user/models/school.py +++ b/codeforlife/user/models/school.py @@ -3,8 +3,11 @@ Created on 20/02/2024 at 15:37:52(+00:00). """ -# pylint: disable-next=unused-import -from common.models import School # type: ignore[import-untyped] +from uuid import uuid4 + +from django.db import models +from django.utils import timezone +from django_countries.fields import CountryField from ...types import Validators from ...validators import UnicodeAlphanumericCharSetValidator @@ -16,3 +19,51 @@ special_chars="'.", ) ] + + +class SchoolModelManager(models.Manager): + def get_original_queryset(self): + return super().get_queryset() + + # Filter out inactive schools by default + def get_queryset(self): + return super().get_queryset().filter(is_active=True) + + +class School(models.Model): + name = models.CharField(max_length=200, unique=True) + country = CountryField( + blank_label="(select country)", null=True, blank=True + ) + # TODO: Create an Address model to house address details + county = models.CharField(max_length=50, blank=True, null=True) + creation_time = models.DateTimeField(default=timezone.now, null=True) + is_active = models.BooleanField(default=True) + + objects = SchoolModelManager() + + def __str__(self): + return self.name + + def classes(self): + teachers = self.teacher_school.all() + if teachers: + classes = [] + for teacher in teachers: + if teacher.class_teacher.all(): + classes.extend(list(teacher.class_teacher.all())) + return classes + return None + + def admins(self): + teachers = self.teacher_school.all() + return ( + [teacher for teacher in teachers if teacher.is_admin] + if teachers + else None + ) + + def anonymise(self): + self.name = uuid4().hex + self.is_active = False + self.save() diff --git a/codeforlife/user/models/student.py b/codeforlife/user/models/student.py index fd48bfea..fe6d7a90 100644 --- a/codeforlife/user/models/student.py +++ b/codeforlife/user/models/student.py @@ -6,16 +6,87 @@ """ import typing as t +from uuid import uuid4 -from common.models import Student, StudentModelManager from django.db import models +from .klass import Class +from .user import User, UserProfile + if t.TYPE_CHECKING: from django_stubs_ext.db.models import TypedModelMeta else: TypedModelMeta = object +class StudentModelManager(models.Manager): + def get_random_username(self): + while True: + random_username = uuid4().hex[:30] # generate a random username + if not User.objects.filter(username=random_username).exists(): + return random_username + + def schoolFactory(self, klass, name, password, login_id=None): + user = User.objects.create_user( + username=self.get_random_username(), + password=password, + first_name=name, + ) + user_profile = UserProfile.objects.create(user=user) + + return Student.objects.create( + class_field=klass, + user=user_profile, + new_user=user, + login_id=login_id, + ) + + def independentStudentFactory(self, name, email, password): + user = User.objects.create_user( + username=email, email=email, password=password, first_name=name + ) + + user_profile = UserProfile.objects.create(user=user) + + return Student.objects.create(user=user_profile, new_user=user) + + +class Student(models.Model): + class_field = models.ForeignKey( + Class, + related_name="students", + null=True, + blank=True, + on_delete=models.CASCADE, + ) + # hashed uuid used for the unique direct login url + login_id = models.CharField(max_length=64, null=True) + user = models.OneToOneField(UserProfile, on_delete=models.CASCADE) + new_user = models.OneToOneField( + User, + related_name="new_student", + null=True, + blank=True, + on_delete=models.CASCADE, + ) + pending_class_request = models.ForeignKey( + Class, + related_name="class_request", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + blocked_time = models.DateTimeField(null=True, blank=True) + + objects = StudentModelManager() + + def is_independent(self): + return not self.class_field + + def __str__(self): + return f"{self.new_user.first_name} {self.new_user.last_name}" + + # TODO: This model is legacy and should be removed in the new data schema. class Independent(Student): """An independent student.""" diff --git a/codeforlife/user/models/teacher.py b/codeforlife/user/models/teacher.py index 2bd39269..97c5b062 100644 --- a/codeforlife/user/models/teacher.py +++ b/codeforlife/user/models/teacher.py @@ -7,19 +7,97 @@ import typing as t -from common.models import Teacher, TeacherModelManager from django.db import models from django.db.models import Q -from .klass import Class from .school import School -from .student import Student +from .user import User, UserProfile if t.TYPE_CHECKING: from django_stubs_ext.db.models import TypedModelMeta else: TypedModelMeta = object + +class TeacherModelManager(models.Manager): + def factory(self, first_name, last_name, email, password): + user = User.objects.create_user( + username=email, + email=email, + password=password, + first_name=first_name, + last_name=last_name, + ) + + user_profile = UserProfile.objects.create(user=user) + + return Teacher.objects.create(user=user_profile, new_user=user) + + def get_original_queryset(self): + return super().get_queryset() + + # Filter out non active teachers by default + def get_queryset(self): + return super().get_queryset().filter(new_user__is_active=True) + + +class Teacher(models.Model): + user = models.OneToOneField(UserProfile, on_delete=models.CASCADE) + new_user = models.OneToOneField( + User, + related_name="new_teacher", + null=True, + blank=True, + on_delete=models.CASCADE, + ) + school = models.ForeignKey( + School, + related_name="teacher_school", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + is_admin = models.BooleanField(default=False) + blocked_time = models.DateTimeField(null=True, blank=True) + invited_by = models.ForeignKey( + "self", + related_name="invited_teachers", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + + objects = TeacherModelManager() + + class Meta: + constraints = [ + models.CheckConstraint( + check=~models.Q( + school__isnull=True, + is_admin=True, + ), + name="teacher__is_admin", + ) + ] + + def teaches(self, userprofile): + if hasattr(userprofile, "student"): + student = userprofile.student + return ( + not student.is_independent() + and student.class_field.teacher == self + ) + + def has_school(self): + return self.school is not (None or "") + + def has_class(self): + return self.class_teacher.exists() + + def __str__(self): + return f"{self.new_user.first_name} {self.new_user.last_name}" + + AnyTeacher = t.TypeVar("AnyTeacher", bound=Teacher) @@ -55,6 +133,9 @@ def student_users(self): @property def students(self): """All students the teacher can query.""" + # pylint: disable-next=import-outside-toplevel + from .student import Student + return Student.objects.filter( **( {"class_field__teacher__school": self.school} @@ -66,6 +147,9 @@ def students(self): @property def classes(self): """All classes the teacher can query.""" + # pylint: disable-next=import-outside-toplevel + from .klass import Class + return Class.objects.filter(teacher__school=self.school) @property @@ -104,7 +188,7 @@ def school_users(self): {"new_student__class_field__teacher__school": self.school} if self.is_admin else {"new_student__class_field__teacher": self} - ) + ), ) | Q( # school-teacher-users new_student__isnull=True, diff --git a/codeforlife/user/models/user/google.py b/codeforlife/user/models/user/google.py index 95f252c8..cee91e46 100644 --- a/codeforlife/user/models/user/google.py +++ b/codeforlife/user/models/user/google.py @@ -7,14 +7,13 @@ import typing as t -from common.models import UserProfile from django.db.models.query import QuerySet from requests import Session from requests.adapters import HTTPAdapter, Retry from ....types import JsonDict from .contactable import ContactableUser, ContactableUserManager -from .user import User +from .user import User, UserProfile if t.TYPE_CHECKING: # pragma: no cover from django_stubs_ext.db.models import TypedModelMeta diff --git a/codeforlife/user/models/user/independent.py b/codeforlife/user/models/user/independent.py index fc21e896..956aec79 100644 --- a/codeforlife/user/models/user/independent.py +++ b/codeforlife/user/models/user/independent.py @@ -7,12 +7,11 @@ import typing as t -from common.models import TotalActivity, UserProfile from django.db.models import F from django.db.models.query import QuerySet from .contactable import ContactableUser, ContactableUserManager -from .user import User +from .user import User, UserProfile if t.TYPE_CHECKING: # pragma: no cover from django_stubs_ext.db.models import TypedModelMeta @@ -50,9 +49,12 @@ def create_user( # type: ignore[override] **extra_fields, ): """Create an independent-user.""" - # pylint: disable-next=import-outside-toplevel + # pylint: disable=import-outside-toplevel + from ..other import TotalActivity from ..student import Student + # pylint: enable=import-outside-toplevel + assert "username" not in extra_fields user = super().create_user( diff --git a/codeforlife/user/models/user/student.py b/codeforlife/user/models/user/student.py index 2b2bd29b..ce716e22 100644 --- a/codeforlife/user/models/user/student.py +++ b/codeforlife/user/models/user/student.py @@ -8,17 +8,16 @@ import string import typing as t -from common.models import TotalActivity, UserProfile from django.db.models import F from django.db.models.query import QuerySet from django.utils.crypto import get_random_string -from ..klass import Class -from .user import User, UserManager +from .user import User, UserManager, UserProfile if t.TYPE_CHECKING: # pragma: no cover from django_stubs_ext.db.models import TypedModelMeta + from ..klass import Class from ..student import Student else: TypedModelMeta = object @@ -29,12 +28,14 @@ # pylint: disable-next=missing-class-docstring,too-few-public-methods class StudentUserManager(UserManager["StudentUser"]): def create_user( # type: ignore[override] - self, first_name: str, klass: Class, **extra_fields + self, first_name: str, klass: "Class", **extra_fields ): """Create a student-user.""" - # pylint: disable-next=import-outside-toplevel + # pylint: disable=import-outside-toplevel + from ..other import TotalActivity from ..student import Student + # pylint: enable=import-outside-toplevel # pylint: disable=protected-access password = StudentUser._get_random_password() login_id, hashed_login_id = StudentUser._get_random_login_id() diff --git a/codeforlife/user/models/user/teacher.py b/codeforlife/user/models/user/teacher.py index a2bb45d0..b0a71aac 100644 --- a/codeforlife/user/models/user/teacher.py +++ b/codeforlife/user/models/user/teacher.py @@ -7,13 +7,12 @@ import typing as t -from common.models import TotalActivity, UserProfile from django.db.models import F from django.db.models.query import QuerySet from ..school import School from .contactable import ContactableUser, ContactableUserManager -from .user import User +from .user import User, UserProfile if t.TYPE_CHECKING: # pragma: no cover from django_stubs_ext.db.models import TypedModelMeta @@ -40,9 +39,12 @@ def create_user( # type: ignore[override] **extra_fields, ): """Create a teacher-user.""" - # pylint: disable-next=import-outside-toplevel + # pylint: disable=import-outside-toplevel + from ..other import TotalActivity from ..teacher import Teacher + # pylint: enable=import-outside-toplevel + assert "username" not in extra_fields # pylint: disable=duplicate-code diff --git a/codeforlife/user/models/user/user.py b/codeforlife/user/models/user/user.py index b7d4c1f5..2d22a2f6 100644 --- a/codeforlife/user/models/user/user.py +++ b/codeforlife/user/models/user/user.py @@ -6,17 +6,17 @@ """ import typing as t -from datetime import datetime - -from common.models import UserProfile +from datetime import datetime, timedelta # pylint: disable-next=imported-auth-user -from django.contrib.auth.models import User as _User +from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import UserManager as _UserManager +from django.db import models from django.db.models.query import QuerySet +from django.utils import timezone from pyotp import TOTP -from ....models import AbstractBaseUser +from ....models import AbstractBaseUser, EncryptedCharField from ....types import Validators from ....validators import UnicodeAlphanumericCharSetValidator @@ -57,7 +57,10 @@ class Meta(TypedModelMeta): # pylint: disable-next=too-many-ancestors -class User(_AbstractBaseUser, _User): +class User( + _AbstractBaseUser, + AbstractUser, # TODO: remove this inheritance in new schema +): """A proxy to Django's user class.""" _password: t.Optional[str] @@ -67,13 +70,10 @@ class User(_AbstractBaseUser, _User): # pylint: disable-next=line-too-long otp_bypass_tokens: QuerySet["OtpBypassToken"] # type: ignore[assignment,misc] session: "Session" # type: ignore[assignment] - userprofile: UserProfile + userprofile: "UserProfile" credential_fields = frozenset(["email", "password"]) - class Meta(TypedModelMeta): - proxy = True - @property def is_authenticated(self): return ( @@ -203,3 +203,39 @@ def filter_users(self, queryset: QuerySet[User]): # pylint: disable-next=missing-function-docstring def get_queryset(self): return self.filter_users(super().get_queryset().filter(is_active=True)) + + +class UserProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + + otp_secret = models.CharField(max_length=40, null=True, blank=True) + last_otp_for_time = models.DateTimeField(null=True, blank=True) + developer = models.BooleanField(default=False) + is_verified = models.BooleanField(default=False) + + # TODO: Make not nullable once data has been transferred + first_name = models.CharField(max_length=200, null=True, blank=True) + _first_name = models.BinaryField(null=True, blank=True) + last_name = models.CharField(max_length=200, null=True, blank=True) + _last_name = models.BinaryField(null=True, blank=True) + email = models.CharField(max_length=200, null=True, blank=True) + _email = models.BinaryField(null=True, blank=True) + # TODO: Make not nullable once data has been transferred + username = models.CharField(max_length=200, null=True, blank=True) + _username = models.BinaryField(null=True, blank=True) + + # Google. + google_refresh_token = EncryptedCharField( + # pylint: disable-next=protected-access + max_length=1000 + len(EncryptedCharField._prefix), + null=True, + blank=True, + ) + google_sub = models.CharField(max_length=255, null=True, blank=True) + + def __str__(self): + return f"{self.user.first_name} {self.user.last_name}" + + def joined_recently(self): + now = timezone.now() + return now - timedelta(days=7) <= self.user.date_joined diff --git a/settings.py b/settings.py index fe271014..18cff3eb 100644 --- a/settings.py +++ b/settings.py @@ -26,9 +26,6 @@ "django.contrib.staticfiles", "django.contrib.sites", "codeforlife.user", - "game", # TODO: remove this. - "common", # TODO: remove this. - "portal", # TODO: remove this. ] MIDDLEWARE = [ From 43718f0d06819fb9ac4d8ed237419495c7ab81d3 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 10 Feb 2026 15:54:01 +0000 Subject: [PATCH 02/25] fix type errors --- codeforlife/forms.py | 2 +- codeforlife/tests/api_client.py | 3 ++- codeforlife/user/models/klass.py | 6 ++---- codeforlife/user/models/student.py | 8 ++++++-- codeforlife/user/models/teacher.py | 25 +++++++++++++++-------- codeforlife/user/serializers/user_test.py | 9 +++++--- codeforlife/user/views/klass_test.py | 7 ++++--- codeforlife/user/views/school_test.py | 6 ++++-- codeforlife/user/views/user_test.py | 10 +++++---- 9 files changed, 47 insertions(+), 29 deletions(-) diff --git a/codeforlife/forms.py b/codeforlife/forms.py index 674fecb9..5241f5a7 100644 --- a/codeforlife/forms.py +++ b/codeforlife/forms.py @@ -62,7 +62,7 @@ def clean(self): "Incorrect user class.", code="incorrect_user_class", ) - if not user.is_active: + if not user.is_active: # type: ignore[attr-defined] raise ValidationError( "User is not active", code="user_not_active", diff --git a/codeforlife/tests/api_client.py b/codeforlife/tests/api_client.py index 7c6a865c..78d183ea 100644 --- a/codeforlife/tests/api_client.py +++ b/codeforlife/tests/api_client.py @@ -558,7 +558,8 @@ def login_as(self, user: "TypedUser", password: str = "password"): auth_user = self.login_teacher(user.email, password) elif isinstance(user, StudentUser): auth_user = self.login_student( - user.student.class_field.access_code, + # pylint: disable-next=line-too-long + user.student.class_field.access_code, # type: ignore[union-attr,arg-type] user.first_name, password, ) diff --git a/codeforlife/user/models/klass.py b/codeforlife/user/models/klass.py index e1f8ac61..d84875c0 100644 --- a/codeforlife/user/models/klass.py +++ b/codeforlife/user/models/klass.py @@ -20,7 +20,6 @@ if t.TYPE_CHECKING: from django.db.models import ManyToManyField - from game.models import Worksheet class_access_code_validators: Validators = [ MinLengthValidator(5), @@ -60,8 +59,6 @@ def get_queryset(self): class Class(models.Model): - locked_worksheets: "ManyToManyField[Worksheet]" - name = models.CharField(max_length=200) teacher = models.ForeignKey( Teacher, related_name="class_teacher", on_delete=models.CASCADE @@ -87,7 +84,8 @@ def __str__(self): @property def active_game(self): - games = self.game_set.filter(game_class=self, is_archived=False) + # pylint: disable-next=line-too-long + games = self.game_set.filter(game_class=self, is_archived=False) # type: ignore[attr-defined] if len(games) >= 1: assert ( len(games) == 1 diff --git a/codeforlife/user/models/student.py b/codeforlife/user/models/student.py index fe6d7a90..989abb72 100644 --- a/codeforlife/user/models/student.py +++ b/codeforlife/user/models/student.py @@ -84,6 +84,9 @@ def is_independent(self): return not self.class_field def __str__(self): + if self.new_user is None: + return super().__str__() + return f"{self.new_user.first_name} {self.new_user.last_name}" @@ -91,7 +94,7 @@ def __str__(self): class Independent(Student): """An independent student.""" - class_field: None + class_field: None # type: ignore[assignment] class Meta(TypedModelMeta): proxy = True @@ -102,4 +105,5 @@ class Manager(StudentModelManager): def get_queryset(self): return super().get_queryset().filter(class_field__isnull=True) - objects: models.Manager["Independent"] = Manager() + # pylint: disable-next=line-too-long + objects: models.Manager["Independent"] = Manager() # type: ignore[assignment,misc] diff --git a/codeforlife/user/models/teacher.py b/codeforlife/user/models/teacher.py index 97c5b062..f50b80b0 100644 --- a/codeforlife/user/models/teacher.py +++ b/codeforlife/user/models/teacher.py @@ -95,6 +95,9 @@ def has_class(self): return self.class_teacher.exists() def __str__(self): + if self.new_user is None: + return super().__str__() + return f"{self.new_user.first_name} {self.new_user.last_name}" @@ -104,7 +107,7 @@ def __str__(self): class SchoolTeacher(Teacher): """A teacher that is in a school.""" - school: School + school: School # type: ignore[assignment] class Meta(TypedModelMeta): proxy = True @@ -114,7 +117,8 @@ class Manager(TeacherModelManager): def get_queryset(self): return super().get_queryset().filter(school__isnull=False) - objects: models.Manager["SchoolTeacher"] = Manager() + # pylint: disable-next=line-too-long + objects: models.Manager["SchoolTeacher"] = Manager() # type: ignore[assignment,misc] @property def student_users(self): @@ -200,7 +204,7 @@ def school_users(self): class AdminSchoolTeacher(SchoolTeacher): """An admin-teacher that is in a school.""" - is_admin: t.Literal[True] + is_admin: t.Literal[True] # type: ignore[assignment] class Meta(TypedModelMeta): proxy = True @@ -210,7 +214,8 @@ class Manager(SchoolTeacher.Manager): def get_queryset(self): return super().get_queryset().filter(is_admin=True) - objects: models.Manager["AdminSchoolTeacher"] = Manager() + # pylint: disable-next=line-too-long + objects: models.Manager["AdminSchoolTeacher"] = Manager() # type: ignore[misc] @property def is_last_admin(self): @@ -225,7 +230,7 @@ def is_last_admin(self): class NonAdminSchoolTeacher(SchoolTeacher): """A non-admin-teacher that is in a school.""" - is_admin: t.Literal[False] + is_admin: t.Literal[False] # type: ignore[assignment] class Meta(TypedModelMeta): proxy = True @@ -235,14 +240,15 @@ class Manager(SchoolTeacher.Manager): def get_queryset(self): return super().get_queryset().filter(is_admin=False) - objects: models.Manager["NonAdminSchoolTeacher"] = Manager() + # pylint: disable-next=line-too-long + objects: models.Manager["NonAdminSchoolTeacher"] = Manager() # type: ignore[misc] class NonSchoolTeacher(Teacher): """A teacher that is not in a school.""" - school: None - is_admin: t.Literal[False] + school: None # type: ignore[assignment] + is_admin: t.Literal[False] # type: ignore[assignment] class Meta(TypedModelMeta): proxy = True @@ -252,7 +258,8 @@ class Manager(TeacherModelManager): def get_queryset(self): return super().get_queryset().filter(school__isnull=True) - objects: models.Manager["NonSchoolTeacher"] = Manager() + # pylint: disable-next=line-too-long + objects: models.Manager["NonSchoolTeacher"] = Manager() # type: ignore[assignment,misc] # pylint: disable-next=invalid-name diff --git a/codeforlife/user/serializers/user_test.py b/codeforlife/user/serializers/user_test.py index 72b94e86..63bdd917 100644 --- a/codeforlife/user/serializers/user_test.py +++ b/codeforlife/user/serializers/user_test.py @@ -25,7 +25,8 @@ def test_to_representation__teacher(self): "requesting_to_join_class": None, "teacher": { "id": user.teacher.id, - "school": user.teacher.school.id, + # pylint: disable-next=line-too-long + "school": user.teacher.school.id, # type: ignore[union-attr] "is_admin": user.teacher.is_admin, }, "student": None, @@ -46,8 +47,10 @@ def test_to_representation__student(self): "teacher": None, "student": { "id": user.student.id, - "klass": user.student.class_field.access_code, - "school": user.student.class_field.teacher.school.id, + # pylint: disable-next=line-too-long + "klass": user.student.class_field.access_code, # type: ignore[union-attr] + # pylint: disable-next=line-too-long + "school": user.student.class_field.teacher.school.id, # type: ignore[union-attr] }, }, # TODO: remove in new schema. diff --git a/codeforlife/user/views/klass_test.py b/codeforlife/user/views/klass_test.py index f9275a70..071931ef 100644 --- a/codeforlife/user/views/klass_test.py +++ b/codeforlife/user/views/klass_test.py @@ -47,7 +47,7 @@ def test_get_queryset__student(self): assert user self.assert_get_queryset( - values=[user.student.class_field], + values=[user.student.class_field], # type: ignore[list-item] request=self.client.request_factory.get(user=user), ) @@ -69,7 +69,8 @@ def test_retrieve(self): assert user self.client.login_as(user, password="Password1") - self.client.retrieve(model=user.student.class_field) + # pylint: disable-next=line-too-long + self.client.retrieve(model=user.student.class_field) # type: ignore[type-var] def test_list(self): """Can successfully list classes.""" @@ -130,6 +131,6 @@ def test_list__teacher(self): self.client.login_as(user) self.client.list( - models=classes, + models=classes, # type: ignore[arg-type] filters={"teacher": str(user.teacher.id)}, ) diff --git a/codeforlife/user/views/school_test.py b/codeforlife/user/views/school_test.py index e569f51e..cf09bc77 100644 --- a/codeforlife/user/views/school_test.py +++ b/codeforlife/user/views/school_test.py @@ -63,7 +63,8 @@ def test_get_queryset__student(self): assert user self.assert_get_queryset( - values=[user.student.class_field.teacher.school], + # pylint: disable-next=line-too-long + values=[user.student.class_field.teacher.school], # type: ignore[union-attr,list-item] request=self.client.request_factory.get(user=user), ) @@ -78,7 +79,8 @@ def test_get_queryset__independent(self): assert user self.assert_get_queryset( - values=[user.student.pending_class_request.teacher.school], + # pylint: disable-next=line-too-long + values=[user.student.pending_class_request.teacher.school], # type: ignore[union-attr,list-item] request=self.client.request_factory.get(user=user), ) diff --git a/codeforlife/user/views/user_test.py b/codeforlife/user/views/user_test.py index 4643579e..97717bf9 100644 --- a/codeforlife/user/views/user_test.py +++ b/codeforlife/user/views/user_test.py @@ -56,16 +56,18 @@ def test_get_queryset__student(self): user = StudentUser.objects.first() assert user - users = [ + users: t.List[User] = [ user, - user.student.class_field.teacher.new_user, + # pylint: disable-next=line-too-long + user.student.class_field.teacher.new_user, # type: ignore[union-attr,list-item] *list( User.objects.exclude(pk=user.pk).filter( - new_student__in=user.student.class_field.students.all() + # pylint: disable-next=line-too-long + new_student__in=user.student.class_field.students.all() # type: ignore[union-attr] ) ), ] - users.sort(key=lambda user: user.pk) + users.sort(key=lambda user: user.pk) # type: ignore[union-attr] self.assert_get_queryset( values=users, From a5cbd1fb1f956fe0eb16a7cf2aa85e8b2fa28262 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 11 Feb 2026 14:03:06 +0000 Subject: [PATCH 03/25] fix linting errors --- Pipfile | 4 +- Pipfile.lock | 25 +- codeforlife/mail.py | 4 +- codeforlife/server.py | 2 +- codeforlife/tasks/bigquery_test.py | 2 +- codeforlife/tests/api_client.py | 4 +- codeforlife/tests/api_request_factory.py | 10 +- codeforlife/tests/model_view_set.py | 2 +- codeforlife/tests/model_view_set_client.py | 12 +- codeforlife/user/migrations/0001_initial.py | 2 +- codeforlife/user/models/auth_factor.py | 16 +- codeforlife/user/models/klass.py | 80 ++++- codeforlife/user/models/other.py | 173 ++++++++-- codeforlife/user/models/otp_bypass_token.py | 15 +- codeforlife/user/models/school.py | 48 ++- .../user/models/session_auth_factor.py | 23 +- codeforlife/user/models/student.py | 70 +++- codeforlife/user/models/teacher.py | 298 ------------------ codeforlife/user/models/teacher/__init__.py | 47 +++ .../user/models/teacher/admin_school.py | 41 +++ .../user/models/teacher/non_admin_school.py | 32 ++ codeforlife/user/models/teacher/non_school.py | 33 ++ codeforlife/user/models/teacher/school.py | 115 +++++++ codeforlife/user/models/teacher/teacher.py | 139 ++++++++ .../user/models/user/admin_school_teacher.py | 7 +- codeforlife/user/models/user/google.py | 10 +- codeforlife/user/models/user/independent.py | 9 +- .../models/user/non_admin_school_teacher.py | 7 +- .../user/models/user/non_school_teacher.py | 7 +- .../user/models/user/school_teacher.py | 13 +- codeforlife/user/models/user/teacher.py | 13 +- codeforlife/user/models/user/user.py | 3 + codeforlife/validators/char_set/ascii.py | 1 + codeforlife/validators/char_set/base.py | 1 + codeforlife/validators/char_set/unicode.py | 1 + codeforlife/validators/enhanced_regex.py | 2 +- 36 files changed, 824 insertions(+), 447 deletions(-) delete mode 100644 codeforlife/user/models/teacher.py create mode 100644 codeforlife/user/models/teacher/__init__.py create mode 100644 codeforlife/user/models/teacher/admin_school.py create mode 100644 codeforlife/user/models/teacher/non_admin_school.py create mode 100644 codeforlife/user/models/teacher/non_school.py create mode 100644 codeforlife/user/models/teacher/school.py create mode 100644 codeforlife/user/models/teacher/teacher.py diff --git a/Pipfile b/Pipfile index e43b8293..02a90ead 100644 --- a/Pipfile +++ b/Pipfile @@ -42,8 +42,8 @@ django-extensions = "==3.2.1" django-test-migrations = "==1.2.0" pyparsing = "==3.0.9" pydot = "==1.4.2" -pylint = "==3.2.7" -pylint-django = "==2.5.5" +pylint = "==4.0.4" +pylint-django = "==2.7.0" isort = "==5.13.2" mypy = "==1.15.0" django-stubs = {version = "==5.1.3", extras = ["compatible-mypy"]} diff --git a/Pipfile.lock b/Pipfile.lock index 0c7242d3..742fd686 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "89ff1d169339b93c750837aed9cf2af6808f5fd41db2092b6c926b57228205e4" + "sha256": "b1b213110b3c6f5a403ba8b3442fb4aa9679d65403d6189c9cf382ea4f742cc5" }, "pipfile-spec": 6, "requires": { @@ -1286,11 +1286,11 @@ }, "astroid": { "hashes": [ - "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a", - "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25" + "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", + "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0" ], - "markers": "python_full_version >= '3.8.0'", - "version": "==3.2.4" + "markers": "python_full_version >= '3.10.0'", + "version": "==4.0.4" }, "black": { "hashes": [ @@ -1868,21 +1868,20 @@ }, "pylint": { "hashes": [ - "sha256:02f4aedeac91be69fb3b4bea997ce580a4ac68ce58b89eaefeaf06749df73f4b", - "sha256:1b7a721b575eaeaa7d39db076b6e7743c993ea44f57979127c517c6c572c803e" + "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", + "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2" ], "index": "pypi", - "markers": "python_full_version >= '3.8.0'", - "version": "==3.2.7" + "markers": "python_full_version >= '3.10.0'", + "version": "==4.0.4" }, "pylint-django": { "hashes": [ - "sha256:2f339e4bf55776958283395c5139c37700c91bd5ef1d8251ef6ac88b5abbba9b", - "sha256:5abd5c2228e0e5e2a4cb6d0b4fc1d1cef1e773d0be911412f4dd4fc1a1a440b7" + "sha256:76ef7e7bbbcf7ee86adbb2beac0ffaa7232509a17bf4a488d81467a1bbaa215b" ], "index": "pypi", - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==2.5.5" + "markers": "python_version >= '3.9' and python_version < '4.0'", + "version": "==2.7.0" }, "pylint-plugin-utils": { "hashes": [ diff --git a/codeforlife/mail.py b/codeforlife/mail.py index 89fbd6a0..306f54f3 100644 --- a/codeforlife/mail.py +++ b/codeforlife/mail.py @@ -38,7 +38,7 @@ class Preference: is_opted_in: t.Optional[bool] = None -# pylint: disable-next=too-many-arguments +# pylint: disable-next=too-many-arguments,too-many-positional-arguments def add_contact( email: str, opt_in_type: t.Optional[ @@ -218,7 +218,7 @@ class EmailAttachment: content: str -# pylint: disable-next=too-many-arguments +# pylint: disable-next=too-many-arguments,too-many-positional-arguments def send_mail( campaign_id: int, to_addresses: t.List[str], diff --git a/codeforlife/server.py b/codeforlife/server.py index 9c8d8aeb..9a669175 100644 --- a/codeforlife/server.py +++ b/codeforlife/server.py @@ -56,7 +56,7 @@ def django_dev_server_is_running(self): and sys.argv[1] == "runserver" ) - # pylint: disable-next=too-many-arguments + # pylint: disable-next=too-many-arguments,too-many-positional-arguments def __init__( self, mode: Mode = t.cast(Mode, os.getenv("SERVER_MODE", "django")), diff --git a/codeforlife/tasks/bigquery_test.py b/codeforlife/tasks/bigquery_test.py index 8b0e8a0f..51374627 100644 --- a/codeforlife/tasks/bigquery_test.py +++ b/codeforlife/tasks/bigquery_test.py @@ -180,7 +180,7 @@ def _assert_csv_file_loaded_into_bigquery( # settings - # pylint: disable-next=too-many-arguments + # pylint: disable-next=too-many-arguments,too-many-positional-arguments def _test_settings( self, code: str, diff --git a/codeforlife/tests/api_client.py b/codeforlife/tests/api_client.py index 78d183ea..e6cbbf93 100644 --- a/codeforlife/tests/api_client.py +++ b/codeforlife/tests/api_client.py @@ -119,7 +119,7 @@ def _make_assertions(): StatusCodeAssertion = t.Optional[t.Union[int, t.Callable[[int], bool]]] - # pylint: disable=too-many-arguments,redefined-builtin + # pylint: disable=too-many-arguments,redefined-builtin,too-many-positional-arguments def generic( self, @@ -304,7 +304,7 @@ def options( # type: ignore[override] **extra, ) - # pylint: enable=too-many-arguments,redefined-builtin + # pylint: enable=too-many-arguments,redefined-builtin,too-many-positional-arguments class APIClient( diff --git a/codeforlife/tests/api_request_factory.py b/codeforlife/tests/api_request_factory.py index 98720b99..4c7e1a6b 100644 --- a/codeforlife/tests/api_request_factory.py +++ b/codeforlife/tests/api_request_factory.py @@ -59,7 +59,8 @@ def request(self, user: t.Optional[AnyAbstractBaseUser] = None, **kwargs): return request - # pylint: disable-next=too-many-arguments + # pylint: disable=too-many-arguments,too-many-positional-arguments + def generic( # type: ignore[override] self, method: str, @@ -100,7 +101,6 @@ def get( # type: ignore[override] ), ) - # pylint: disable-next=too-many-arguments def post( # type: ignore[override] self, path: t.Optional[str] = None, @@ -126,7 +126,6 @@ def post( # type: ignore[override] ), ) - # pylint: disable-next=too-many-arguments def put( # type: ignore[override] self, path: t.Optional[str] = None, @@ -152,7 +151,6 @@ def put( # type: ignore[override] ), ) - # pylint: disable-next=too-many-arguments def patch( # type: ignore[override] self, path: t.Optional[str] = None, @@ -178,7 +176,6 @@ def patch( # type: ignore[override] ), ) - # pylint: disable-next=too-many-arguments def delete( # type: ignore[override] self, path: t.Optional[str] = None, @@ -204,7 +201,6 @@ def delete( # type: ignore[override] ), ) - # pylint: disable-next=too-many-arguments def options( # type: ignore[override] self, path: t.Optional[str] = None, @@ -230,6 +226,8 @@ def options( # type: ignore[override] ), ) + # pylint: enable=too-many-arguments,too-many-positional-arguments + class APIRequestFactory( BaseAPIRequestFactory[Request[AnyUser], AnyUser], diff --git a/codeforlife/tests/model_view_set.py b/codeforlife/tests/model_view_set.py index dc54b0b7..9b2fb342 100644 --- a/codeforlife/tests/model_view_set.py +++ b/codeforlife/tests/model_view_set.py @@ -98,7 +98,7 @@ def reverse_action( # Assertion Helpers # -------------------------------------------------------------------------- - # pylint: disable-next=too-many-arguments + # pylint: disable-next=too-many-arguments,too-many-positional-arguments def assert_serialized_model_equals_json_model( self, model: AnyModel, diff --git a/codeforlife/tests/model_view_set_client.py b/codeforlife/tests/model_view_set_client.py index d1551eff..fd68ebf4 100644 --- a/codeforlife/tests/model_view_set_client.py +++ b/codeforlife/tests/model_view_set_client.py @@ -206,7 +206,7 @@ def retrieve( return response - # pylint: disable-next=too-many-arguments + # pylint: disable-next=too-many-arguments,too-many-positional-arguments def list( self, models: t.Collection[AnyModel], @@ -268,7 +268,7 @@ def _make_assertions(response_json: JsonDict): # Partial Update (HTTP PATCH) # -------------------------------------------------------------------------- - # pylint: disable-next=too-many-arguments + # pylint: disable-next=too-many-arguments,too-many-positional-arguments def _assert_update( self, model: AnyModel, @@ -282,7 +282,7 @@ def _assert_update( model, json_model, action, request_method, contains_subset=partial ) - # pylint: disable-next=too-many-arguments + # pylint: disable-next=too-many-arguments,too-many-positional-arguments def partial_update( self, model: AnyModel, @@ -333,7 +333,7 @@ def partial_update( return response - # pylint: disable-next=too-many-arguments + # pylint: disable-next=too-many-arguments,too-many-positional-arguments def bulk_partial_update( self, models: t.Union[t.List[AnyModel], QuerySet[AnyModel]], @@ -394,7 +394,7 @@ def _make_assertions(json_models: t.List[JsonDict]): # Update (HTTP PUT) # -------------------------------------------------------------------------- - # pylint: disable-next=too-many-arguments + # pylint: disable-next=too-many-arguments,too-many-positional-arguments def update( self, model: AnyModel, @@ -445,7 +445,7 @@ def update( return response - # pylint: disable-next=too-many-arguments + # pylint: disable-next=too-many-arguments,too-many-positional-arguments def bulk_update( self, models: t.Union[t.List[AnyModel], QuerySet[AnyModel]], diff --git a/codeforlife/user/migrations/0001_initial.py b/codeforlife/user/migrations/0001_initial.py index ca28de0e..29705ce5 100644 --- a/codeforlife/user/migrations/0001_initial.py +++ b/codeforlife/user/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.15 on 2026-02-10 14:27 +# Generated by Django 5.1.15 on 2026-02-11 10:37 import codeforlife.models.encrypted_char_field import codeforlife.user.models.user.admin_school_teacher diff --git a/codeforlife/user/models/auth_factor.py b/codeforlife/user/models/auth_factor.py index f7ae2e33..67c12217 100644 --- a/codeforlife/user/models/auth_factor.py +++ b/codeforlife/user/models/auth_factor.py @@ -12,10 +12,14 @@ from ...types import Validators from ...validators import AsciiNumericCharSetValidator -from .user import User if t.TYPE_CHECKING: # pragma: no cover + from django_stubs_ext.db.models import TypedModelMeta + from .session_auth_factor import SessionAuthFactor + from .user import User +else: + TypedModelMeta = object class AuthFactor(models.Model): @@ -35,15 +39,17 @@ class Type(models.TextChoices): OTP = "otp", _("one-time password") - user = models.ForeignKey( - User, + user: "User" + user = models.ForeignKey( # type: ignore[assignment] + "user.User", related_name="auth_factors", on_delete=models.CASCADE, ) - type = models.TextField(choices=Type.choices) + type: str + type = models.TextField(choices=Type.choices) # type: ignore[assignment] - class Meta: + class Meta(TypedModelMeta): unique_together = ["user", "type"] def __str__(self): diff --git a/codeforlife/user/models/klass.py b/codeforlife/user/models/klass.py index d84875c0..e525e3d3 100644 --- a/codeforlife/user/models/klass.py +++ b/codeforlife/user/models/klass.py @@ -16,10 +16,16 @@ UnicodeAlphanumericCharSetValidator, UppercaseAsciiAlphanumericCharSetValidator, ) -from .teacher import Teacher -if t.TYPE_CHECKING: - from django.db.models import ManyToManyField +if t.TYPE_CHECKING: # pragma: no cover + from datetime import datetime + + from django_stubs_ext.db.models import TypedModelMeta + + from .teacher import Teacher +else: + TypedModelMeta = object + class_access_code_validators: Validators = [ MinLengthValidator(5), @@ -36,7 +42,10 @@ class ClassModelManager(models.Manager): + """Manager for Class model.""" + def all_members(self, user): + """Get all members of the class associated with the user.""" members = [] if hasattr(user, "teacher"): members.append(user.teacher) @@ -51,26 +60,58 @@ def all_members(self, user): return members def get_original_queryset(self): + """Get the original queryset without filtering.""" return super().get_queryset() - # Filter out non active classes by default def get_queryset(self): + """Filter out non active classes by default.""" return super().get_queryset().filter(is_active=True) class Class(models.Model): + """A class.""" + name = models.CharField(max_length=200) - teacher = models.ForeignKey( - Teacher, related_name="class_teacher", on_delete=models.CASCADE + + teacher: "Teacher" + teacher = models.ForeignKey( # type: ignore[assignment] + "user.Teacher", + related_name="class_teacher", + on_delete=models.CASCADE, ) - access_code = models.CharField(max_length=5, null=True) - classmates_data_viewable = models.BooleanField(default=False) - always_accept_requests = models.BooleanField(default=False) - accept_requests_until = models.DateTimeField(null=True) - creation_time = models.DateTimeField(default=timezone.now, null=True) - is_active = models.BooleanField(default=True) - created_by = models.ForeignKey( - Teacher, + + access_code: t.Optional[str] + access_code = models.CharField( # type: ignore[assignment] + max_length=5, + null=True, + ) + + classmates_data_viewable: bool + classmates_data_viewable = models.BooleanField( # type: ignore[assignment] + default=False + ) + + always_accept_requests: bool + always_accept_requests = models.BooleanField( # type: ignore[assignment] + default=False + ) + + accept_requests_until: t.Optional["datetime"] + accept_requests_until = models.DateTimeField( # type: ignore[assignment] + null=True + ) + + creation_time: t.Optional["datetime"] + creation_time = models.DateTimeField( # type: ignore[assignment] + default=timezone.now, null=True + ) + + is_active: bool + is_active = models.BooleanField(default=True) # type: ignore[assignment] + + created_by: t.Optional["Teacher"] + created_by = models.ForeignKey( # type: ignore[assignment] + "user.Teacher", null=True, blank=True, related_name="created_classes", @@ -84,6 +125,10 @@ def __str__(self): @property def active_game(self): + """ + Get the active game for the class, if it exists. There should only be + one active game per class. + """ # pylint: disable-next=line-too-long games = self.game_set.filter(game_class=self, is_archived=False) # type: ignore[attr-defined] if len(games) >= 1: @@ -94,10 +139,12 @@ def active_game(self): return None def has_students(self): + """Check if the class has any students.""" students = self.students.all() return students.count() != 0 def get_requests_message(self): + """Get the message regarding the class's request acceptance status.""" if self.always_accept_requests: external_requests_message = ( "This class is currently set to always accept requests." @@ -108,6 +155,7 @@ def get_requests_message(self): ): external_requests_message = ( "This class is accepting external requests until " + # pylint: disable-next=no-member + self.accept_requests_until.strftime("%d-%m-%Y %H:%M") + " " + timezone.get_current_timezone_name() @@ -120,13 +168,15 @@ def get_requests_message(self): return external_requests_message def anonymise(self): + """Anonymise the class.""" self.name = uuid4().hex self.access_code = "" self.is_active = False self.save() # Remove independent students' requests to join this class + # pylint: disable-next=no-member self.class_request.clear() - class Meta(object): + class Meta(TypedModelMeta): verbose_name_plural = "classes" diff --git a/codeforlife/user/models/other.py b/codeforlife/user/models/other.py index 58697a5f..9b8437b7 100644 --- a/codeforlife/user/models/other.py +++ b/codeforlife/user/models/other.py @@ -7,23 +7,58 @@ integrated or removed in the new schema in the future. """ +import typing as t + from django.db import models from django.utils import timezone -from .klass import Class -from .school import School -from .student import Student -from .user import User +if t.TYPE_CHECKING: # pragma: no cover + from datetime import datetime + + from django_stubs_ext.db.models import TypedModelMeta + + from .klass import Class + from .school import School + from .student import Student + from .user import User +else: + TypedModelMeta = object class UserSession(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE) - login_time = models.DateTimeField(default=timezone.now) - school = models.ForeignKey(School, null=True, on_delete=models.SET_NULL) - class_field = models.ForeignKey(Class, null=True, on_delete=models.SET_NULL) - login_type = models.CharField( - max_length=100, null=True - ) # for student login + """A model to track user sessions.""" + + user: "User" + user = models.ForeignKey( # type: ignore[assignment] + "user.User", + on_delete=models.CASCADE, + ) + + login_time: "datetime" + login_time = models.DateTimeField( # type: ignore[assignment] + default=timezone.now + ) + + school: t.Optional["School"] + school = models.ForeignKey( # type: ignore[assignment] + "user.School", + null=True, + on_delete=models.SET_NULL, + ) + + class_field: t.Optional["Class"] + class_field = models.ForeignKey( # type: ignore[assignment] + "user.Class", + null=True, + on_delete=models.SET_NULL, + ) + + # for student login + login_type: t.Optional[str] + login_type = models.CharField( # type: ignore[assignment] + max_length=100, + null=True, + ) def __str__(self): return f"{self.user} login: {self.login_time} type: {self.login_type}" @@ -38,12 +73,21 @@ class JoinReleaseStudent(models.Model): JOIN = "join" RELEASE = "release" - student = models.ForeignKey( - Student, related_name="student", on_delete=models.CASCADE + student: "Student" + student = models.ForeignKey( # type: ignore[assignment] + "user.Student", + related_name="student", + on_delete=models.CASCADE, ) + # either "release" or "join" - action_type = models.CharField(max_length=64) - action_time = models.DateTimeField(default=timezone.now) + action_type: str + action_type = models.CharField(max_length=64) # type: ignore[assignment] + + action_time: "datetime" + action_time = models.DateTimeField( # type: ignore[assignment] + default=timezone.now + ) class TotalActivity(models.Model): @@ -52,13 +96,32 @@ class TotalActivity(models.Model): records all total activity. An example of this is total ever registrations. """ - teacher_registrations = models.PositiveIntegerField(default=0) - student_registrations = models.PositiveIntegerField(default=0) - independent_registrations = models.PositiveIntegerField(default=0) - anonymised_unverified_teachers = models.PositiveIntegerField(default=0) - anonymised_unverified_independents = models.PositiveIntegerField(default=0) + teacher_registrations: int + teacher_registrations = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] + + student_registrations: int + student_registrations = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] + + independent_registrations: int + independent_registrations = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] + + anonymised_unverified_teachers: int + anonymised_unverified_teachers = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] + + anonymised_unverified_independents: int + anonymised_unverified_independents = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] - class Meta: + class Meta(TypedModelMeta): verbose_name_plural = "Total activity" def __str__(self): @@ -72,20 +135,62 @@ class DailyActivity(models.Model): cards methods, per day. """ - date = models.DateField(default=timezone.now) - csv_click_count = models.PositiveIntegerField(default=0) - login_cards_click_count = models.PositiveIntegerField(default=0) - primary_coding_club_downloads = models.PositiveIntegerField(default=0) - python_coding_club_downloads = models.PositiveIntegerField(default=0) - level_control_submits = models.PositiveBigIntegerField(default=0) - teacher_lockout_resets = models.PositiveIntegerField(default=0) - indy_lockout_resets = models.PositiveIntegerField(default=0) - school_student_lockout_resets = models.PositiveIntegerField(default=0) - anonymised_unverified_teachers = models.PositiveIntegerField(default=0) - anonymised_unverified_independents = models.PositiveIntegerField(default=0) - - class Meta: + date: "datetime" + date = models.DateField(default=timezone.now) # type: ignore[assignment] + + csv_click_count: int + csv_click_count = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] + + login_cards_click_count: int + login_cards_click_count = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] + + primary_coding_club_downloads: int + primary_coding_club_downloads = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] + + python_coding_club_downloads: int + python_coding_club_downloads = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] + + level_control_submits: int + level_control_submits = models.PositiveBigIntegerField( + default=0 + ) # type: ignore[assignment] + + teacher_lockout_resets: int + teacher_lockout_resets = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] + + indy_lockout_resets: int + indy_lockout_resets = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] + + school_student_lockout_resets: int + school_student_lockout_resets = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] + + anonymised_unverified_teachers: int + anonymised_unverified_teachers = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] + + anonymised_unverified_independents: int + anonymised_unverified_independents = models.PositiveIntegerField( + default=0 + ) # type: ignore[assignment] + + class Meta(TypedModelMeta): verbose_name_plural = "Daily activities" def __str__(self): + # pylint: disable-next=line-too-long return f"Activity on {self.date}: CSV clicks: {self.csv_click_count}, login cards clicks: {self.login_cards_click_count}, primary pack downloads: {self.primary_coding_club_downloads}, python pack downloads: {self.python_coding_club_downloads}, level control submits: {self.level_control_submits}, teacher lockout resets: {self.teacher_lockout_resets}, indy lockout resets: {self.indy_lockout_resets}, school student lockout resets: {self.school_student_lockout_resets}, unverified teachers anonymised: {self.anonymised_unverified_teachers}, unverified independents anonymised: {self.anonymised_unverified_independents}" diff --git a/codeforlife/user/models/otp_bypass_token.py b/codeforlife/user/models/otp_bypass_token.py index db4a4438..a669caee 100644 --- a/codeforlife/user/models/otp_bypass_token.py +++ b/codeforlife/user/models/otp_bypass_token.py @@ -15,10 +15,11 @@ from ...models import EncryptedCharField from ...types import Validators from ...validators import CharSetValidatorBuilder -from .user import User -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover from django_stubs_ext.db.models import TypedModelMeta + + from .user import User else: TypedModelMeta = object @@ -40,7 +41,7 @@ class OtpBypassToken(models.Model): # pylint: disable-next=missing-class-docstring,too-few-public-methods class Manager(models.Manager["OtpBypassToken"]): - def bulk_create(self, user: User): # type: ignore[override] + def bulk_create(self, user: "User"): # type: ignore[override] """Bulk create OTP-bypass tokens. Args: @@ -66,13 +67,15 @@ def bulk_create(self, user: User): # type: ignore[override] objects: Manager = Manager() - user = models.ForeignKey( - User, + user: "User" + user = models.ForeignKey( # type: ignore[assignment] + "user.User", related_name="otp_bypass_tokens", on_delete=models.CASCADE, ) - token = EncryptedCharField( + token: str + token = EncryptedCharField( # type: ignore[assignment] _("token"), max_length=100, help_text=_("The encrypted equivalent of the token."), diff --git a/codeforlife/user/models/school.py b/codeforlife/user/models/school.py index 283db471..40980fd0 100644 --- a/codeforlife/user/models/school.py +++ b/codeforlife/user/models/school.py @@ -3,6 +3,7 @@ Created on 20/02/2024 at 15:37:52(+00:00). """ +import typing as t from uuid import uuid4 from django.db import models @@ -12,6 +13,10 @@ from ...types import Validators from ...validators import UnicodeAlphanumericCharSetValidator +if t.TYPE_CHECKING: # pragma: no cover + from datetime import datetime + + # TODO: add to School.name field-validators in new schema. school_name_validators: Validators = [ UnicodeAlphanumericCharSetValidator( @@ -22,23 +27,49 @@ class SchoolModelManager(models.Manager): + """Manager for School model.""" + def get_original_queryset(self): + """Get the original queryset without filtering.""" return super().get_queryset() - # Filter out inactive schools by default def get_queryset(self): + """Filter out inactive schools by default.""" return super().get_queryset().filter(is_active=True) class School(models.Model): - name = models.CharField(max_length=200, unique=True) - country = CountryField( - blank_label="(select country)", null=True, blank=True + """A school.""" + + name: str + name = models.CharField( # type: ignore[assignment] + max_length=200, + unique=True, + ) + + country: t.Optional[str] + country = CountryField( # type: ignore[assignment] + blank_label="(select country)", + null=True, + blank=True, ) + # TODO: Create an Address model to house address details - county = models.CharField(max_length=50, blank=True, null=True) - creation_time = models.DateTimeField(default=timezone.now, null=True) - is_active = models.BooleanField(default=True) + county: t.Optional[str] + county = models.CharField( # type: ignore[assignment] + max_length=50, + blank=True, + null=True, + ) + + creation_time: t.Optional["datetime"] + creation_time = models.DateTimeField( # type: ignore[assignment] + default=timezone.now, + null=True, + ) + + is_active: bool + is_active = models.BooleanField(default=True) # type: ignore[assignment] objects = SchoolModelManager() @@ -46,6 +77,7 @@ def __str__(self): return self.name def classes(self): + """Get all classes associated with the school.""" teachers = self.teacher_school.all() if teachers: classes = [] @@ -56,6 +88,7 @@ def classes(self): return None def admins(self): + """Get all admin teachers associated with the school.""" teachers = self.teacher_school.all() return ( [teacher for teacher in teachers if teacher.is_admin] @@ -64,6 +97,7 @@ def admins(self): ) def anonymise(self): + """Anonymize the school.""" self.name = uuid4().hex self.is_active = False self.save() diff --git a/codeforlife/user/models/session_auth_factor.py b/codeforlife/user/models/session_auth_factor.py index 60a9277a..6a76117f 100644 --- a/codeforlife/user/models/session_auth_factor.py +++ b/codeforlife/user/models/session_auth_factor.py @@ -3,28 +3,37 @@ Created on 20/02/2024 at 15:36:28(+00:00). """ +import typing as t + from django.db import models -from .auth_factor import AuthFactor -from .session import Session +if t.TYPE_CHECKING: # pragma: no cover + from django_stubs_ext.db.models import TypedModelMeta + + from .auth_factor import AuthFactor + from .session import Session +else: + TypedModelMeta = object class SessionAuthFactor(models.Model): """A pending authentication factor for a session.""" - session = models.ForeignKey( - Session, + session: "Session" + session = models.ForeignKey( # type: ignore[assignment] + "user.Session", related_name="auth_factors", on_delete=models.CASCADE, ) - auth_factor = models.ForeignKey( - AuthFactor, + auth_factor: "AuthFactor" + auth_factor = models.ForeignKey( # type: ignore[assignment] + "user.AuthFactor", related_name="sessions", on_delete=models.CASCADE, ) - class Meta: + class Meta(TypedModelMeta): unique_together = ["session", "auth_factor"] def __str__(self): diff --git a/codeforlife/user/models/student.py b/codeforlife/user/models/student.py index 989abb72..4489fc1c 100644 --- a/codeforlife/user/models/student.py +++ b/codeforlife/user/models/student.py @@ -10,23 +10,38 @@ from django.db import models -from .klass import Class -from .user import User, UserProfile +if t.TYPE_CHECKING: # pragma: no cover + from datetime import datetime -if t.TYPE_CHECKING: from django_stubs_ext.db.models import TypedModelMeta + + from .klass import Class + from .user import User, UserProfile else: TypedModelMeta = object class StudentModelManager(models.Manager): + """Manager for Student model.""" + def get_random_username(self): + """Generate a random username that does not already exist.""" + # NOTE: avoid circular imports by importing here + # pylint: disable-next=import-outside-toplevel + from .user import User + while True: random_username = uuid4().hex[:30] # generate a random username if not User.objects.filter(username=random_username).exists(): return random_username + # pylint: disable-next=invalid-name def schoolFactory(self, klass, name, password, login_id=None): + """Factory method to create a student user associated with a class.""" + # NOTE: avoid circular imports by importing here + # pylint: disable-next=import-outside-toplevel + from .user import User, UserProfile + user = User.objects.create_user( username=self.get_random_username(), password=password, @@ -41,7 +56,13 @@ def schoolFactory(self, klass, name, password, login_id=None): login_id=login_id, ) + # pylint: disable-next=invalid-name def independentStudentFactory(self, name, email, password): + """Factory method to create an independent student user.""" + # NOTE: avoid circular imports by importing here + # pylint: disable-next=import-outside-toplevel + from .user import User, UserProfile + user = User.objects.create_user( username=email, email=email, password=password, first_name=name ) @@ -52,35 +73,60 @@ def independentStudentFactory(self, name, email, password): class Student(models.Model): - class_field = models.ForeignKey( - Class, + """A student.""" + + class_field: t.Optional["Class"] + class_field = models.ForeignKey( # type: ignore[assignment] + "user.Class", related_name="students", null=True, blank=True, on_delete=models.CASCADE, ) + # hashed uuid used for the unique direct login url - login_id = models.CharField(max_length=64, null=True) - user = models.OneToOneField(UserProfile, on_delete=models.CASCADE) - new_user = models.OneToOneField( - User, + login_id: str + login_id = models.CharField( # type: ignore[assignment] + max_length=64, + null=True, + ) + + # pylint: disable=duplicate-code + user: "UserProfile" + user = models.OneToOneField( # type: ignore[assignment] + "user.UserProfile", + on_delete=models.CASCADE, + ) + + new_user: t.Optional["User"] + new_user = models.OneToOneField( # type: ignore[assignment] + "user.User", related_name="new_student", null=True, blank=True, on_delete=models.CASCADE, ) - pending_class_request = models.ForeignKey( - Class, + # pylint: enable=duplicate-code + + pending_class_request: t.Optional["Class"] + pending_class_request = models.ForeignKey( # type: ignore[assignment] + "user.Class", related_name="class_request", null=True, blank=True, on_delete=models.SET_NULL, ) - blocked_time = models.DateTimeField(null=True, blank=True) + + blocked_time: t.Optional["datetime"] + blocked_time = models.DateTimeField( # type: ignore[assignment] + null=True, + blank=True, + ) objects = StudentModelManager() def is_independent(self): + """Whether the student is independent (not associated with a class).""" return not self.class_field def __str__(self): diff --git a/codeforlife/user/models/teacher.py b/codeforlife/user/models/teacher.py deleted file mode 100644 index f50b80b0..00000000 --- a/codeforlife/user/models/teacher.py +++ /dev/null @@ -1,298 +0,0 @@ -# TODO: remove this in new system -# mypy: disable-error-code="import-untyped" -""" -© Ocado Group -Created on 05/02/2024 at 09:49:56(+00:00). -""" - -import typing as t - -from django.db import models -from django.db.models import Q - -from .school import School -from .user import User, UserProfile - -if t.TYPE_CHECKING: - from django_stubs_ext.db.models import TypedModelMeta -else: - TypedModelMeta = object - - -class TeacherModelManager(models.Manager): - def factory(self, first_name, last_name, email, password): - user = User.objects.create_user( - username=email, - email=email, - password=password, - first_name=first_name, - last_name=last_name, - ) - - user_profile = UserProfile.objects.create(user=user) - - return Teacher.objects.create(user=user_profile, new_user=user) - - def get_original_queryset(self): - return super().get_queryset() - - # Filter out non active teachers by default - def get_queryset(self): - return super().get_queryset().filter(new_user__is_active=True) - - -class Teacher(models.Model): - user = models.OneToOneField(UserProfile, on_delete=models.CASCADE) - new_user = models.OneToOneField( - User, - related_name="new_teacher", - null=True, - blank=True, - on_delete=models.CASCADE, - ) - school = models.ForeignKey( - School, - related_name="teacher_school", - null=True, - blank=True, - on_delete=models.SET_NULL, - ) - is_admin = models.BooleanField(default=False) - blocked_time = models.DateTimeField(null=True, blank=True) - invited_by = models.ForeignKey( - "self", - related_name="invited_teachers", - null=True, - blank=True, - on_delete=models.SET_NULL, - ) - - objects = TeacherModelManager() - - class Meta: - constraints = [ - models.CheckConstraint( - check=~models.Q( - school__isnull=True, - is_admin=True, - ), - name="teacher__is_admin", - ) - ] - - def teaches(self, userprofile): - if hasattr(userprofile, "student"): - student = userprofile.student - return ( - not student.is_independent() - and student.class_field.teacher == self - ) - - def has_school(self): - return self.school is not (None or "") - - def has_class(self): - return self.class_teacher.exists() - - def __str__(self): - if self.new_user is None: - return super().__str__() - - return f"{self.new_user.first_name} {self.new_user.last_name}" - - -AnyTeacher = t.TypeVar("AnyTeacher", bound=Teacher) - - -class SchoolTeacher(Teacher): - """A teacher that is in a school.""" - - school: School # type: ignore[assignment] - - class Meta(TypedModelMeta): - proxy = True - - # pylint: disable-next=missing-class-docstring - class Manager(TeacherModelManager): - def get_queryset(self): - return super().get_queryset().filter(school__isnull=False) - - # pylint: disable-next=line-too-long - objects: models.Manager["SchoolTeacher"] = Manager() # type: ignore[assignment,misc] - - @property - def student_users(self): - """All student-users the teacher can query.""" - # pylint: disable-next=import-outside-toplevel - from .user.student import StudentUser - - return StudentUser.objects.filter( - **( - {"new_student__class_field__teacher__school": self.school} - if self.is_admin - else {"new_student__class_field__teacher": self} - ) - ).prefetch_related("new_student") - - @property - def students(self): - """All students the teacher can query.""" - # pylint: disable-next=import-outside-toplevel - from .student import Student - - return Student.objects.filter( - **( - {"class_field__teacher__school": self.school} - if self.is_admin - else {"class_field__teacher": self} - ) - ).prefetch_related("new_user") - - @property - def classes(self): - """All classes the teacher can query.""" - # pylint: disable-next=import-outside-toplevel - from .klass import Class - - return Class.objects.filter(teacher__school=self.school) - - @property - def indy_users(self): - """All independent-users the teacher can query.""" - # pylint: disable-next=import-outside-toplevel - from .user.independent import IndependentUser - - return IndependentUser.objects.filter( - new_student__pending_class_request__in=self.classes - ) - - @property - def school_teacher_users(self): - """All school-teacher-users the teacher can query.""" - # pylint: disable-next=import-outside-toplevel - from .user.school_teacher import SchoolTeacherUser - - return SchoolTeacherUser.objects.filter(new_teacher__school=self.school) - - @property - def school_teachers(self): - """All school-teachers the teacher can query.""" - return SchoolTeacher.objects.filter(school=self.school) - - @property - def school_users(self): - """All users in the school the teacher can query.""" - # pylint: disable-next=import-outside-toplevel - from .user.user import User - - return User.objects.filter( - Q( # student-users - new_teacher__isnull=True, - **( - {"new_student__class_field__teacher__school": self.school} - if self.is_admin - else {"new_student__class_field__teacher": self} - ), - ) - | Q( # school-teacher-users - new_student__isnull=True, - new_teacher__school=self.school, - ) - ) - - -class AdminSchoolTeacher(SchoolTeacher): - """An admin-teacher that is in a school.""" - - is_admin: t.Literal[True] # type: ignore[assignment] - - class Meta(TypedModelMeta): - proxy = True - - # pylint: disable-next=missing-class-docstring - class Manager(SchoolTeacher.Manager): - def get_queryset(self): - return super().get_queryset().filter(is_admin=True) - - # pylint: disable-next=line-too-long - objects: models.Manager["AdminSchoolTeacher"] = Manager() # type: ignore[misc] - - @property - def is_last_admin(self): - """Whether of not the teacher is the last admin in the school.""" - return ( - not self.__class__.objects.filter(school=self.school) - .exclude(pk=self.pk) - .exists() - ) - - -class NonAdminSchoolTeacher(SchoolTeacher): - """A non-admin-teacher that is in a school.""" - - is_admin: t.Literal[False] # type: ignore[assignment] - - class Meta(TypedModelMeta): - proxy = True - - # pylint: disable-next=missing-class-docstring - class Manager(SchoolTeacher.Manager): - def get_queryset(self): - return super().get_queryset().filter(is_admin=False) - - # pylint: disable-next=line-too-long - objects: models.Manager["NonAdminSchoolTeacher"] = Manager() # type: ignore[misc] - - -class NonSchoolTeacher(Teacher): - """A teacher that is not in a school.""" - - school: None # type: ignore[assignment] - is_admin: t.Literal[False] # type: ignore[assignment] - - class Meta(TypedModelMeta): - proxy = True - - # pylint: disable-next=missing-class-docstring - class Manager(TeacherModelManager): - def get_queryset(self): - return super().get_queryset().filter(school__isnull=True) - - # pylint: disable-next=line-too-long - objects: models.Manager["NonSchoolTeacher"] = Manager() # type: ignore[assignment,misc] - - -# pylint: disable-next=invalid-name -TypedTeacher = t.Union[ - SchoolTeacher, - AdminSchoolTeacher, - NonAdminSchoolTeacher, - NonSchoolTeacher, -] - -AnyTypedTeacher = t.TypeVar("AnyTypedTeacher", bound=TypedTeacher) - - -# TODO: add this as a method on base Teacher model in new schema. -def teacher_as_type( - teacher: Teacher, typed_teacher_class: t.Type[AnyTypedTeacher] -): - """Convert a generic teacher to a typed teacher. - - Args: - teacher: The teacher to convert. - typed_teacher_class: The type of teacher to convert to. - - Returns: - An instance of the typed teacher. - """ - - return typed_teacher_class( - pk=teacher.pk, - user=teacher.user, - new_user=teacher.new_user, - school=teacher.school, - is_admin=teacher.is_admin, - blocked_time=teacher.blocked_time, - invited_by=teacher.invited_by, - ) diff --git a/codeforlife/user/models/teacher/__init__.py b/codeforlife/user/models/teacher/__init__.py new file mode 100644 index 00000000..0182d976 --- /dev/null +++ b/codeforlife/user/models/teacher/__init__.py @@ -0,0 +1,47 @@ +""" +© Ocado Group +Created on 19/02/2024 at 21:54:04(+00:00). +""" + +import typing as t + +from .admin_school import AdminSchoolTeacher +from .non_admin_school import NonAdminSchoolTeacher +from .non_school import NonSchoolTeacher +from .school import SchoolTeacher +from .teacher import AnyTeacher, Teacher + +# pylint: disable-next=invalid-name +TypedTeacher = t.Union[ + SchoolTeacher, + AdminSchoolTeacher, + NonAdminSchoolTeacher, + NonSchoolTeacher, +] + +AnyTypedTeacher = t.TypeVar("AnyTypedTeacher", bound=TypedTeacher) + + +# TODO: add this as a method on base Teacher model in new schema. +def teacher_as_type( + teacher: Teacher, typed_teacher_class: t.Type[AnyTypedTeacher] +): + """Convert a generic teacher to a typed teacher. + + Args: + teacher: The teacher to convert. + typed_teacher_class: The type of teacher to convert to. + + Returns: + An instance of the typed teacher. + """ + + return typed_teacher_class( + pk=teacher.pk, + user=teacher.user, + new_user=teacher.new_user, + school=teacher.school, + is_admin=teacher.is_admin, + blocked_time=teacher.blocked_time, + invited_by=teacher.invited_by, + ) diff --git a/codeforlife/user/models/teacher/admin_school.py b/codeforlife/user/models/teacher/admin_school.py new file mode 100644 index 00000000..9246e930 --- /dev/null +++ b/codeforlife/user/models/teacher/admin_school.py @@ -0,0 +1,41 @@ +""" +© Ocado Group +Created on 19/02/2024 at 21:54:04(+00:00). +""" + +import typing as t + +from django.db import models + +from .school import SchoolTeacher + +if t.TYPE_CHECKING: + from django_stubs_ext.db.models import TypedModelMeta +else: + TypedModelMeta = object + + +class AdminSchoolTeacher(SchoolTeacher): + """An admin-teacher that is in a school.""" + + is_admin: t.Literal[True] # type: ignore[assignment] + + class Meta(TypedModelMeta): + proxy = True + + # pylint: disable-next=missing-class-docstring + class Manager(SchoolTeacher.Manager): + def get_queryset(self): + return super().get_queryset().filter(is_admin=True) + + # pylint: disable-next=line-too-long + objects: models.Manager["AdminSchoolTeacher"] = Manager() # type: ignore[misc] + + @property + def is_last_admin(self): + """Whether of not the teacher is the last admin in the school.""" + return ( + not self.__class__.objects.filter(school=self.school) + .exclude(pk=self.pk) + .exists() + ) diff --git a/codeforlife/user/models/teacher/non_admin_school.py b/codeforlife/user/models/teacher/non_admin_school.py new file mode 100644 index 00000000..2e90ffbc --- /dev/null +++ b/codeforlife/user/models/teacher/non_admin_school.py @@ -0,0 +1,32 @@ +""" +© Ocado Group +Created on 19/02/2024 at 21:54:04(+00:00). +""" + +import typing as t + +from django.db import models + +from .school import SchoolTeacher + +if t.TYPE_CHECKING: + from django_stubs_ext.db.models import TypedModelMeta +else: + TypedModelMeta = object + + +class NonAdminSchoolTeacher(SchoolTeacher): + """A non-admin-teacher that is in a school.""" + + is_admin: t.Literal[False] # type: ignore[assignment] + + class Meta(TypedModelMeta): + proxy = True + + # pylint: disable-next=missing-class-docstring + class Manager(SchoolTeacher.Manager): + def get_queryset(self): + return super().get_queryset().filter(is_admin=False) + + # pylint: disable-next=line-too-long + objects: models.Manager["NonAdminSchoolTeacher"] = Manager() # type: ignore[misc] diff --git a/codeforlife/user/models/teacher/non_school.py b/codeforlife/user/models/teacher/non_school.py new file mode 100644 index 00000000..8c34ede4 --- /dev/null +++ b/codeforlife/user/models/teacher/non_school.py @@ -0,0 +1,33 @@ +""" +© Ocado Group +Created on 19/02/2024 at 21:54:04(+00:00). +""" + +import typing as t + +from django.db import models + +from .teacher import Teacher, TeacherModelManager + +if t.TYPE_CHECKING: + from django_stubs_ext.db.models import TypedModelMeta +else: + TypedModelMeta = object + + +class NonSchoolTeacher(Teacher): + """A teacher that is not in a school.""" + + school: None # type: ignore[assignment] + is_admin: t.Literal[False] # type: ignore[assignment] + + class Meta(TypedModelMeta): + proxy = True + + # pylint: disable-next=missing-class-docstring + class Manager(TeacherModelManager): + def get_queryset(self): + return super().get_queryset().filter(school__isnull=True) + + # pylint: disable-next=line-too-long + objects: models.Manager["NonSchoolTeacher"] = Manager() # type: ignore[assignment,misc] diff --git a/codeforlife/user/models/teacher/school.py b/codeforlife/user/models/teacher/school.py new file mode 100644 index 00000000..6c41ba35 --- /dev/null +++ b/codeforlife/user/models/teacher/school.py @@ -0,0 +1,115 @@ +""" +© Ocado Group +Created on 19/02/2024 at 21:54:04(+00:00). +""" + +import typing as t + +from django.db import models +from django.db.models import Q + +from .teacher import Teacher, TeacherModelManager + +if t.TYPE_CHECKING: + from django_stubs_ext.db.models import TypedModelMeta + + from ..school import School +else: + TypedModelMeta = object + + +class SchoolTeacher(Teacher): + """A teacher that is in a school.""" + + school: "School" # type: ignore[assignment] + + class Meta(TypedModelMeta): + proxy = True + + # pylint: disable-next=missing-class-docstring + class Manager(TeacherModelManager): + def get_queryset(self): + return super().get_queryset().filter(school__isnull=False) + + # pylint: disable-next=line-too-long + objects: models.Manager["SchoolTeacher"] = Manager() # type: ignore[assignment,misc] + + @property + def student_users(self): + """All student-users the teacher can query.""" + # pylint: disable-next=import-outside-toplevel + from ..user.student import StudentUser + + return StudentUser.objects.filter( + **( + {"new_student__class_field__teacher__school": self.school} + if self.is_admin + else {"new_student__class_field__teacher": self} + ) + ).prefetch_related("new_student") + + @property + def students(self): + """All students the teacher can query.""" + # pylint: disable-next=import-outside-toplevel + from ..student import Student + + return Student.objects.filter( + **( + {"class_field__teacher__school": self.school} + if self.is_admin + else {"class_field__teacher": self} + ) + ).prefetch_related("new_user") + + @property + def classes(self): + """All classes the teacher can query.""" + # pylint: disable-next=import-outside-toplevel + from ..klass import Class + + return Class.objects.filter(teacher__school=self.school) + + @property + def indy_users(self): + """All independent-users the teacher can query.""" + # pylint: disable-next=import-outside-toplevel + from ..user.independent import IndependentUser + + return IndependentUser.objects.filter( + new_student__pending_class_request__in=self.classes + ) + + @property + def school_teacher_users(self): + """All school-teacher-users the teacher can query.""" + # pylint: disable-next=import-outside-toplevel + from ..user.school_teacher import SchoolTeacherUser + + return SchoolTeacherUser.objects.filter(new_teacher__school=self.school) + + @property + def school_teachers(self): + """All school-teachers the teacher can query.""" + return SchoolTeacher.objects.filter(school=self.school) + + @property + def school_users(self): + """All users in the school the teacher can query.""" + # pylint: disable-next=import-outside-toplevel + from ..user.user import User + + return User.objects.filter( + Q( # student-users + new_teacher__isnull=True, + **( + {"new_student__class_field__teacher__school": self.school} + if self.is_admin + else {"new_student__class_field__teacher": self} + ), + ) + | Q( # school-teacher-users + new_student__isnull=True, + new_teacher__school=self.school, + ) + ) diff --git a/codeforlife/user/models/teacher/teacher.py b/codeforlife/user/models/teacher/teacher.py new file mode 100644 index 00000000..7604e7f2 --- /dev/null +++ b/codeforlife/user/models/teacher/teacher.py @@ -0,0 +1,139 @@ +""" +© Ocado Group +Created on 19/02/2024 at 21:54:04(+00:00). +""" + +import typing as t + +from django.db import models + +if t.TYPE_CHECKING: # pragma: no cover + from datetime import datetime + + from django_stubs_ext.db.models import TypedModelMeta + + from ..school import School + from ..user import User, UserProfile +else: + TypedModelMeta = object + + +class TeacherModelManager(models.Manager): + """Manager for Teacher model.""" + + def factory(self, first_name, last_name, email, password): + """ + Factory method to create a new teacher with an associated user and user + profile. + """ + # NOTE: avoid circular imports by importing here + # pylint: disable-next=import-outside-toplevel + from ..user import User, UserProfile + + user = User.objects.create_user( + username=email, + email=email, + password=password, + first_name=first_name, + last_name=last_name, + ) + + user_profile = UserProfile.objects.create(user=user) + + return Teacher.objects.create(user=user_profile, new_user=user) + + def get_original_queryset(self): + """Get the original queryset without filtering.""" + return super().get_queryset() + + # Filter out non active teachers by default + def get_queryset(self): + """Filter out non active teachers by default.""" + return super().get_queryset().filter(new_user__is_active=True) + + +class Teacher(models.Model): + """A teacher.""" + + user: "UserProfile" + user = models.OneToOneField( # type: ignore[assignment] + "user.UserProfile", + on_delete=models.CASCADE, + ) + + new_user: t.Optional["User"] + new_user = models.OneToOneField( # type: ignore[assignment] + "user.User", + related_name="new_teacher", + null=True, + blank=True, + on_delete=models.CASCADE, + ) + + school: t.Optional["School"] + school = models.ForeignKey( # type: ignore[assignment] + "user.School", + related_name="teacher_school", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + + is_admin: bool + is_admin = models.BooleanField(default=False) # type: ignore[assignment] + + blocked_time: t.Optional["datetime"] + blocked_time = models.DateTimeField( # type: ignore[assignment] + null=True, + blank=True, + ) + + invited_by: t.Optional["Teacher"] + invited_by = models.ForeignKey( # type: ignore[assignment] + "self", + related_name="invited_teachers", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + + objects = TeacherModelManager() + + class Meta(TypedModelMeta): + constraints = [ + models.CheckConstraint( + check=~models.Q( + school__isnull=True, + is_admin=True, + ), + name="teacher__is_admin", + ) + ] + + def teaches(self, userprofile): + """Check if the teacher teaches the given userprofile.""" + if hasattr(userprofile, "student"): + student = userprofile.student + return ( + not student.is_independent() + and student.class_field.teacher == self + ) + + return False + + def has_school(self): + """Check if the teacher has an associated school.""" + return self.school is not (None or "") + + def has_class(self): + """Check if the teacher has an associated class.""" + return self.class_teacher.exists() + + def __str__(self): + if self.new_user is None: + return super().__str__() + + return f"{self.new_user.first_name} {self.new_user.last_name}" + + +AnyTeacher = t.TypeVar("AnyTeacher", bound=Teacher) diff --git a/codeforlife/user/models/user/admin_school_teacher.py b/codeforlife/user/models/user/admin_school_teacher.py index 4f8a72e9..54d41559 100644 --- a/codeforlife/user/models/user/admin_school_teacher.py +++ b/codeforlife/user/models/user/admin_school_teacher.py @@ -10,21 +10,22 @@ from django.db.models.query import QuerySet from .school_teacher import SchoolTeacherUser, SchoolTeacherUserManager -from .user import User if t.TYPE_CHECKING: # pragma: no cover from django_stubs_ext.db.models import TypedModelMeta + + from .user import User else: TypedModelMeta = object -AnyUser = t.TypeVar("AnyUser", bound=User) +AnyUser = t.TypeVar("AnyUser", bound="User") # pylint: disable-next=missing-class-docstring,too-few-public-methods class AdminSchoolTeacherUserManager( SchoolTeacherUserManager["AdminSchoolTeacherUser"] ): - def filter_users(self, queryset: QuerySet[User]): + def filter_users(self, queryset: QuerySet["User"]): return super().filter_users(queryset).filter(new_teacher__is_admin=True) diff --git a/codeforlife/user/models/user/google.py b/codeforlife/user/models/user/google.py index cee91e46..d9dbca1b 100644 --- a/codeforlife/user/models/user/google.py +++ b/codeforlife/user/models/user/google.py @@ -13,14 +13,15 @@ from ....types import JsonDict from .contactable import ContactableUser, ContactableUserManager -from .user import User, UserProfile if t.TYPE_CHECKING: # pragma: no cover from django_stubs_ext.db.models import TypedModelMeta + + from .user import User else: TypedModelMeta = object -AnyUser = t.TypeVar("AnyUser", bound=User) +AnyUser = t.TypeVar("AnyUser", bound="User") # pylint: disable-next=missing-class-docstring,too-few-public-methods @@ -35,6 +36,9 @@ def __init__(self): ) def _sync(self, auth_header: str, refresh_token: t.Optional[str] = None): + # pylint: disable=import-outside-toplevel + from .user import UserProfile + response = self.session.get( url="https://www.googleapis.com/oauth2/v3/userinfo", headers={"Authorization": auth_header}, @@ -102,7 +106,7 @@ def sync_or_create(self, auth_header: str, refresh_token: str): """Syncs an existing Google-user or creates a new one.""" return self._sync(auth_header=auth_header, refresh_token=refresh_token) - def filter_users(self, queryset: QuerySet[User]): + def filter_users(self, queryset: QuerySet["User"]): return ( super() .filter_users(queryset) diff --git a/codeforlife/user/models/user/independent.py b/codeforlife/user/models/user/independent.py index 956aec79..c61a2854 100644 --- a/codeforlife/user/models/user/independent.py +++ b/codeforlife/user/models/user/independent.py @@ -11,21 +11,21 @@ from django.db.models.query import QuerySet from .contactable import ContactableUser, ContactableUserManager -from .user import User, UserProfile if t.TYPE_CHECKING: # pragma: no cover from django_stubs_ext.db.models import TypedModelMeta from ..student import Independent + from .user import User else: TypedModelMeta = object -AnyUser = t.TypeVar("AnyUser", bound=User) +AnyUser = t.TypeVar("AnyUser", bound="User") # pylint: disable-next=missing-class-docstring,too-few-public-methods class IndependentUserManager(ContactableUserManager["IndependentUser"]): - def filter_users(self, queryset: QuerySet[User]): + def filter_users(self, queryset: QuerySet["User"]): return ( super() .filter_users(queryset) @@ -52,11 +52,13 @@ def create_user( # type: ignore[override] # pylint: disable=import-outside-toplevel from ..other import TotalActivity from ..student import Student + from .user import UserProfile # pylint: enable=import-outside-toplevel assert "username" not in extra_fields + # pylint: disable=duplicate-code user = super().create_user( username=email, email=email, @@ -65,6 +67,7 @@ def create_user( # type: ignore[override] last_name=last_name, **extra_fields, ) + # pylint: enable=duplicate-code # NOTE: Indy user needs a student object for now while we use the # old models. diff --git a/codeforlife/user/models/user/non_admin_school_teacher.py b/codeforlife/user/models/user/non_admin_school_teacher.py index c43e58f5..d5c26a40 100644 --- a/codeforlife/user/models/user/non_admin_school_teacher.py +++ b/codeforlife/user/models/user/non_admin_school_teacher.py @@ -10,21 +10,22 @@ from django.db.models.query import QuerySet from .school_teacher import SchoolTeacherUser, SchoolTeacherUserManager -from .user import User if t.TYPE_CHECKING: # pragma: no cover from django_stubs_ext.db.models import TypedModelMeta + + from .user import User else: TypedModelMeta = object -AnyUser = t.TypeVar("AnyUser", bound=User) +AnyUser = t.TypeVar("AnyUser", bound="User") # pylint: disable-next=missing-class-docstring,too-few-public-methods class NonAdminSchoolTeacherUserManager( SchoolTeacherUserManager["NonAdminSchoolTeacherUser"] ): - def filter_users(self, queryset: QuerySet[User]): + def filter_users(self, queryset: QuerySet["User"]): return ( super().filter_users(queryset).filter(new_teacher__is_admin=False) ) diff --git a/codeforlife/user/models/user/non_school_teacher.py b/codeforlife/user/models/user/non_school_teacher.py index 62880f7d..791fac32 100644 --- a/codeforlife/user/models/user/non_school_teacher.py +++ b/codeforlife/user/models/user/non_school_teacher.py @@ -10,19 +10,20 @@ from django.db.models.query import QuerySet from .teacher import TeacherUser, TeacherUserManager -from .user import User if t.TYPE_CHECKING: # pragma: no cover from django_stubs_ext.db.models import TypedModelMeta + + from .user import User else: TypedModelMeta = object -AnyUser = t.TypeVar("AnyUser", bound=User) +AnyUser = t.TypeVar("AnyUser", bound="User") # pylint: disable-next=missing-class-docstring,too-few-public-methods class NonSchoolTeacherUserManager(TeacherUserManager["NonSchoolTeacherUser"]): - def filter_users(self, queryset: QuerySet[User]): + def filter_users(self, queryset: QuerySet["User"]): return ( super() .filter_users(queryset) diff --git a/codeforlife/user/models/user/school_teacher.py b/codeforlife/user/models/user/school_teacher.py index 8a9d24fe..97a55514 100644 --- a/codeforlife/user/models/user/school_teacher.py +++ b/codeforlife/user/models/user/school_teacher.py @@ -9,28 +9,29 @@ from django.db.models.query import QuerySet -from ..school import School from .teacher import TeacherUser, TeacherUserManager -from .user import User if t.TYPE_CHECKING: # pragma: no cover from django_stubs_ext.db.models import TypedModelMeta + + from ..school import School + from .user import User else: TypedModelMeta = object -AnyUser = t.TypeVar("AnyUser", bound=User) +AnyUser = t.TypeVar("AnyUser", bound="User") # pylint: disable-next=missing-class-docstring,too-few-public-methods class SchoolTeacherUserManager(TeacherUserManager[AnyUser], t.Generic[AnyUser]): - # pylint: disable-next=signature-differs,too-many-arguments + # pylint: disable-next=signature-differs,too-many-arguments,too-many-positional-arguments def create_user( # type: ignore[override] self, first_name: str, last_name: str, email: str, password: str, - school: School, + school: "School", is_admin: bool = False, is_verified: bool = False, **extra_fields, @@ -46,7 +47,7 @@ def create_user( # type: ignore[override] **extra_fields, ) - def filter_users(self, queryset: QuerySet[User]): + def filter_users(self, queryset: QuerySet["User"]): return ( super() .filter_users(queryset) diff --git a/codeforlife/user/models/user/teacher.py b/codeforlife/user/models/user/teacher.py index b0a71aac..8b6f4c40 100644 --- a/codeforlife/user/models/user/teacher.py +++ b/codeforlife/user/models/user/teacher.py @@ -10,30 +10,30 @@ from django.db.models import F from django.db.models.query import QuerySet -from ..school import School from .contactable import ContactableUser, ContactableUserManager -from .user import User, UserProfile if t.TYPE_CHECKING: # pragma: no cover from django_stubs_ext.db.models import TypedModelMeta + from ..school import School from ..teacher import Teacher + from .user import User else: TypedModelMeta = object -AnyUser = t.TypeVar("AnyUser", bound=User) +AnyUser = t.TypeVar("AnyUser", bound="User") # pylint: disable-next=missing-class-docstring,too-few-public-methods class TeacherUserManager(ContactableUserManager[AnyUser], t.Generic[AnyUser]): - # pylint: disable-next=too-many-arguments + # pylint: disable-next=too-many-arguments,too-many-positional-arguments def create_user( # type: ignore[override] self, first_name: str, last_name: str, email: str, password: str, - school: t.Optional[School] = None, + school: t.Optional["School"] = None, is_admin: bool = False, is_verified: bool = False, **extra_fields, @@ -42,6 +42,7 @@ def create_user( # type: ignore[override] # pylint: disable=import-outside-toplevel from ..other import TotalActivity from ..teacher import Teacher + from .user import UserProfile # pylint: enable=import-outside-toplevel @@ -72,7 +73,7 @@ def create_user( # type: ignore[override] return user - def filter_users(self, queryset: QuerySet[User]): + def filter_users(self, queryset: QuerySet["User"]): return ( super() .filter_users(queryset) diff --git a/codeforlife/user/models/user/user.py b/codeforlife/user/models/user/user.py index 2d22a2f6..9a396b5f 100644 --- a/codeforlife/user/models/user/user.py +++ b/codeforlife/user/models/user/user.py @@ -206,6 +206,8 @@ def get_queryset(self): class UserProfile(models.Model): + """A user's profile.""" + user = models.OneToOneField(User, on_delete=models.CASCADE) otp_secret = models.CharField(max_length=40, null=True, blank=True) @@ -237,5 +239,6 @@ def __str__(self): return f"{self.user.first_name} {self.user.last_name}" def joined_recently(self): + """Whether the user joined within the last week.""" now = timezone.now() return now - timedelta(days=7) <= self.user.date_joined diff --git a/codeforlife/validators/char_set/ascii.py b/codeforlife/validators/char_set/ascii.py index 9cd803dd..7efbda80 100644 --- a/codeforlife/validators/char_set/ascii.py +++ b/codeforlife/validators/char_set/ascii.py @@ -11,6 +11,7 @@ from .base import CharSetValidatorBuilder # pylint: disable=too-few-public-methods +# pylint: disable=too-many-positional-arguments # pylint: disable=too-many-arguments # pylint: disable=duplicate-code diff --git a/codeforlife/validators/char_set/base.py b/codeforlife/validators/char_set/base.py index 3ac0e797..fab7db77 100644 --- a/codeforlife/validators/char_set/base.py +++ b/codeforlife/validators/char_set/base.py @@ -12,6 +12,7 @@ # pylint: disable=too-few-public-methods # pylint: disable=too-many-arguments +# pylint: disable=too-many-positional-arguments class CharSetValidator(EnhancedRegexValidator): diff --git a/codeforlife/validators/char_set/unicode.py b/codeforlife/validators/char_set/unicode.py index b8fceb18..4ef249b2 100644 --- a/codeforlife/validators/char_set/unicode.py +++ b/codeforlife/validators/char_set/unicode.py @@ -12,6 +12,7 @@ # pylint: disable=too-few-public-methods # pylint: disable=too-many-arguments +# pylint: disable=too-many-positional-arguments # pylint: disable=duplicate-code diff --git a/codeforlife/validators/enhanced_regex.py b/codeforlife/validators/enhanced_regex.py index 4fec0d2c..4009c62f 100644 --- a/codeforlife/validators/enhanced_regex.py +++ b/codeforlife/validators/enhanced_regex.py @@ -35,7 +35,7 @@ def _compile(): class EnhancedRegexValidator(RegexValidator): """Extends Django's default regex validator to support enhanced patterns.""" - # pylint: disable-next=too-many-arguments + # pylint: disable-next=too-many-arguments,too-many-positional-arguments def __init__( self, # pylint: disable-next=redefined-outer-name From dac77f8eaa323ad437b62631a2d49b6b40b2ec29 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 11 Feb 2026 15:03:40 +0000 Subject: [PATCH 04/25] fix tests --- codeforlife/tasks/bigquery_test.py | 1 + .../user/auth/backends/student_auto.py | 14 +- .../password_validators/independent_test.py | 2 + .../auth/password_validators/student_test.py | 2 + .../auth/password_validators/teacher_test.py | 2 + codeforlife/user/fixtures/legacy.json | 1158 +++++++++++++++++ codeforlife/user/migrations/0001_initial.py | 9 +- codeforlife/user/models/user/student.py | 18 +- codeforlife/user/models/user/user.py | 16 + codeforlife/user/serializers/user_test.py | 5 +- codeforlife/user/views/klass_test.py | 2 +- codeforlife/user/views/school_test.py | 2 +- codeforlife/user/views/user_test.py | 18 +- 13 files changed, 1229 insertions(+), 20 deletions(-) create mode 100644 codeforlife/user/fixtures/legacy.json diff --git a/codeforlife/tasks/bigquery_test.py b/codeforlife/tasks/bigquery_test.py index 51374627..41b43b72 100644 --- a/codeforlife/tasks/bigquery_test.py +++ b/codeforlife/tasks/bigquery_test.py @@ -31,6 +31,7 @@ # pylint: disable-next=too-many-instance-attributes,too-many-public-methods class TestLoadDataIntoBigQueryTask(CeleryTestCase): + fixtures = ["school_1"] append_users: BigQueryTask truncate_users: BigQueryTask diff --git a/codeforlife/user/auth/backends/student_auto.py b/codeforlife/user/auth/backends/student_auto.py index 1d48e33e..a4317939 100644 --- a/codeforlife/user/auth/backends/student_auto.py +++ b/codeforlife/user/auth/backends/student_auto.py @@ -3,20 +3,20 @@ Created on 01/02/2024 at 14:44:16(+00:00). """ +import hashlib import typing as t -# isort: off -from common.helpers.generators import ( # type: ignore[import-untyped] - get_hashed_login_id, -) - -# isort: on - from ....request import HttpRequest from ...models import Student, StudentUser from .base import BaseBackend +# NOTE: copied from legacy code. +def get_hashed_login_id(login_id): + """Returns the hash of a given string used for login url""" + return hashlib.sha256(login_id.encode()).hexdigest() + + class StudentAutoBackend(BaseBackend): """Authenticate a student using their ID and auto-generated password.""" diff --git a/codeforlife/user/auth/password_validators/independent_test.py b/codeforlife/user/auth/password_validators/independent_test.py index b705c833..0ed1d028 100644 --- a/codeforlife/user/auth/password_validators/independent_test.py +++ b/codeforlife/user/auth/password_validators/independent_test.py @@ -10,6 +10,8 @@ # pylint: disable-next=missing-class-docstring class TestIndependentPasswordValidator(TestCase): + fixtures = ["school_1", "independent"] + def setUp(self): # TODO: Update to check for not student and not teacher once we # switch to new models diff --git a/codeforlife/user/auth/password_validators/student_test.py b/codeforlife/user/auth/password_validators/student_test.py index 9f275235..e638bbf9 100644 --- a/codeforlife/user/auth/password_validators/student_test.py +++ b/codeforlife/user/auth/password_validators/student_test.py @@ -10,6 +10,8 @@ # pylint: disable-next=missing-class-docstring class TestStudentPasswordValidator(TestCase): + fixtures = ["school_1"] + def setUp(self): # TODO: Remove second check once we switch to new models self.user = User.objects.filter( diff --git a/codeforlife/user/auth/password_validators/teacher_test.py b/codeforlife/user/auth/password_validators/teacher_test.py index 85d0f382..4e11f534 100644 --- a/codeforlife/user/auth/password_validators/teacher_test.py +++ b/codeforlife/user/auth/password_validators/teacher_test.py @@ -10,6 +10,8 @@ # pylint: disable-next=missing-class-docstring class TestTeacherPasswordValidator(TestCase): + fixtures = ["school_1"] + def setUp(self): self.user = User.objects.filter(new_teacher__isnull=False).first() assert self.user is not None diff --git a/codeforlife/user/fixtures/legacy.json b/codeforlife/user/fixtures/legacy.json new file mode 100644 index 00000000..98a2c5c5 --- /dev/null +++ b/codeforlife/user/fixtures/legacy.json @@ -0,0 +1,1158 @@ +[ + { + "model": "user.userprofile", + "pk": 1, + "fields": { + "user": 2, + "otp_secret": null, + "last_otp_for_time": null, + "developer": true, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 2, + "fields": { + "user": 3, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 3, + "fields": { + "user": 4, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 4, + "fields": { + "user": 5, + "otp_secret": null, + "last_otp_for_time": null, + "developer": true, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 5, + "fields": { + "user": 6, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 6, + "fields": { + "user": 7, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 7, + "fields": { + "user": 8, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 8, + "fields": { + "user": 9, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 9, + "fields": { + "user": 10, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 10, + "fields": { + "user": 11, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 11, + "fields": { + "user": 12, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 12, + "fields": { + "user": 13, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 13, + "fields": { + "user": 14, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 14, + "fields": { + "user": 15, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 15, + "fields": { + "user": 16, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 16, + "fields": { + "user": 17, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 17, + "fields": { + "user": 18, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 18, + "fields": { + "user": 19, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 19, + "fields": { + "user": 20, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 20, + "fields": { + "user": 1, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.userprofile", + "pk": 21, + "fields": { + "user": 21, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": false, + "first_name": null, + "_first_name": null, + "last_name": null, + "_last_name": null, + "email": null, + "_email": null, + "username": null, + "_username": null, + "google_refresh_token": null, + "google_sub": null + } + }, + { + "model": "user.school", + "pk": 1, + "fields": { + "name": "Swiss Federal Polytechnic", + "country": "GB", + "county": "nan", + "creation_time": null, + "is_active": true + } + }, + { + "model": "user.teacher", + "pk": 1, + "fields": { + "user": 1, + "new_user": 2, + "school": 1, + "is_admin": true, + "blocked_time": null, + "invited_by": null + } + }, + { + "model": "user.teacher", + "pk": 2, + "fields": { + "user": 2, + "new_user": 3, + "school": 1, + "is_admin": false, + "blocked_time": null, + "invited_by": null + } + }, + { + "model": "user.teacher", + "pk": 3, + "fields": { + "user": 3, + "new_user": 4, + "school": 1, + "is_admin": true, + "blocked_time": null, + "invited_by": null + } + }, + { + "model": "user.teacher", + "pk": 4, + "fields": { + "user": 20, + "new_user": 1, + "school": 1, + "is_admin": true, + "blocked_time": null, + "invited_by": null + } + }, + { + "model": "user.class", + "pk": 1, + "fields": { + "name": "Class 101", + "teacher": 1, + "access_code": "AB123", + "classmates_data_viewable": true, + "always_accept_requests": true, + "accept_requests_until": null, + "creation_time": null, + "is_active": true, + "created_by": null + } + }, + { + "model": "user.class", + "pk": 2, + "fields": { + "name": "Class 102", + "teacher": 2, + "access_code": "AB124", + "classmates_data_viewable": true, + "always_accept_requests": true, + "accept_requests_until": null, + "creation_time": null, + "is_active": true, + "created_by": null + } + }, + { + "model": "user.class", + "pk": 3, + "fields": { + "name": "Class 103", + "teacher": 2, + "access_code": "AB125", + "classmates_data_viewable": true, + "always_accept_requests": true, + "accept_requests_until": null, + "creation_time": null, + "is_active": true, + "created_by": null + } + }, + { + "model": "user.class", + "pk": 4, + "fields": { + "name": "Young Coders 101", + "teacher": 3, + "access_code": "RL123", + "classmates_data_viewable": true, + "always_accept_requests": true, + "accept_requests_until": null, + "creation_time": null, + "is_active": true, + "created_by": null + } + }, + { + "model": "user.class", + "pk": 5, + "fields": { + "name": "Portaladmin's class", + "teacher": 4, + "access_code": "PO123", + "classmates_data_viewable": true, + "always_accept_requests": true, + "accept_requests_until": null, + "creation_time": null, + "is_active": true, + "created_by": null + } + }, + { + "model": "user.student", + "pk": 1, + "fields": { + "class_field": 1, + "login_id": null, + "user": 4, + "new_user": 5, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 2, + "fields": { + "class_field": 1, + "login_id": null, + "user": 5, + "new_user": 6, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 3, + "fields": { + "class_field": null, + "login_id": null, + "user": 6, + "new_user": 7, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 4, + "fields": { + "class_field": 2, + "login_id": null, + "user": 7, + "new_user": 8, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 5, + "fields": { + "class_field": 2, + "login_id": null, + "user": 8, + "new_user": 9, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 6, + "fields": { + "class_field": 3, + "login_id": null, + "user": 9, + "new_user": 10, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 7, + "fields": { + "class_field": null, + "login_id": null, + "user": 10, + "new_user": 11, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 8, + "fields": { + "class_field": 4, + "login_id": null, + "user": 11, + "new_user": 12, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 9, + "fields": { + "class_field": 4, + "login_id": null, + "user": 12, + "new_user": 13, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 10, + "fields": { + "class_field": 4, + "login_id": null, + "user": 13, + "new_user": 14, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 11, + "fields": { + "class_field": 4, + "login_id": null, + "user": 14, + "new_user": 15, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 12, + "fields": { + "class_field": 4, + "login_id": null, + "user": 15, + "new_user": 16, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 13, + "fields": { + "class_field": 4, + "login_id": null, + "user": 16, + "new_user": 17, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 14, + "fields": { + "class_field": 4, + "login_id": null, + "user": 17, + "new_user": 18, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 15, + "fields": { + "class_field": 4, + "login_id": null, + "user": 18, + "new_user": 19, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.student", + "pk": 16, + "fields": { + "class_field": 5, + "login_id": null, + "user": 21, + "new_user": 21, + "pending_class_request": null, + "blocked_time": null + } + }, + { + "model": "user.totalactivity", + "pk": 1, + "fields": { + "teacher_registrations": 4, + "student_registrations": 14, + "independent_registrations": 2, + "anonymised_unverified_teachers": 0, + "anonymised_unverified_independents": 0 + } + }, + { + "model": "user.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$1000000$y2lUeK8u5GnocWVuonlViT$TnNTL7sG9pcvz6MotXSSptrHdQzAFxmLnqNbj7syLS4=", + "last_login": null, + "is_superuser": true, + "username": "codeforlife-portal@ocado.com", + "first_name": "Portal", + "last_name": "Admin", + "email": "codeforlife-portal@ocado.com", + "is_staff": true, + "is_active": true, + "date_joined": "2026-02-04T16:02:33.631Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 2, + "fields": { + "password": "pbkdf2_sha256$1000000$IOkFLrqDnlDiEoASMl5OSY$C7gzJeOhwbfq/nJp47LaErHOxgA/3DIkaUI93ZUqauc=", + "last_login": null, + "is_superuser": false, + "username": "alberteinstein@codeforlife.com", + "first_name": "Albert", + "last_name": "Einstein", + "email": "alberteinstein@codeforlife.com", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:34.051Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 3, + "fields": { + "password": "pbkdf2_sha256$1000000$QHwukC6Cv822F2nSjQjLu0$09yKE5c8mFVwIHS5BWccM+q33Rs6XSCcc5rik3RD7eY=", + "last_login": null, + "is_superuser": false, + "username": "maxplanck@codeforlife.com", + "first_name": "Max", + "last_name": "Planck", + "email": "maxplanck@codeforlife.com", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:34.252Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 4, + "fields": { + "password": "pbkdf2_sha256$1000000$OcLHQ75ALkslmiUxFwe1Ft$7hpkII5kIi1lytO4PQ0p/6HdtT6//mPcZfZNjROXqrg=", + "last_login": null, + "is_superuser": false, + "username": "ramleith@codeforlife.com", + "first_name": "Ram", + "last_name": "Leith", + "email": "ramleith@codeforlife.com", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:34.448Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 5, + "fields": { + "password": "pbkdf2_sha256$1000000$6QiofvSo5LPJ0DlGshTh0h$XdFmFi/Al5vuhxw02nlZE/rUG8g2uHxFnHXs46XgT/c=", + "last_login": null, + "is_superuser": false, + "username": "leonardodavinci@codeforlife.com", + "first_name": "Leonardo", + "last_name": "DaVinci", + "email": "leonardodavinci@codeforlife.com", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:34.641Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 6, + "fields": { + "password": "pbkdf2_sha256$1000000$HF1HBQVpMLdAuYb0vt55r1$gY3+SetJqidFfylntSqmmrtremtNk8QuGeAdlHNqD9A=", + "last_login": null, + "is_superuser": false, + "username": "galileogalilei@codeforlife.com", + "first_name": "Galileo", + "last_name": "Galilei", + "email": "galileogalilei@codeforlife.com", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:34.839Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 7, + "fields": { + "password": "pbkdf2_sha256$1000000$BUSt8V8F4DwWZPlCZeS1Sc$WMQElSAhvIE7LRHx6lwTde9YG5P74dc7Az+639AHdYc=", + "last_login": null, + "is_superuser": false, + "username": "isaacnewton@codeforlife.com", + "first_name": "Isaac", + "last_name": "Newton", + "email": "isaacnewton@codeforlife.com", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:35.036Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 8, + "fields": { + "password": "pbkdf2_sha256$1000000$TaYcwU3iqPA4c4co14ZYHx$FAwypkSE7UucZod6BY/YY+A4zV4JbySNxDwd4ypOZT8=", + "last_login": null, + "is_superuser": false, + "username": "richardfeynman@codeforlife.com", + "first_name": "Richard", + "last_name": "Feynman", + "email": "richardfeynman@codeforlife.com", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:35.230Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 9, + "fields": { + "password": "pbkdf2_sha256$1000000$mCuu3Ptx9EGmKQ7FSNIqzw$dqwQK/ovljow4iGxfLIQTm8PPR8Iu5AITzQ9tVQxv9w=", + "last_login": null, + "is_superuser": false, + "username": "alexanderflemming@codeforlife.com", + "first_name": "Alexander", + "last_name": "Flemming", + "email": "alexanderflemming@codeforlife.com", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:35.422Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 10, + "fields": { + "password": "pbkdf2_sha256$1000000$isUqjEe0ZtdHC0tDFilALA$k20Gy//4APEqsGNYGa8aQPUTUBqMPKe6hI5GMIWiMnE=", + "last_login": null, + "is_superuser": false, + "username": "danielbernoulli@codeforlife.com", + "first_name": "Daniel", + "last_name": "Bernoulli", + "email": "danielbernoulli@codeforlife.com", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:35.611Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 11, + "fields": { + "password": "pbkdf2_sha256$1000000$mjbmbYr7G3WRyxxHlDcH6d$w1wf3D0lDP6pF3nSoTUD3vSRioCzPEloW3r8sXnLSJw=", + "last_login": null, + "is_superuser": false, + "username": "indianajones@codeforlife.com", + "first_name": "Indiana", + "last_name": "Jones", + "email": "indianajones@codeforlife.com", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:35.803Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 12, + "fields": { + "password": "pbkdf2_sha256$1000000$qm1oQx3HNeIx6dpY1aRzXb$xMKwoqXoW6MuHqoABww+Yq+3A1uwkLbkpaWWytNu6l0=", + "last_login": null, + "is_superuser": false, + "username": "media noah", + "first_name": "Noah", + "last_name": "Monaghan", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:35.999Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 13, + "fields": { + "password": "pbkdf2_sha256$1000000$vQZKG1iO3Np3cdOHSrG85F$QnuZLe1KvMt64uWXjz3RKOEnr6owhloxfgs3u8xB6GQ=", + "last_login": null, + "is_superuser": false, + "username": "media elliot", + "first_name": "Elliot", + "last_name": "Sharp", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:36.195Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 14, + "fields": { + "password": "pbkdf2_sha256$1000000$iCO3IsvmN5C8rpX5J0d1ob$TFK2zYume/i4StDly6EUiqFej/sOnuzV/wYE9rgr/cg=", + "last_login": null, + "is_superuser": false, + "username": "media tajmae", + "first_name": "Tajmae", + "last_name": "Joseph", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:36.394Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 15, + "fields": { + "password": "pbkdf2_sha256$1000000$6Nhm1sHtcMbFdBitdz7nxq$s5i8H7MwdH6SRQ3+DZDDfsPtqxXVDPAC5t67G4FluM4=", + "last_login": null, + "is_superuser": false, + "username": "media carlton", + "first_name": "Carlton", + "last_name": "Joseph", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:36.589Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 16, + "fields": { + "password": "pbkdf2_sha256$1000000$FRA5lx8xBTO5Rn8J9wwZZW$KiQzLItBM40bzbwRdXpKnrI1uWXv0xsSzTMkFwAewwo=", + "last_login": null, + "is_superuser": false, + "username": "media nadal", + "first_name": "Nadal", + "last_name": "Spencer-Jennings", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:36.792Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 17, + "fields": { + "password": "pbkdf2_sha256$1000000$LAQQycnLxi5rY9QCD15nHw$Ad5r4JEo5CCQrwuD2F12ajXnV/bDSL+FGQ99vM2l0/k=", + "last_login": null, + "is_superuser": false, + "username": "media freddie", + "first_name": "Freddie", + "last_name": "Goff", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:37.009Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 18, + "fields": { + "password": "pbkdf2_sha256$1000000$XHhBEg6yrF9a6TR487gNrU$DOR6TONr+9q0rLXJwB43UuezRcQkQAmEmoN0nYmm4do=", + "last_login": null, + "is_superuser": false, + "username": "media leon", + "first_name": "Leon", + "last_name": "Scott", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:37.216Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 19, + "fields": { + "password": "pbkdf2_sha256$1000000$ua0WUUCFIijIjxpLdkc25v$mvl+UejCNGop0krujly8TPlMbgWPFVZ42tH+NPDltS4=", + "last_login": null, + "is_superuser": false, + "username": "media betty", + "first_name": "Betty", + "last_name": "Kessell", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:37.413Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 20, + "fields": { + "password": "pbkdf2_sha256$1000000$o2AfZh0JqqIFRTZGHWEQKV$c1es5kdOakAvCGkeMVgDOX5q+rFhdQgOkMZiKAtqMrk=", + "last_login": null, + "is_superuser": false, + "username": "4271ee7b7ce94e34a58d1f4e82025280", + "first_name": "Deleted", + "last_name": "User", + "email": "", + "is_staff": false, + "is_active": false, + "date_joined": "2026-02-04T16:02:37.614Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "user.user", + "pk": 21, + "fields": { + "password": "pbkdf2_sha256$1000000$cv54m0PSNCQrH0MBLU56pk$nhGzkqLXL01EEtLAsoAbaWcTxuj1SOrZ7s6rBdA7eZk=", + "last_login": null, + "is_superuser": false, + "username": "adminstudent@codeforlife.com", + "first_name": "Portaladmin", + "last_name": "Student", + "email": "adminstudent@codeforlife.com", + "is_staff": false, + "is_active": true, + "date_joined": "2026-02-04T16:02:40.242Z", + "groups": [], + "user_permissions": [] + } + } +] diff --git a/codeforlife/user/migrations/0001_initial.py b/codeforlife/user/migrations/0001_initial.py index 29705ce5..aacc3369 100644 --- a/codeforlife/user/migrations/0001_initial.py +++ b/codeforlife/user/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.15 on 2026-02-11 10:37 +# Generated by Django 5.1.15 on 2026-02-11 14:16 import codeforlife.models.encrypted_char_field import codeforlife.user.models.user.admin_school_teacher @@ -228,6 +228,13 @@ class Migration(migrations.Migration): default=django.utils.timezone.now, verbose_name="date joined" ), ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), ( "groups", models.ManyToManyField( diff --git a/codeforlife/user/models/user/student.py b/codeforlife/user/models/user/student.py index ce716e22..aa94c273 100644 --- a/codeforlife/user/models/user/student.py +++ b/codeforlife/user/models/user/student.py @@ -116,8 +116,22 @@ def _get_random_login_id(): # login_id = get_random_string(length=64) # TODO: replace below code with commented out code above. - # pylint: disable-next=import-outside-toplevel - from common.helpers.generators import generate_login_id + # pylint: disable=import-outside-toplevel + import hashlib + from uuid import uuid4 + + # pylint: enable=import-outside-toplevel + + def get_hashed_login_id(login_id): + """Returns the hash of a given string used for login url""" + return hashlib.sha256(login_id.encode()).hexdigest() + + def generate_login_id(): + """Returns the uuid string and its hashed. + The string is used for URL, and the hashed is stored in the DB.""" + login_id = uuid4().hex + hashed_login_id = get_hashed_login_id(login_id) + return login_id, hashed_login_id return generate_login_id() diff --git a/codeforlife/user/models/user/user.py b/codeforlife/user/models/user/user.py index 9a396b5f..b0609b73 100644 --- a/codeforlife/user/models/user/user.py +++ b/codeforlife/user/models/user/user.py @@ -14,6 +14,7 @@ from django.db import models from django.db.models.query import QuerySet from django.utils import timezone +from django.utils.translation import gettext_lazy as _ from pyotp import TOTP from ....models import AbstractBaseUser, EncryptedCharField @@ -74,6 +75,21 @@ class User( credential_fields = frozenset(["email", "password"]) + # TODO: remove in new schema + password: str # type: ignore[assignment] + password = models.CharField( # type: ignore[assignment] + _("password"), + max_length=128, + ) + + # TODO: remove in new schema + last_login: t.Optional[datetime] # type: ignore[assignment] + last_login = models.DateTimeField( # type: ignore[assignment] + _("last login"), + blank=True, + null=True, + ) + @property def is_authenticated(self): return ( diff --git a/codeforlife/user/serializers/user_test.py b/codeforlife/user/serializers/user_test.py index 63bdd917..a77506ae 100644 --- a/codeforlife/user/serializers/user_test.py +++ b/codeforlife/user/serializers/user_test.py @@ -10,6 +10,7 @@ # pylint: disable-next=missing-class-docstring,too-many-ancestors class TestUserSerializer(ModelSerializerTestCase[User, User]): + fixtures = ["school_1", "independent"] model_serializer_class = UserSerializer # type: ignore[assignment,override] # test: to representation @@ -59,7 +60,9 @@ def test_to_representation__student(self): def test_to_representation__indy(self): """Serialize independent user to representation.""" - user = IndependentUser.objects.first() + user = IndependentUser.objects.filter( + new_student__pending_class_request__isnull=True + ).first() assert user self.assert_to_representation( diff --git a/codeforlife/user/views/klass_test.py b/codeforlife/user/views/klass_test.py index 071931ef..b7383cdb 100644 --- a/codeforlife/user/views/klass_test.py +++ b/codeforlife/user/views/klass_test.py @@ -68,7 +68,7 @@ def test_retrieve(self): user = StudentUser.objects.first() assert user - self.client.login_as(user, password="Password1") + self.client.login_as(user) # pylint: disable-next=line-too-long self.client.retrieve(model=user.student.class_field) # type: ignore[type-var] diff --git a/codeforlife/user/views/school_test.py b/codeforlife/user/views/school_test.py index cf09bc77..d9f50bb1 100644 --- a/codeforlife/user/views/school_test.py +++ b/codeforlife/user/views/school_test.py @@ -91,5 +91,5 @@ def test_retrieve(self): user = SchoolTeacherUser.objects.first() assert user - self.client.login_as(user, password="abc123") + self.client.login_as(user) self.client.retrieve(model=user.teacher.school) diff --git a/codeforlife/user/views/user_test.py b/codeforlife/user/views/user_test.py index 97717bf9..c36d833e 100644 --- a/codeforlife/user/views/user_test.py +++ b/codeforlife/user/views/user_test.py @@ -95,6 +95,7 @@ def test_get_queryset__teacher__admin(self): users = [ *list(user.teacher.school_teacher_users), *list(user.teacher.student_users), + *list(user.teacher.indy_users), ] users.sort(key=lambda user: user.pk) @@ -118,6 +119,7 @@ def test_get_queryset__teacher__non_admin(self): ) ), *list(user.teacher.student_users), + *list(user.teacher.indy_users), ] users.sort(key=lambda user: user.pk) @@ -136,10 +138,11 @@ def test_list(self): users = [ *list(user.teacher.school_teacher_users), *list(user.teacher.student_users), + *list(user.teacher.indy_users), ] users.sort(key=lambda user: user.pk) - self.client.login_as(user, password="abc123") + self.client.login_as(user) self.client.list(models=users) def test_list__students_in_class(self): @@ -172,7 +175,7 @@ def test_list__type__teacher(self): self.client.login_as(user) self.client.list( - models=school_teacher_users, + models=school_teacher_users.order_by("pk"), filters={"type": "teacher"}, ) @@ -184,7 +187,7 @@ def test_list__type__student(self): self.client.login_as(user) self.client.list( - models=student_users, + models=student_users.order_by("pk"), filters={"type": "student"}, ) @@ -196,7 +199,7 @@ def test_list__type__indy(self): self.client.login_as(user) self.client.list( - models=indy_users, + models=indy_users.order_by("pk"), filters={"type": "indy"}, ) @@ -208,13 +211,14 @@ def test_list___id(self): users = [ *list(user.teacher.school_teacher_users), *list(user.teacher.student_users), + *list(user.teacher.indy_users), ] users.sort(key=lambda user: user.pk) exclude_user_1: User = users.pop() exclude_user_2: User = users.pop() - self.client.login_as(user, password="abc123") + self.client.login_as(user) self.client.list( models=users, filters={ @@ -233,7 +237,7 @@ def test_list__name(self): school_users = user.teacher.school_users first_name, last_name = user.first_name, user.last_name[:1] - self.client.login_as(user, password="abc123") + self.client.login_as(user) self.client.list( models=school_users.filter( Q(first_name__icontains=first_name) @@ -247,5 +251,5 @@ def test_retrieve(self): user = AdminSchoolTeacherUser.objects.first() assert user - self.client.login_as(user, password="abc123") + self.client.login_as(user) self.client.retrieve(model=user) From 4b30d45251ea95b64938db024e439502994ce126 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 11 Feb 2026 15:17:05 +0000 Subject: [PATCH 05/25] fix --- codeforlife/models/encrypted_char_field.py | 6 ++++++ setup.py | 1 + 2 files changed, 7 insertions(+) diff --git a/codeforlife/models/encrypted_char_field.py b/codeforlife/models/encrypted_char_field.py index 056e94e0..07a52111 100644 --- a/codeforlife/models/encrypted_char_field.py +++ b/codeforlife/models/encrypted_char_field.py @@ -23,6 +23,12 @@ def __init__(self, *args, **kwargs): kwargs["max_length"] += len(self._prefix) super().__init__(*args, **kwargs) + def deconstruct(self): + # pylint: disable-next=no-member + name, path, args, kwargs = super().deconstruct() + kwargs["max_length"] += len(self._prefix) + return name, path, args, kwargs + # pylint: disable-next=unused-argument def from_db_value(self, value: t.Optional[str], expression, connection): """ diff --git a/setup.py b/setup.py index 39a06af2..2a20229f 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ import typing as t from pathlib import Path +# pylint: disable-next=import-error from setuptools import find_packages, setup # type: ignore[import-untyped] from codeforlife import DATA_DIR, TEMPLATES_DIR, __version__ From c658dcca574622fefd649d87d4c079af129339dc Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 11 Feb 2026 15:35:25 +0000 Subject: [PATCH 06/25] upgrade packages --- Pipfile | 6 +-- Pipfile.lock | 106 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 65 insertions(+), 47 deletions(-) diff --git a/Pipfile b/Pipfile index 02a90ead..47575f45 100644 --- a/Pipfile +++ b/Pipfile @@ -5,9 +5,9 @@ name = "pypi" [packages] celery = {version = "==5.4.0", extras = ["sqs"]} -cryptography = "==44.0.1" +cryptography = "==46.0.5" boto3 = "==1.36.14" -django = "==5.1.15" +django = "==5.2.11" djangorestframework = "==3.16.0" django-filter = "==25.1" django-countries = "==7.6.1" @@ -46,7 +46,7 @@ pylint = "==4.0.4" pylint-django = "==2.7.0" isort = "==5.13.2" mypy = "==1.15.0" -django-stubs = {version = "==5.1.3", extras = ["compatible-mypy"]} +django-stubs = {version = "==5.2.9", extras = ["compatible-mypy"]} djangorestframework-stubs = {version = "==3.15.3", extras = ["compatible-mypy"]} types-regex = "==2024.11.6.*" types-psutil = "==7.0.0.20250601" diff --git a/Pipfile.lock b/Pipfile.lock index 742fd686..16501cf7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b1b213110b3c6f5a403ba8b3442fb4aa9679d65403d6189c9cf382ea4f742cc5" + "sha256": "35766b468d3e233eb753ef7405409d3e6942204311d42b4d57b192bafa4446a6" }, "pipfile-spec": 6, "requires": { @@ -326,41 +326,59 @@ }, "cryptography": { "hashes": [ - "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", - "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", - "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183", - "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", - "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", - "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", - "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", - "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", - "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", - "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", - "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83", - "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12", - "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", - "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", - "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", - "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", - "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", - "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", - "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4", - "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", - "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", - "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", - "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", - "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7", - "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", - "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", - "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", - "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", - "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420", - "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", - "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00" + "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", + "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", + "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", + "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", + "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", + "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", + "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", + "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", + "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", + "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", + "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", + "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", + "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", + "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", + "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", + "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", + "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", + "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", + "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", + "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", + "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", + "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", + "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", + "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", + "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", + "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", + "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", + "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", + "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", + "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", + "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", + "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", + "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", + "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", + "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", + "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", + "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", + "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", + "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", + "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", + "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", + "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", + "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", + "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", + "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", + "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", + "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", + "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", + "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87" ], "index": "pypi", - "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==44.0.1" + "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", + "version": "==46.0.5" }, "diff-match-patch": { "hashes": [ @@ -372,12 +390,12 @@ }, "django": { "hashes": [ - "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432", - "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947" + "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3", + "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==5.1.15" + "version": "==5.2.11" }, "django-cors-headers": { "hashes": [ @@ -1609,12 +1627,12 @@ }, "django": { "hashes": [ - "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432", - "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947" + "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3", + "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==5.1.15" + "version": "==5.2.11" }, "django-extensions": { "hashes": [ @@ -1630,11 +1648,11 @@ "compatible-mypy" ], "hashes": [ - "sha256:716758ced158b439213062e52de6df3cff7c586f9f9ad7ab59210efbea5dfe78", - "sha256:8c230bc5bebee6da282ba8a27ad1503c84a0c4cd2f46e63d149e76d2a63e639a" + "sha256:2317a7130afdaa76f6ff7f623650d7f3bf1b6c86a60f95840e14e6ec6de1a7cd", + "sha256:c192257120b08785cfe6f2f1c91f1797aceae8e9daa689c336e52c91e8f6a493" ], - "markers": "python_version >= '3.8'", - "version": "==5.1.3" + "markers": "python_version >= '3.10'", + "version": "==5.2.9" }, "django-stubs-ext": { "hashes": [ From 7b37cb4f7533667fac6e426c4fa47f76247f5584 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 11 Feb 2026 16:03:49 +0000 Subject: [PATCH 07/25] fix --- codeforlife/tests/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codeforlife/tests/api.py b/codeforlife/tests/api.py index 44cbe644..5856cb49 100644 --- a/codeforlife/tests/api.py +++ b/codeforlife/tests/api.py @@ -35,10 +35,10 @@ def setUpClass(cls): return super().setUpClass() - def _pre_setup(self): + def _setup_and_call(self, result, debug=False): # pylint: disable-next=protected-access self.client_class._test_case = self - super()._pre_setup() # type: ignore[misc] + super()._setup_and_call(result, debug) # type: ignore[misc] class APITestCase( From 0f5d76b014478ed0a216193ac64c5b0532e6baef Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 11 Feb 2026 16:45:00 +0000 Subject: [PATCH 08/25] fix imports --- Pipfile | 2 -- Pipfile.lock | 75 +--------------------------------------------------- 2 files changed, 1 insertion(+), 76 deletions(-) diff --git a/Pipfile b/Pipfile index 47575f45..7a93f5d4 100644 --- a/Pipfile +++ b/Pipfile @@ -11,10 +11,8 @@ django = "==5.2.11" djangorestframework = "==3.16.0" django-filter = "==25.1" django-countries = "==7.6.1" -django-two-factor-auth = "==1.17.0" django-cors-headers = "==4.7.0" django-csp = "==3.8" -django-import-export = "==4.2.0" django-storages = {version = "==1.14.6", extras = ["s3"]} pyotp = "==2.9.0" python-dotenv = "==1.0.1" diff --git a/Pipfile.lock b/Pipfile.lock index 16501cf7..02774fc4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "35766b468d3e233eb753ef7405409d3e6942204311d42b4d57b192bafa4446a6" + "sha256": "1aae90ff799f71d4e05946630c6b8957cb4ad354d878c3bb714a05037b10f468" }, "pipfile-spec": 6, "requires": { @@ -380,14 +380,6 @@ "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", "version": "==46.0.5" }, - "diff-match-patch": { - "hashes": [ - "sha256:93cea333fb8b2bc0d181b0de5e16df50dd344ce64828226bda07728818936782", - "sha256:beae57a99fa48084532935ee2968b8661db861862ec82c6f21f4acdd6d835073" - ], - "markers": "python_version >= '3.7'", - "version": "==20241021" - }, "django": { "hashes": [ "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3", @@ -431,39 +423,6 @@ "markers": "python_version >= '3.9'", "version": "==25.1" }, - "django-formtools": { - "hashes": [ - "sha256:47cb34552c6efca088863d693284d04fc36eaaf350eb21e1a1d935e0df523c93", - "sha256:bce9b64eda52cc1eef6961cc649cf75aacd1a707c2fff08d6c3efcbc8e7e761a" - ], - "markers": "python_version >= '3.8'", - "version": "==2.5.1" - }, - "django-import-export": { - "hashes": [ - "sha256:6a616046498b44bf4291610609615b00101bb2b9c4701b59b78edfaa5552aa7b", - "sha256:bb8482bd8b124f1f47e58a877e34358820d09293c65fe36c21e9dbcdee170d4d" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==4.2.0" - }, - "django-otp": { - "hashes": [ - "sha256:406d2d7f797dc313569270e06d6c360c7d986c9f653eab80b190d663ed5f1133", - "sha256:961ccf2d80a67303cb46d97427b16c476ee075acfa2b4c82a59d8f1e0745a454" - ], - "markers": "python_version >= '3.8'", - "version": "==1.7.0" - }, - "django-phonenumber-field": { - "hashes": [ - "sha256:2b83e843dac35eec6a69880a166487235b737a71a1e38c9a52e5ad67d6996083", - "sha256:7a1cb3a6456edb54d879f11ffa0acb227ded08c93b587035d0f28093f0e46511" - ], - "markers": "python_version >= '3.10'", - "version": "==8.4.0" - }, "django-storages": { "extras": [ "s3" @@ -475,15 +434,6 @@ "markers": "python_version >= '3.7'", "version": "==1.14.6" }, - "django-two-factor-auth": { - "hashes": [ - "sha256:622e78b0d6cf12eeafa239665d99c1221c399228f2f902fe478aea7759995e0e", - "sha256:a2dcc3efedd0ce4b4c14d389766c9fd8e13cabdff5e4e1b645adeb650c550cf7" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.17.0" - }, "djangorestframework": { "hashes": [ "sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361", @@ -1042,13 +992,6 @@ "markers": "python_version >= '3.7'", "version": "==2.9.0" }, - "pypng": { - "hashes": [ - "sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c", - "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1" - ], - "version": "==0.20220715.0" - }, "python-dateutil": { "hashes": [ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", @@ -1066,14 +1009,6 @@ "markers": "python_version >= '3.8'", "version": "==1.0.1" }, - "qrcode": { - "hashes": [ - "sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a", - "sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845" - ], - "markers": "python_version >= '3.7'", - "version": "==7.4.2" - }, "redis": { "extras": [ "hiredis" @@ -1227,14 +1162,6 @@ "markers": "python_version >= '3.8'", "version": "==0.5.5" }, - "tablib": { - "hashes": [ - "sha256:9a6930037cfe0f782377963ca3f2b1dae3fd4cdbf0883848f22f1447e7bb718b", - "sha256:f9db84ed398df5109bd69c11d46613d16cc572fb9ad3213f10d95e2b5f12c18e" - ], - "markers": "python_version >= '3.9'", - "version": "==3.7.0" - }, "typing-extensions": { "hashes": [ "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", From a83b9d8b029c83b97e3ed970098a79b69aa8b0a9 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 11 Feb 2026 16:50:00 +0000 Subject: [PATCH 09/25] fix --- Pipfile | 2 +- Pipfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Pipfile b/Pipfile index 7a93f5d4..b75ea629 100644 --- a/Pipfile +++ b/Pipfile @@ -8,7 +8,7 @@ celery = {version = "==5.4.0", extras = ["sqs"]} cryptography = "==46.0.5" boto3 = "==1.36.14" django = "==5.2.11" -djangorestframework = "==3.16.0" +djangorestframework = "==3.16.1" django-filter = "==25.1" django-countries = "==7.6.1" django-cors-headers = "==4.7.0" diff --git a/Pipfile.lock b/Pipfile.lock index 02774fc4..ca0c25a1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1aae90ff799f71d4e05946630c6b8957cb4ad354d878c3bb714a05037b10f468" + "sha256": "9c419d386ba3f611f24d60bc56c6850984f2d4fb82b621217dcf7699db56fea8" }, "pipfile-spec": 6, "requires": { @@ -436,12 +436,12 @@ }, "djangorestframework": { "hashes": [ - "sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361", - "sha256:f022ff46613584de994c0c6a4aebbace5fd700555fbe9d33b865ebf173eba6c9" + "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", + "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.16.0" + "version": "==3.16.1" }, "google-api-core": { "extras": [ From 584963f3823f8d893e08442411c4206991a936d5 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 11 Feb 2026 17:45:51 +0000 Subject: [PATCH 10/25] support git requirements --- setup.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 2a20229f..0995c452 100644 --- a/setup.py +++ b/setup.py @@ -66,12 +66,16 @@ def parse_requirements(packages: t.Dict[str, t.Dict[str, t.Any]]): requirements: t.List[str] = [] for name, package in packages.items(): requirement = name - if "extras" in package: - requirement += f"[{','.join(package['extras'])}]" - if "version" in package: + if "git" in package: + requirement += f" @ git+{package['git']}" + if "ref" in package: + requirement += f"@{package['ref']}" + elif "version" in package: + if "extras" in package: + requirement += f"[{','.join(package['extras'])}]" requirement += package["version"] - if "markers" in package: - requirement += f"; {package['markers']}" + if "markers" in package: + requirement += f"; {package['markers']}" requirements.append(requirement) return requirements From e1a1a3786b6fe3b0a325f84494304173bd71eac4 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 12 Feb 2026 11:36:15 +0000 Subject: [PATCH 11/25] use separate encryption key --- codeforlife/__init__.py | 2 +- codeforlife/models/encrypted_char_field.py | 2 +- codeforlife/settings/custom.py | 2 ++ codeforlife/settings/django.py | 3 +++ codeforlife/user/models/otp_bypass_token_test.py | 2 +- settings.py | 4 +--- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/codeforlife/__init__.py b/codeforlife/__init__.py index 9c7b31bd..090c6ca7 100644 --- a/codeforlife/__init__.py +++ b/codeforlife/__init__.py @@ -114,7 +114,7 @@ def set_up_settings(service_base_dir: Path, service_name: str): secrets = dotenv_values(secrets_path) secrets.setdefault( # NOTE: This is only used locally for testing purposes. - "SECRET_KEY", + "ENCRYPTION_KEY", "XTgWqMlZCMI_E5BvCArkif9nrJIIhe_6Ic6Q_UcWJDk=", ) else: diff --git a/codeforlife/models/encrypted_char_field.py b/codeforlife/models/encrypted_char_field.py index 07a52111..42a3b52e 100644 --- a/codeforlife/models/encrypted_char_field.py +++ b/codeforlife/models/encrypted_char_field.py @@ -16,7 +16,7 @@ class EncryptedCharField(models.CharField): retrieved. """ - _fernet = Fernet(settings.SECRET_KEY) + _fernet = Fernet(settings.ENCRYPTION_KEY) _prefix = "ENC:" def __init__(self, *args, **kwargs): diff --git a/codeforlife/settings/custom.py b/codeforlife/settings/custom.py index 294083ff..0259a6cf 100644 --- a/codeforlife/settings/custom.py +++ b/codeforlife/settings/custom.py @@ -144,3 +144,5 @@ def get_redis_url(): GOOGLE_CLOUD_BIGQUERY_DATASET_ID = os.getenv( "GOOGLE_CLOUD_BIGQUERY_DATASET_ID", "REPLACE_ME" ) + +ENCRYPTION_KEY = os.getenv("ENCRYPTION_KEY", "REPLACE_ME") diff --git a/codeforlife/settings/django.py b/codeforlife/settings/django.py index 330c0d77..231f3c3e 100644 --- a/codeforlife/settings/django.py +++ b/codeforlife/settings/django.py @@ -52,6 +52,9 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = bool(int(os.getenv("DEBUG", "1"))) +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.getenv("SECRET_KEY", "replace-me") + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ diff --git a/codeforlife/user/models/otp_bypass_token_test.py b/codeforlife/user/models/otp_bypass_token_test.py index 75d006d4..b423fac4 100644 --- a/codeforlife/user/models/otp_bypass_token_test.py +++ b/codeforlife/user/models/otp_bypass_token_test.py @@ -16,7 +16,7 @@ class TestOtpBypassToken(ModelTestCase[OtpBypassToken]): fixtures = ["school_2"] def setUp(self): - self.fernet = Fernet(settings.SECRET_KEY) + self.fernet = Fernet(settings.ENCRYPTION_KEY) user = User.objects.filter(otp_bypass_tokens__isnull=False).first() assert user diff --git a/settings.py b/settings.py index 18cff3eb..e6c44990 100644 --- a/settings.py +++ b/settings.py @@ -13,9 +13,7 @@ from codeforlife.settings import * # NOTE: This is only used locally for testing purposes. -SECRET_KEY = "XTgWqMlZCMI_E5BvCArkif9nrJIIhe_6Ic6Q_UcWJDk=" -# TODO: remove this when cfl-common is not longer installed -ENCRYPTION_KEY = SECRET_KEY +ENCRYPTION_KEY = "XTgWqMlZCMI_E5BvCArkif9nrJIIhe_6Ic6Q_UcWJDk=" INSTALLED_APPS = [ "django.contrib.admin", From b4cd735ccf2a08843e32507b502f26c5b7b4b2b2 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 12 Feb 2026 15:21:23 +0000 Subject: [PATCH 12/25] final fixes --- codeforlife/models/encrypted_char_field.py | 10 -- codeforlife/user/migrations/0001_initial.py | 58 +++++++++- codeforlife/user/models/__init__.py | 8 +- codeforlife/user/models/other.py | 112 ++++++++++++++++++++ codeforlife/user/models/otp_bypass_token.py | 3 +- 5 files changed, 176 insertions(+), 15 deletions(-) diff --git a/codeforlife/models/encrypted_char_field.py b/codeforlife/models/encrypted_char_field.py index 42a3b52e..774494fb 100644 --- a/codeforlife/models/encrypted_char_field.py +++ b/codeforlife/models/encrypted_char_field.py @@ -19,16 +19,6 @@ class EncryptedCharField(models.CharField): _fernet = Fernet(settings.ENCRYPTION_KEY) _prefix = "ENC:" - def __init__(self, *args, **kwargs): - kwargs["max_length"] += len(self._prefix) - super().__init__(*args, **kwargs) - - def deconstruct(self): - # pylint: disable-next=no-member - name, path, args, kwargs = super().deconstruct() - kwargs["max_length"] += len(self._prefix) - return name, path, args, kwargs - # pylint: disable-next=unused-argument def from_db_value(self, value: t.Optional[str], expression, connection): """ diff --git a/codeforlife/user/migrations/0001_initial.py b/codeforlife/user/migrations/0001_initial.py index aacc3369..ccbff82f 100644 --- a/codeforlife/user/migrations/0001_initial.py +++ b/codeforlife/user/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.15 on 2026-02-11 14:16 +# Generated by Django 5.2.11 on 2026-02-12 15:19 import codeforlife.models.encrypted_char_field import codeforlife.user.models.user.admin_school_teacher @@ -336,7 +336,7 @@ class Migration(migrations.Migration): "token", codeforlife.models.encrypted_char_field.EncryptedCharField( help_text="The encrypted equivalent of the token.", - max_length=108, + max_length=104, verbose_name="token", ), ), @@ -516,6 +516,58 @@ class Migration(migrations.Migration): ), ], ), + migrations.CreateModel( + name="SchoolTeacherInvitation", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("token", models.CharField(max_length=88)), + ("invited_teacher_first_name", models.CharField(max_length=150)), + ( + "_invited_teacher_first_name", + models.BinaryField(blank=True, null=True), + ), + ("invited_teacher_last_name", models.CharField(max_length=150)), + ( + "_invited_teacher_last_name", + models.BinaryField(blank=True, null=True), + ), + ("invited_teacher_email", models.EmailField(max_length=254)), + ("_invited_teacher_email", models.BinaryField(blank=True, null=True)), + ("invited_teacher_is_admin", models.BooleanField(default=False)), + ("expiry", models.DateTimeField()), + ( + "creation_time", + models.DateTimeField(default=django.utils.timezone.now, null=True), + ), + ("is_active", models.BooleanField(default=True)), + ( + "school", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="teacher_invitations", + to="user.school", + ), + ), + ( + "from_teacher", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="school_invitations", + to="user.teacher", + ), + ), + ], + ), migrations.AddField( model_name="class", name="created_by", @@ -583,7 +635,7 @@ class Migration(migrations.Migration): ( "google_refresh_token", codeforlife.models.encrypted_char_field.EncryptedCharField( - blank=True, max_length=1012, null=True + blank=True, max_length=1004, null=True ), ), ("google_sub", models.CharField(blank=True, max_length=255, null=True)), diff --git a/codeforlife/user/models/__init__.py b/codeforlife/user/models/__init__.py index fe9765ab..bafb5754 100644 --- a/codeforlife/user/models/__init__.py +++ b/codeforlife/user/models/__init__.py @@ -5,7 +5,13 @@ from .auth_factor import AuthFactor from .klass import Class, class_name_validators -from .other import DailyActivity, JoinReleaseStudent, TotalActivity, UserSession +from .other import ( + DailyActivity, + JoinReleaseStudent, + SchoolTeacherInvitation, + TotalActivity, + UserSession, +) from .otp_bypass_token import OtpBypassToken from .school import School, school_name_validators from .session import Session diff --git a/codeforlife/user/models/other.py b/codeforlife/user/models/other.py index 9b8437b7..ea75b8b7 100644 --- a/codeforlife/user/models/other.py +++ b/codeforlife/user/models/other.py @@ -8,6 +8,7 @@ """ import typing as t +from uuid import uuid4 from django.db import models from django.utils import timezone @@ -20,6 +21,7 @@ from .klass import Class from .school import School from .student import Student + from .teacher import Teacher from .user import User else: TypedModelMeta = object @@ -194,3 +196,113 @@ class Meta(TypedModelMeta): def __str__(self): # pylint: disable-next=line-too-long return f"Activity on {self.date}: CSV clicks: {self.csv_click_count}, login cards clicks: {self.login_cards_click_count}, primary pack downloads: {self.primary_coding_club_downloads}, python pack downloads: {self.python_coding_club_downloads}, level control submits: {self.level_control_submits}, teacher lockout resets: {self.teacher_lockout_resets}, indy lockout resets: {self.indy_lockout_resets}, school student lockout resets: {self.school_student_lockout_resets}, unverified teachers anonymised: {self.anonymised_unverified_teachers}, unverified independents anonymised: {self.anonymised_unverified_independents}" + + +class SchoolTeacherInvitationModelManager(models.Manager): + """ + A custom model manager for the SchoolTeacherInvitation model to filter out + inactive invitations by default. + """ + + def get_original_queryset(self): + """ + Get the original queryset without filtering out inactive invitations. + """ + return super().get_queryset() + + def get_queryset(self): + """ + Get the queryset for the SchoolTeacherInvitation model, filtering out + inactive invitations by default. + """ + return super().get_queryset().filter(is_active=True) + + +class SchoolTeacherInvitation(models.Model): + """ + A model to track invitations for teachers to join a school. This is meant to + be used when a teacher invites another teacher to join their school, and the + invitation needs to be tracked until the invited teacher accepts or declines + the invitation, or the invitation expires. + """ + + token: str + token = models.CharField(max_length=88) # type: ignore[assignment] + + school: t.Optional["School"] + school = models.ForeignKey( # type: ignore[assignment] + "user.School", + related_name="teacher_invitations", + null=True, + on_delete=models.SET_NULL, + ) + + from_teacher: t.Optional["Teacher"] + from_teacher = models.ForeignKey( # type: ignore[assignment] + "user.Teacher", + related_name="school_invitations", + null=True, + on_delete=models.SET_NULL, + ) + + invited_teacher_first_name: str + invited_teacher_first_name = models.CharField( # type: ignore[assignment] + max_length=150 + ) # Same as User model + # TODO: Make not nullable once data has been transferred + _invited_teacher_first_name = models.BinaryField(null=True, blank=True) + + invited_teacher_last_name: str + invited_teacher_last_name = models.CharField( # type: ignore[assignment] + max_length=150 + ) # Same as User model + # TODO: Make not nullable once data has been transferred + _invited_teacher_last_name = models.BinaryField(null=True, blank=True) + + # TODO: Switch to a CharField to be able to hold hashed value + invited_teacher_email: str + invited_teacher_email = ( + models.EmailField() # type: ignore[assignment] + ) # Same as User model + # TODO: Make not nullable once data has been transferred + _invited_teacher_email = models.BinaryField(null=True, blank=True) + + invited_teacher_is_admin: bool + invited_teacher_is_admin = models.BooleanField( # type: ignore[assignment] + default=False + ) + + expiry: "datetime" + expiry = models.DateTimeField() # type: ignore[assignment] + + # pylint: disable=duplicate-code + creation_time: t.Optional["datetime"] + creation_time = models.DateTimeField( # type: ignore[assignment] + default=timezone.now, null=True + ) + + is_active: bool + is_active = models.BooleanField(default=True) # type: ignore[assignment] + # pylint: enable=duplicate-code + + objects = SchoolTeacherInvitationModelManager() + + @property + def is_expired(self): + """Whether the invitation has expired based on the expiry datetime.""" + return self.expiry < timezone.now() + + def __str__(self): + if self.school is None: + return super().__str__() + + # pylint: disable-next=line-too-long + return f"School teacher invitation for {self.invited_teacher_email} to {self.school.name}" + + def anonymise(self): + """Anonymise the invitation.""" + self.invited_teacher_first_name = uuid4().hex + self.invited_teacher_last_name = uuid4().hex + self.invited_teacher_email = uuid4().hex + self.is_active = False + self.save() diff --git a/codeforlife/user/models/otp_bypass_token.py b/codeforlife/user/models/otp_bypass_token.py index a669caee..dfebdcbf 100644 --- a/codeforlife/user/models/otp_bypass_token.py +++ b/codeforlife/user/models/otp_bypass_token.py @@ -77,7 +77,8 @@ def bulk_create(self, user: "User"): # type: ignore[override] token: str token = EncryptedCharField( # type: ignore[assignment] _("token"), - max_length=100, + # pylint: disable-next=protected-access + max_length=100 + len(EncryptedCharField._prefix), help_text=_("The encrypted equivalent of the token."), ) From 056971fd4f41468a84a1cb5d1cf87de57fcd05ef Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 17 Feb 2026 13:46:03 +0000 Subject: [PATCH 13/25] subdirectory --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 0995c452..ba30c913 100644 --- a/setup.py +++ b/setup.py @@ -70,6 +70,8 @@ def parse_requirements(packages: t.Dict[str, t.Dict[str, t.Any]]): requirement += f" @ git+{package['git']}" if "ref" in package: requirement += f"@{package['ref']}" + if "subdirectory" in package: + requirement += f"#subdirectory={package['subdirectory']}" elif "version" in package: if "extras" in package: requirement += f"[{','.join(package['extras'])}]" From b97682452377c995bbe0ab3565f922ebb69d02f5 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 17 Feb 2026 15:48:05 +0000 Subject: [PATCH 14/25] fix --- codeforlife/settings/custom.py | 7 +++++++ codeforlife/user/models/user/user.py | 14 ++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/codeforlife/settings/custom.py b/codeforlife/settings/custom.py index 0259a6cf..f05d1e56 100644 --- a/codeforlife/settings/custom.py +++ b/codeforlife/settings/custom.py @@ -127,6 +127,13 @@ def get_redis_url(): # The URL to connect to the Redis cache. REDIS_URL = get_redis_url() +# A flag to indicate whether the old system is the current runtime to +# conditionally run code that is still needed for the old system to work but is +# no longer needed in the new system. Once the old system is fully deprecated, +# this flag and all code that depends on it should be removed. +# WARN: This setting should never be imported in the old system. +OLD_SYSTEM = False + # Our Google OAuth 2.0 client credentials # https://console.cloud.google.com/auth/clients GOOGLE_CLIENT_ID = os.getenv( diff --git a/codeforlife/user/models/user/user.py b/codeforlife/user/models/user/user.py index b0609b73..3feb655e 100644 --- a/codeforlife/user/models/user/user.py +++ b/codeforlife/user/models/user/user.py @@ -8,6 +8,8 @@ import typing as t from datetime import datetime, timedelta +from django.conf import settings + # pylint: disable-next=imported-auth-user from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import UserManager as _UserManager @@ -93,10 +95,14 @@ class User( @property def is_authenticated(self): return ( - not self.session.auth_factors.exists() - and self.userprofile.is_verified - if super().is_authenticated - else False + True + if getattr(settings, "OLD_SYSTEM", True) + else ( + not self.session.auth_factors.exists() + and self.userprofile.is_verified + if super().is_authenticated + else False + ) ) @property From c5b44ea762c9f8ee8739f1fb61ca4969da2d5a1c Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 18 Feb 2026 11:27:12 +0000 Subject: [PATCH 15/25] dynamically define is_verified --- codeforlife/user/models/user/user.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/codeforlife/user/models/user/user.py b/codeforlife/user/models/user/user.py index 3feb655e..8aa9f838 100644 --- a/codeforlife/user/models/user/user.py +++ b/codeforlife/user/models/user/user.py @@ -139,10 +139,13 @@ def last_otp_for_time(self): """Shorthand for user-profile field.""" return self.userprofile.last_otp_for_time - @property - def is_verified(self): - """Shorthand for user-profile field.""" - return self.userprofile.is_verified + # This property is set up differently in the old and new systems, so is not + # defined on the model in the old system. + is_verified: bool + # @property + # def is_verified(self): + # """Shorthand for user-profile field.""" + # return self.userprofile.is_verified @property def totp(self): @@ -206,6 +209,15 @@ def anonymize(self): ) +if not getattr(settings, "OLD_SYSTEM", True): + + def is_verified(self: User): + """Shorthand for user-profile field.""" + return self.userprofile.is_verified + + User.is_verified = property(fget=is_verified) # type: ignore[assignment] + + AnyUser = t.TypeVar("AnyUser", bound=User) From ed633c3f1ac6cf66b80ddac008f0b184a98f1506 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 23 Feb 2026 16:36:45 +0000 Subject: [PATCH 16/25] merge from main --- codeforlife/user/models/user/user.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/codeforlife/user/models/user/user.py b/codeforlife/user/models/user/user.py index 8aa9f838..f35486cb 100644 --- a/codeforlife/user/models/user/user.py +++ b/codeforlife/user/models/user/user.py @@ -19,7 +19,7 @@ from django.utils.translation import gettext_lazy as _ from pyotp import TOTP -from ....models import AbstractBaseUser, EncryptedCharField +from ....models import AbstractBaseUser from ....types import Validators from ....validators import UnicodeAlphanumericCharSetValidator @@ -249,25 +249,14 @@ class UserProfile(models.Model): developer = models.BooleanField(default=False) is_verified = models.BooleanField(default=False) - # TODO: Make not nullable once data has been transferred - first_name = models.CharField(max_length=200, null=True, blank=True) - _first_name = models.BinaryField(null=True, blank=True) - last_name = models.CharField(max_length=200, null=True, blank=True) - _last_name = models.BinaryField(null=True, blank=True) - email = models.CharField(max_length=200, null=True, blank=True) - _email = models.BinaryField(null=True, blank=True) - # TODO: Make not nullable once data has been transferred - username = models.CharField(max_length=200, null=True, blank=True) - _username = models.BinaryField(null=True, blank=True) - # Google. - google_refresh_token = EncryptedCharField( - # pylint: disable-next=protected-access - max_length=1000 + len(EncryptedCharField._prefix), - null=True, - blank=True, - ) - google_sub = models.CharField(max_length=255, null=True, blank=True) + # google_refresh_token = EncryptedCharField( + # # pylint: disable-next=protected-access + # max_length=1000 + len(EncryptedCharField._prefix), + # null=True, + # blank=True, + # ) + # google_sub = models.CharField(max_length=255, null=True, blank=True) def __str__(self): return f"{self.user.first_name} {self.user.last_name}" From 422755f3f7faaaf855d1d9ef02b9e15cf78c4bad Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 23 Feb 2026 16:48:54 +0000 Subject: [PATCH 17/25] handle old encrypted char fields --- codeforlife/user/fixtures/google_users.json | 4 +- codeforlife/user/fixtures/legacy.json | 252 ++------------------ codeforlife/user/models/user/google.py | 3 +- codeforlife/user/models/user/user.py | 2 + 4 files changed, 26 insertions(+), 235 deletions(-) diff --git a/codeforlife/user/fixtures/google_users.json b/codeforlife/user/fixtures/google_users.json index f56b4119..13cd2a2a 100644 --- a/codeforlife/user/fixtures/google_users.json +++ b/codeforlife/user/fixtures/google_users.json @@ -15,9 +15,7 @@ "pk": 34, "fields": { "user": 34, - "is_verified": true, - "google_refresh_token": "example", - "google_sub": "34" + "is_verified": true } }, { diff --git a/codeforlife/user/fixtures/legacy.json b/codeforlife/user/fixtures/legacy.json index 98a2c5c5..ff6bb9dc 100644 --- a/codeforlife/user/fixtures/legacy.json +++ b/codeforlife/user/fixtures/legacy.json @@ -7,17 +7,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": true, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -28,17 +18,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -49,17 +29,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -70,17 +40,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": true, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -91,17 +51,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -112,17 +62,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -133,17 +73,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -154,17 +84,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -175,17 +95,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -196,17 +106,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -217,17 +117,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -238,17 +128,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -259,17 +139,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -280,17 +150,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -301,17 +161,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -322,17 +172,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -343,17 +183,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -364,17 +194,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -385,17 +205,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -406,17 +216,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": true, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": true } }, { @@ -427,17 +227,7 @@ "otp_secret": null, "last_otp_for_time": null, "developer": false, - "is_verified": false, - "first_name": null, - "_first_name": null, - "last_name": null, - "_last_name": null, - "email": null, - "_email": null, - "username": null, - "_username": null, - "google_refresh_token": null, - "google_sub": null + "is_verified": false } }, { diff --git a/codeforlife/user/models/user/google.py b/codeforlife/user/models/user/google.py index d9dbca1b..08d19da7 100644 --- a/codeforlife/user/models/user/google.py +++ b/codeforlife/user/models/user/google.py @@ -82,7 +82,8 @@ def _sync(self, auth_header: str, refresh_token: t.Optional[str] = None): last_name=last_name, ) - UserProfile.objects.create( + # TODO: remove type ignore when we add back these fields. + UserProfile.objects.create( # type: ignore[misc] user=user, is_verified=is_verified, google_refresh_token=refresh_token, diff --git a/codeforlife/user/models/user/user.py b/codeforlife/user/models/user/user.py index f35486cb..6d938423 100644 --- a/codeforlife/user/models/user/user.py +++ b/codeforlife/user/models/user/user.py @@ -250,12 +250,14 @@ class UserProfile(models.Model): is_verified = models.BooleanField(default=False) # Google. + google_refresh_token: t.Optional[str] # google_refresh_token = EncryptedCharField( # # pylint: disable-next=protected-access # max_length=1000 + len(EncryptedCharField._prefix), # null=True, # blank=True, # ) + google_sub: t.Optional[str] # google_sub = models.CharField(max_length=255, null=True, blank=True) def __str__(self): From 66b7b9e5c52d93ab362cb5f885fb77008ed70ea4 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 23 Feb 2026 17:03:10 +0000 Subject: [PATCH 18/25] comment out otp bypass token tests --- .../auth/backends/otp_bypass_token_test.py | 60 +++++++------- codeforlife/user/models/otp_bypass_token.py | 2 +- .../user/models/otp_bypass_token_test.py | 79 +++++++++---------- codeforlife/user/signals/auth_factor_test.py | 34 ++++---- 4 files changed, 88 insertions(+), 87 deletions(-) diff --git a/codeforlife/user/auth/backends/otp_bypass_token_test.py b/codeforlife/user/auth/backends/otp_bypass_token_test.py index 5a98c4df..20e38b3d 100644 --- a/codeforlife/user/auth/backends/otp_bypass_token_test.py +++ b/codeforlife/user/auth/backends/otp_bypass_token_test.py @@ -3,35 +3,37 @@ Created on 10/04/2024 at 13:17:18(+01:00). """ -from ....tests import APIRequestFactory, TestCase -from ...models import AuthFactor, User -from .otp_bypass_token import OtpBypassTokenBackend +# TODO: uncomment test once CSE is implemented + +# from ....tests import APIRequestFactory, TestCase +# from ...models import AuthFactor, User +# from .otp_bypass_token import OtpBypassTokenBackend # pylint: disable-next=missing-class-docstring,too-many-instance-attributes -class TestTokenBackend(TestCase): - fixtures = ["school_2", "school_2_sessions"] - - def setUp(self): - self.backend = OtpBypassTokenBackend() - self.request_factory = APIRequestFactory(User) - - user = User.objects.filter( - otp_bypass_tokens__isnull=False, - session__isnull=False, - session__auth_factors__auth_factor__type__in=[AuthFactor.Type.OTP], - ).first() - assert user - self.user = user - - def test_authenticate(self): - """Can authenticate by bypassing a user's enabled OTP auth factor.""" - otp_bypass_token_count = self.user.otp_bypass_tokens.count() - - user = self.backend.authenticate( - request=self.request_factory.post("/", user=self.user), - token="aaaaaaaa", - ) - - assert user == self.user - assert user.otp_bypass_tokens.count() == otp_bypass_token_count - 1 +# class TestTokenBackend(TestCase): +# fixtures = ["school_2", "school_2_sessions"] + +# def setUp(self): +# self.backend = OtpBypassTokenBackend() +# self.request_factory = APIRequestFactory(User) + +# user = User.objects.filter( +# otp_bypass_tokens__isnull=False, +# session__isnull=False, +# session__auth_factors__auth_factor__type__in=[AuthFactor.Type.OTP], +# ).first() +# assert user +# self.user = user + +# def test_authenticate(self): +# """Can authenticate by bypassing a user's enabled OTP auth factor.""" +# otp_bypass_token_count = self.user.otp_bypass_tokens.count() + +# user = self.backend.authenticate( +# request=self.request_factory.post("/", user=self.user), +# token="aaaaaaaa", +# ) + +# assert user == self.user +# assert user.otp_bypass_tokens.count() == otp_bypass_token_count - 1 diff --git a/codeforlife/user/models/otp_bypass_token.py b/codeforlife/user/models/otp_bypass_token.py index 330ac8a9..73ab31fe 100644 --- a/codeforlife/user/models/otp_bypass_token.py +++ b/codeforlife/user/models/otp_bypass_token.py @@ -81,7 +81,7 @@ def bulk_create(self, user: "User"): # type: ignore[override] ) token: str - token = EncryptedTextField( + token = EncryptedTextField( # type: ignore[assignment] associated_data="token", verbose_name=_("token"), help_text=_("The encrypted equivalent of the token."), diff --git a/codeforlife/user/models/otp_bypass_token_test.py b/codeforlife/user/models/otp_bypass_token_test.py index 435f89bf..e25fa877 100644 --- a/codeforlife/user/models/otp_bypass_token_test.py +++ b/codeforlife/user/models/otp_bypass_token_test.py @@ -3,57 +3,54 @@ Created on 24/01/2024 at 16:17:22(+00:00). """ -from cryptography.fernet import Fernet -from django.conf import settings +# TODO: uncomment test once CSE is implemented -from ...tests import ModelTestCase -from .otp_bypass_token import OtpBypassToken -from .user import User +# from ...tests import ModelTestCase +# from .otp_bypass_token import OtpBypassToken +# from .user import User -# pylint: disable-next=missing-class-docstring -class TestOtpBypassToken(ModelTestCase[OtpBypassToken]): - fixtures = ["school_2"] +# # pylint: disable-next=missing-class-docstring +# class TestOtpBypassToken(ModelTestCase[OtpBypassToken]): +# fixtures = ["school_2"] - def setUp(self): - self.fernet = Fernet(settings.ENCRYPTION_KEY) +# def setUp(self): +# user = User.objects.filter(otp_bypass_tokens__isnull=False).first() +# assert user +# self.user = user - user = User.objects.filter(otp_bypass_tokens__isnull=False).first() - assert user - self.user = user +# def test_objects__bulk_create(self): +# """Can bulk create a new set of tokens.""" +# original_otp_bypass_tokens = list(self.user.otp_bypass_tokens.all()) - def test_objects__bulk_create(self): - """Can bulk create a new set of tokens.""" - original_otp_bypass_tokens = list(self.user.otp_bypass_tokens.all()) +# otp_bypass_tokens = OtpBypassToken.objects.bulk_create(self.user) - otp_bypass_tokens = OtpBypassToken.objects.bulk_create(self.user) +# for otp_bypass_token in original_otp_bypass_tokens: +# self.assert_does_not_exist(otp_bypass_token) - for otp_bypass_token in original_otp_bypass_tokens: - self.assert_does_not_exist(otp_bypass_token) +# assert len(otp_bypass_tokens) == OtpBypassToken.max_count +# assert len(otp_bypass_tokens) == self.user.otp_bypass_tokens.count() - assert len(otp_bypass_tokens) == OtpBypassToken.max_count - assert len(otp_bypass_tokens) == self.user.otp_bypass_tokens.count() +# for otp_bypass_token in otp_bypass_tokens: +# assert otp_bypass_token.token is not None +# assert len(otp_bypass_token.token) == OtpBypassToken.length +# assert all( +# char in OtpBypassToken.allowed_chars +# for char in otp_bypass_token.token +# ) - for otp_bypass_token in otp_bypass_tokens: - assert otp_bypass_token.token is not None - assert len(otp_bypass_token.token) == OtpBypassToken.length - assert all( - char in OtpBypassToken.allowed_chars - for char in otp_bypass_token.token - ) +# def test_save(self): +# """Cannot create or update a single instance.""" +# with self.assert_raises_integrity_error(): +# OtpBypassToken().save() - def test_save(self): - """Cannot create or update a single instance.""" - with self.assert_raises_integrity_error(): - OtpBypassToken().save() +# def test_check_token(self): +# """Can check a single token.""" +# otp_bypass_token = self.user.otp_bypass_tokens.first() +# assert otp_bypass_token - def test_check_token(self): - """Can check a single token.""" - otp_bypass_token = self.user.otp_bypass_tokens.first() - assert otp_bypass_token +# assert not otp_bypass_token.check_token("--------") +# otp_bypass_token.refresh_from_db() # assert exists - assert not otp_bypass_token.check_token("--------") - otp_bypass_token.refresh_from_db() # assert exists - - assert otp_bypass_token.check_token("aaaaaaaa") - self.assert_does_not_exist(otp_bypass_token) +# assert otp_bypass_token.check_token("aaaaaaaa") +# self.assert_does_not_exist(otp_bypass_token) diff --git a/codeforlife/user/signals/auth_factor_test.py b/codeforlife/user/signals/auth_factor_test.py index efc763ee..f74b49c4 100644 --- a/codeforlife/user/signals/auth_factor_test.py +++ b/codeforlife/user/signals/auth_factor_test.py @@ -3,26 +3,28 @@ Created on 17/01/2025 at 16:04:46(+00:00). """ -from django.test import TestCase +# TODO: uncomment test once CSE is implemented -from ..models import AuthFactor +# from django.test import TestCase +# from ..models import AuthFactor -# pylint: disable-next=missing-class-docstring -class TestAuthFactor(TestCase): - fixtures = ["school_2"] - def test_post_delete(self): - """Deleting an otp-auth-factor assigns a new otp-secret to its user.""" - auth_factor = AuthFactor.objects.filter( - type=AuthFactor.Type.OTP - ).first() - assert auth_factor +# # pylint: disable-next=missing-class-docstring +# class TestAuthFactor(TestCase): +# fixtures = ["school_2"] - userprofile = auth_factor.user.userprofile - otp_secret = userprofile.otp_secret +# def test_post_delete(self): +# """Deleting an otp-auth-factor assigns a new otp-secret to its user.""" +# auth_factor = AuthFactor.objects.filter( +# type=AuthFactor.Type.OTP +# ).first() +# assert auth_factor - auth_factor.delete() +# userprofile = auth_factor.user.userprofile +# otp_secret = userprofile.otp_secret - userprofile.refresh_from_db() - assert otp_secret != userprofile.otp_secret +# auth_factor.delete() + +# userprofile.refresh_from_db() +# assert otp_secret != userprofile.otp_secret From 345691467347d6e8f3ca29b0cdabe251a6270476 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 23 Feb 2026 17:30:00 +0000 Subject: [PATCH 19/25] fix linting errors --- codeforlife/user/auth/backends/otp_bypass_token_test.py | 2 +- codeforlife/user/models/otp_bypass_token.py | 2 +- codeforlife/user/signals/auth_factor_test.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/codeforlife/user/auth/backends/otp_bypass_token_test.py b/codeforlife/user/auth/backends/otp_bypass_token_test.py index 20e38b3d..8a7b4a70 100644 --- a/codeforlife/user/auth/backends/otp_bypass_token_test.py +++ b/codeforlife/user/auth/backends/otp_bypass_token_test.py @@ -21,7 +21,7 @@ # user = User.objects.filter( # otp_bypass_tokens__isnull=False, # session__isnull=False, -# session__auth_factors__auth_factor__type__in=[AuthFactor.Type.OTP], +# session__auth_factors__auth_factor__type__in=[AuthFactor.Type.OTP], # ).first() # assert user # self.user = user diff --git a/codeforlife/user/models/otp_bypass_token.py b/codeforlife/user/models/otp_bypass_token.py index 73ab31fe..23a3552f 100644 --- a/codeforlife/user/models/otp_bypass_token.py +++ b/codeforlife/user/models/otp_bypass_token.py @@ -93,7 +93,7 @@ class Meta(TypedModelMeta): @property def dek_aead(self): - return self.user.userprofile.dek_aead + return self.user.userprofile.dek_aead # type: ignore[attr-defined] def save(self, *args, **kwargs): raise IntegrityError("Cannot create or update a single instance.") diff --git a/codeforlife/user/signals/auth_factor_test.py b/codeforlife/user/signals/auth_factor_test.py index f74b49c4..033e65ca 100644 --- a/codeforlife/user/signals/auth_factor_test.py +++ b/codeforlife/user/signals/auth_factor_test.py @@ -15,7 +15,8 @@ # fixtures = ["school_2"] # def test_post_delete(self): -# """Deleting an otp-auth-factor assigns a new otp-secret to its user.""" +# """ +# Deleting an otp-auth-factor assigns a new otp-secret to its user.""" # auth_factor = AuthFactor.objects.filter( # type=AuthFactor.Type.OTP # ).first() From 663c526a1a96a61fc8a8cd885a5f599a4b8c3070 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 23 Feb 2026 17:35:09 +0000 Subject: [PATCH 20/25] test --- codeforlife/tests/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codeforlife/tests/exceptions.py b/codeforlife/tests/exceptions.py index a15d488b..462f6c0d 100644 --- a/codeforlife/tests/exceptions.py +++ b/codeforlife/tests/exceptions.py @@ -22,7 +22,7 @@ class InterruptPipelineError(Exception): """ @classmethod - # pylint: disable-next=too-many-arguments + # pylint: disable-next=too-many-arguments,too-many-positional-argument def run( cls, test_case: "TestCase", From 894599638ba8873b9aa25cab655a59abfac37cb7 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 23 Feb 2026 17:53:14 +0000 Subject: [PATCH 21/25] test --- codeforlife/tests/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codeforlife/tests/exceptions.py b/codeforlife/tests/exceptions.py index 462f6c0d..87f6dc40 100644 --- a/codeforlife/tests/exceptions.py +++ b/codeforlife/tests/exceptions.py @@ -22,7 +22,7 @@ class InterruptPipelineError(Exception): """ @classmethod - # pylint: disable-next=too-many-arguments,too-many-positional-argument + # pylint: disable-next=too-many-arguments,too-many-positional-argumen def run( cls, test_case: "TestCase", From c9184f6cd5d5650c303ceabfd47e72c44b339adb Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 23 Feb 2026 18:01:17 +0000 Subject: [PATCH 22/25] test --- codeforlife/tests/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codeforlife/tests/exceptions.py b/codeforlife/tests/exceptions.py index 87f6dc40..462f6c0d 100644 --- a/codeforlife/tests/exceptions.py +++ b/codeforlife/tests/exceptions.py @@ -22,7 +22,7 @@ class InterruptPipelineError(Exception): """ @classmethod - # pylint: disable-next=too-many-arguments,too-many-positional-argumen + # pylint: disable-next=too-many-arguments,too-many-positional-argument def run( cls, test_case: "TestCase", From 33555ed7cccc2827f589659ea9965d4c4c5d59ee Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 23 Feb 2026 18:07:04 +0000 Subject: [PATCH 23/25] fix --- codeforlife/tests/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codeforlife/tests/exceptions.py b/codeforlife/tests/exceptions.py index 462f6c0d..5662c3ce 100644 --- a/codeforlife/tests/exceptions.py +++ b/codeforlife/tests/exceptions.py @@ -22,7 +22,7 @@ class InterruptPipelineError(Exception): """ @classmethod - # pylint: disable-next=too-many-arguments,too-many-positional-argument + # pylint: disable-next=too-many-arguments,too-many-positional-arguments def run( cls, test_case: "TestCase", From 4d55e43b1161de5f638c2f5dd520ddba33eb72a3 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 24 Feb 2026 11:36:10 +0000 Subject: [PATCH 24/25] delete ENCRYPTION_KEY --- codeforlife/__init__.py | 5 ----- settings.py | 3 --- 2 files changed, 8 deletions(-) diff --git a/codeforlife/__init__.py b/codeforlife/__init__.py index 090c6ca7..b00ba40e 100644 --- a/codeforlife/__init__.py +++ b/codeforlife/__init__.py @@ -112,11 +112,6 @@ def set_up_settings(service_base_dir: Path, service_name: str): secrets_file.write(secrets_file_comment) secrets = dotenv_values(secrets_path) - secrets.setdefault( - # NOTE: This is only used locally for testing purposes. - "ENCRYPTION_KEY", - "XTgWqMlZCMI_E5BvCArkif9nrJIIhe_6Ic6Q_UcWJDk=", - ) else: # pylint: disable-next=import-outside-toplevel import boto3 diff --git a/settings.py b/settings.py index e6c44990..f74c773a 100644 --- a/settings.py +++ b/settings.py @@ -12,9 +12,6 @@ # pylint: disable-next=wildcard-import,unused-wildcard-import,wrong-import-position from codeforlife.settings import * -# NOTE: This is only used locally for testing purposes. -ENCRYPTION_KEY = "XTgWqMlZCMI_E5BvCArkif9nrJIIhe_6Ic6Q_UcWJDk=" - INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", From 0ec24d0a88ad87d5a9524495e507a9af4721a466 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 24 Feb 2026 16:39:10 +0000 Subject: [PATCH 25/25] feedback --- codeforlife/user/migrations/0001_initial.py | 11 +------- codeforlife/user/models/klass.py | 30 --------------------- codeforlife/user/models/other.py | 6 ----- 3 files changed, 1 insertion(+), 46 deletions(-) diff --git a/codeforlife/user/migrations/0001_initial.py b/codeforlife/user/migrations/0001_initial.py index 1f21f37f..e928c13c 100644 --- a/codeforlife/user/migrations/0001_initial.py +++ b/codeforlife/user/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.11 on 2026-02-23 16:34 +# Generated by Django 5.2.11 on 2026-02-24 16:38 import codeforlife.models.fields.encrypted_text import codeforlife.user.models.user.admin_school_teacher @@ -532,17 +532,8 @@ class Migration(migrations.Migration): ), ("token", models.CharField(max_length=88)), ("invited_teacher_first_name", models.CharField(max_length=150)), - ( - "_invited_teacher_first_name", - models.BinaryField(blank=True, null=True), - ), ("invited_teacher_last_name", models.CharField(max_length=150)), - ( - "_invited_teacher_last_name", - models.BinaryField(blank=True, null=True), - ), ("invited_teacher_email", models.EmailField(max_length=254)), - ("_invited_teacher_email", models.BinaryField(blank=True, null=True)), ("invited_teacher_is_admin", models.BooleanField(default=False)), ("expiry", models.DateTimeField()), ( diff --git a/codeforlife/user/models/klass.py b/codeforlife/user/models/klass.py index e525e3d3..8d5bcced 100644 --- a/codeforlife/user/models/klass.py +++ b/codeforlife/user/models/klass.py @@ -44,21 +44,6 @@ class ClassModelManager(models.Manager): """Manager for Class model.""" - def all_members(self, user): - """Get all members of the class associated with the user.""" - members = [] - if hasattr(user, "teacher"): - members.append(user.teacher) - if user.teacher.has_school(): - classes = user.teacher.class_teacher.all() - for c in classes: - members.extend(c.students.all()) - else: - c = user.student.class_field - members.append(c.teacher) - members.extend(c.students.all()) - return members - def get_original_queryset(self): """Get the original queryset without filtering.""" return super().get_queryset() @@ -123,21 +108,6 @@ class Class(models.Model): def __str__(self): return self.name - @property - def active_game(self): - """ - Get the active game for the class, if it exists. There should only be - one active game per class. - """ - # pylint: disable-next=line-too-long - games = self.game_set.filter(game_class=self, is_archived=False) # type: ignore[attr-defined] - if len(games) >= 1: - assert ( - len(games) == 1 - ) # there should NOT be more than one active game - return games[0] - return None - def has_students(self): """Check if the class has any students.""" students = self.students.all() diff --git a/codeforlife/user/models/other.py b/codeforlife/user/models/other.py index ea75b8b7..ea000bac 100644 --- a/codeforlife/user/models/other.py +++ b/codeforlife/user/models/other.py @@ -249,23 +249,17 @@ class SchoolTeacherInvitation(models.Model): invited_teacher_first_name = models.CharField( # type: ignore[assignment] max_length=150 ) # Same as User model - # TODO: Make not nullable once data has been transferred - _invited_teacher_first_name = models.BinaryField(null=True, blank=True) invited_teacher_last_name: str invited_teacher_last_name = models.CharField( # type: ignore[assignment] max_length=150 ) # Same as User model - # TODO: Make not nullable once data has been transferred - _invited_teacher_last_name = models.BinaryField(null=True, blank=True) # TODO: Switch to a CharField to be able to hold hashed value invited_teacher_email: str invited_teacher_email = ( models.EmailField() # type: ignore[assignment] ) # Same as User model - # TODO: Make not nullable once data has been transferred - _invited_teacher_email = models.BinaryField(null=True, blank=True) invited_teacher_is_admin: bool invited_teacher_is_admin = models.BooleanField( # type: ignore[assignment]