diff --git a/Pipfile b/Pipfile index 4534dd06..51b83572 100644 --- a/Pipfile +++ b/Pipfile @@ -5,16 +5,14 @@ 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" -djangorestframework = "==3.16.0" +django = "==5.2.11" +djangorestframework = "==3.16.1" 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" @@ -26,13 +24,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.48.0" google-cloud-bigquery = "==3.38.0" tink = {version = "==1.13.0", extras = ["gcpkms"]} @@ -51,11 +42,11 @@ 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"]} +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 18df9d2b..99177924 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "696ecd4e6a607df072aa76da1da2fb578391ee41d7cfa9902a0de680fd1e2cc4" + "sha256": "ef6232591f29d42bb834968c9a088e3ef2a7c810bd5cc49c98fc5807c09e9c0b" }, "pipfile-spec": 6, "requires": { @@ -34,26 +34,18 @@ }, "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" }, "bazel-runfiles": { "hashes": [ - "sha256:57a2cc04e0b924606e8dd70fc8b31d157db43680a300f546dc60207b5ce7ca82" + "sha256:7c34df05e0115e5559cbc3069b580ef66e330e7f5dde29dba50f312dfad346d9" ], "markers": "python_version >= '3.7'", - "version": "==1.8.3" + "version": "==1.8.5" }, "billiard": { "hashes": [ @@ -198,14 +190,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", @@ -356,84 +340,70 @@ "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", - "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" - }, - "decorator": { - "hashes": [ - "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", - "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a" - ], - "markers": "python_version >= '3.8'", - "version": "==5.2.1" - }, - "diff-match-patch": { - "hashes": [ - "sha256:93cea333fb8b2bc0d181b0de5e16df50dd344ce64828226bda07728818936782", - "sha256:beae57a99fa48084532935ee2968b8661db861862ec82c6f21f4acdd6d835073" - ], - "markers": "python_version >= '3.7'", - "version": "==20241021" + "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", + "version": "==46.0.5" }, "django": { "hashes": [ - "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432", - "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947" + "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3", + "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0" ], "index": "pypi", "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" + "version": "==5.2.11" }, "django-cors-headers": { "hashes": [ @@ -469,85 +439,6 @@ "markers": "python_version >= '3.9'", "version": "==25.1" }, - "django-formtools": { - "hashes": [ - "sha256:47cb34552c6efca088863d693284d04fc36eaaf350eb21e1a1d935e0df523c93", - "sha256:bce9b64eda52cc1eef6961cc649cf75aacd1a707c2fff08d6c3efcbc8e7e761a" - ], - "index": "pypi", - "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-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" @@ -559,50 +450,25 @@ "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", - "sha256:a2dcc3efedd0ce4b4c14d389766c9fd8e13cabdff5e4e1b645adeb650c550cf7" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.17.0" - }, "djangorestframework": { "hashes": [ - "sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361", - "sha256:f022ff46613584de994c0c6a4aebbace5fd700555fbe9d33b865ebf173eba6c9" + "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", + "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.16.0" - }, - "executing": { - "hashes": [ - "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", - "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017" - ], - "markers": "python_version >= '3.8'", - "version": "==2.2.1" + "version": "==3.16.1" }, "google-api-core": { "extras": [ "grpc" ], "hashes": [ - "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7", - "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9" + "sha256:02edfa9fab31e17fc0befb5f161b3bf93c9096d99aed584625f38065c511ad9b", + "sha256:80be49ee937ff9aba0fd79a6eddfde35fe658b9953ab9b79c57dd7061afa8df5" ], - "markers": "python_version >= '3.7'", - "version": "==2.29.0" + "markers": "python_version >= '3.9'", + "version": "==2.30.0" }, "google-auth": { "hashes": [ @@ -632,11 +498,11 @@ }, "google-cloud-kms": { "hashes": [ - "sha256:2060d56cebe856ecdff04b6ea6b477a498b620ed57577804e0aea2950a2edab1", - "sha256:adc9924ec7bafe1b01236f8f5aba706902b39d64abceef0b4f739725dc66d08b" + "sha256:07f2829e4ed986220802d013219fe159ecbdecec35907a6ddeea37ea9daecd8d", + "sha256:5f7d7bdb347f13a8a2b7bad6cbdf3846a51690df7215586845b62851b88839f7" ], "markers": "python_version >= '3.7'", - "version": "==3.10.0" + "version": "==3.11.0" }, "google-crc32c": { "hashes": [ @@ -706,78 +572,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:02b82dcd2fa580f5e82b4cf62ecde1b3c7cc9ba27b946421200706a6e5acaf85", + "sha256:07eb016ea7444a22bef465cce045512756956433f54450aeaa0b443b8563b9ca", + "sha256:09fbd4bcaadb6d8604ed1504b0bdf7ac18e48467e83a9d930a70a7fefa27e862", + "sha256:0fa9943d4c7f4a14a9a876153a4e8ee2bb20a410b65c09f31510b2a42271f41b", + "sha256:13937b28986f45fee342806b07c6344db785ad74a549ebcb00c659142973556f", + "sha256:15f6e636d1152667ddb4022b37534c161c8477274edb26a0b65b215dd0a81e97", + "sha256:1a56bf3ee99af5cf32d469de91bf5de79bdac2e18082b495fc1063ea33f4f2d0", + "sha256:263307118791bc350f4642749a9c8c2d13fec496228ab11070973e568c256bfd", + "sha256:27b5cb669603efb7883a882275db88b6b5d6b6c9f0267d5846ba8699b7ace338", + "sha256:27c625532d33ace45d57e775edf1982e183ff8641c72e4e91ef7ba667a149d72", + "sha256:2b7ad2981550ce999e25ce3f10c8863f718a352a2fd655068d29ea3fd37b4907", + "sha256:2c473b54ef1618f4fb85e82ff4994de18143b74efc088b91b5a935a3a45042ba", + "sha256:34b6cb16f4b67eeb5206250dc5b4d5e8e3db939535e58efc330e4c61341554bd", + "sha256:36aeff5ba8aaf70ceb2cbf6cbba9ad6beef715ad744841f3e0cd977ec02e5966", + "sha256:389b77484959bdaad6a2b7dda44d7d1228381dd669a03f5660392aa0e9385b22", + "sha256:39d21fd30d38a5afb93f0e2e71e2ec2bd894605fb75d41d5a40060c2f98f8d11", + "sha256:39da1680d260c0c619c3b5fa2dc47480ca24d5704c7a548098bca7de7f5dd17f", + "sha256:3a8aa79bc6e004394c0abefd4b034c14affda7b66480085d87f5fbadf43b593b", + "sha256:409bfe22220889b9906739910a0ee4c197a967c21b8dd14b4b06dd477f8819ce", + "sha256:41e4605c923e0e9a84a2718e4948a53a530172bfaf1a6d1ded16ef9c5849fca2", + "sha256:4393bef64cf26dc07cd6f18eaa5170ae4eebaafd4418e7e3a59ca9526a6fa30b", + "sha256:43b930cf4f9c4a2262bb3e5d5bc40df426a72538b4f98e46f158b7eb112d2d70", + "sha256:4b8d7fda614cf2af0f73bbb042f3b7fee2ecd4aea69ec98dbd903590a1083529", + "sha256:4d50329b081c223d444751076bb5b389d4f06c2b32d51b31a1e98172e6cecfb9", + "sha256:5380268ab8513445740f1f77bd966d13043d07e2793487e61fd5b5d0935071eb", + "sha256:5572c5dd1e43dbb452b466be9794f77e3502bdb6aa6a1a7feca72c98c5085ca7", + "sha256:559f58b6823e1abc38f82e157800aff649146f8906f7998c356cd48ae274d512", + "sha256:5ce1855e8cfc217cdf6bcfe0cf046d7cf81ddcc3e6894d6cfd075f87a2d8f460", + "sha256:656a5bd142caeb8b1efe1fe0b4434ecc7781f44c97cfc7927f6608627cf178c0", + "sha256:716a544969660ed609164aff27b2effd3ff84e54ac81aa4ce77b1607ca917d22", + "sha256:75fa92c47d048d696f12b81a775316fca68385ffc6e6cb1ed1d76c8562579f74", + "sha256:7e836778c13ff70edada16567e8da0c431e8818eaae85b80d11c1ba5782eccbb", + "sha256:849cc62eb989bc3be5629d4f3acef79be0d0ff15622201ed251a86d17fef6494", + "sha256:86edb3966778fa05bfdb333688fde5dc9079f9e2a9aa6a5c42e9564b7656ba04", + "sha256:888ceb7821acd925b1c90f0cdceaed1386e69cfe25e496e0771f6c35a156132f", + "sha256:8942bdfc143b467c264b048862090c4ba9a0223c52ae28c9ae97754361372e42", + "sha256:8991c2add0d8505178ff6c3ae54bd9386279e712be82fa3733c54067aae9eda1", + "sha256:8e1fcb419da5811deb47b7749b8049f7c62b993ba17822e3c7231e3e0ba65b79", + "sha256:8f27683ca68359bd3f0eb4925824d71e538f84338b3ae337ead2ae43977d7541", + "sha256:917047c19cd120b40aab9a4b8a22e9ce3562f4a1343c0d62b3cd2d5199da3d67", + "sha256:99550e344482e3c21950c034f74668fccf8a546d50c1ecb4f717543bbdc071ba", + "sha256:9a00992d6fafe19d648b9ccb4952200c50d8e36d0cce8cf026c56ed3fdc28465", + "sha256:9dee66d142f4a8cca36b5b98a38f006419138c3c89e72071747f8fca415a6d8f", + "sha256:a40515b69ac50792f9b8ead260f194ba2bb3285375b6c40c7ff938f14c3df17d", + "sha256:a6afd191551fd72e632367dfb083e33cd185bf9ead565f2476bba8ab864ae496", + "sha256:b071dccac245c32cd6b1dd96b722283b855881ca0bf1c685cf843185f5d5d51e", + "sha256:b2acd83186305c0802dbc4d81ed0ec2f3e8658d7fde97cfba2f78d7372f05b89", + "sha256:b5d5881d72a09b8336a8f874784a8eeffacde44a7bc1a148bce5a0243a265ef0", + "sha256:ca6aebae928383e971d5eace4f1a217fd7aadaf18d5ddd3163d80354105e9068", + "sha256:cd26048d066b51f39fe9206e2bcc2cea869a5e5b2d13c8d523f4179193047ebd", + "sha256:d101fe49b1e0fb4a7aa36ed0c3821a0f67a5956ef572745452d2cd790d723a3f", + "sha256:d6fb962947e4fe321eeef3be1ba5ba49d32dea9233c825fcbade8e858c14aaf4", + "sha256:db681513a1bdd879c0b24a5a6a70398da5eaaba0e077a306410dc6008426847a", + "sha256:e2a6b33d1050dce2c6f563c5caf7f7cbeebf7fba8cde37ffe3803d50526900d1", + "sha256:e49e720cd6b092504ec7bb2f60eb459aaaf4ce0e5fe20521c201b179e93b5d5d", + "sha256:e840405a3f1249509892be2399f668c59b9d492068a2cf326d661a8c79e5e747", + "sha256:ebeec1383aed86530a5f39646984e92d6596c050629982ac54eeb4e2f6ead668", + "sha256:f81816faa426da461e9a597a178832a351d6f1078102590a4b32c77d251b71eb", + "sha256:f8759a1347f3b4f03d9a9d4ce8f9f31ad5e5d0144ba06ccfb1ffaeb0ba4c1e20", + "sha256:ff7de398bb3528d44d17e6913a7cfe639e3b15c65595a71155322df16978c5e1", + "sha256:ffbb760df1cd49e0989f9826b2fd48930700db6846ac171eaff404f3cfbe5c28" ], "markers": "python_version >= '3.9'", - "version": "==1.76.0" + "version": "==1.78.1" }, "grpcio-status": { "hashes": [ - "sha256:25fcbfec74c15d1a1cb5da3fab8ee9672852dc16a5a9eeb5baf7d7a9952943cd", - "sha256:380568794055a8efbbd8871162df92012e0228a5f6dffaf57f2a00c534103b18" + "sha256:47e7fa903549c5881344f1cba23c814b5f69d09233541036eb25642d32497c8e", + "sha256:5f6660b99063f918b7f84d99cab68084aeb0dd09949e1224a6073026cea6820c" ], "markers": "python_version >= '3.9'", - "version": "==1.76.0" + "version": "==1.78.1" }, "gunicorn": { "hashes": [ @@ -915,39 +781,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", @@ -967,112 +800,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", @@ -1081,189 +808,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", @@ -1392,20 +936,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", @@ -1479,22 +1009,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", @@ -1513,13 +1027,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", @@ -1537,81 +1044,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", - "sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845" - ], - "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" @@ -1724,14 +1156,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", @@ -1741,14 +1165,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", @@ -1765,14 +1181,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", @@ -1789,21 +1197,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", - "sha256:f9db84ed398df5109bd69c11d46613d16cc572fb9ad3213f10d95e2b5f12c18e" - ], - "markers": "python_version >= '3.9'", - "version": "==3.7.0" - }, "tink": { "extras": [ "gcpkms" @@ -1834,14 +1227,6 @@ "markers": "python_version >= '3.9'", "version": "==1.13.0" }, - "traitlets": { - "hashes": [ - "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", - "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f" - ], - "markers": "python_version >= '3.8'", - "version": "==5.14.3" - }, "typing-extensions": { "hashes": [ "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", @@ -1868,11 +1253,11 @@ }, "uvicorn": { "hashes": [ - "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", - "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee" + "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", + "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187" ], "markers": "python_version >= '3.10'", - "version": "==0.40.0" + "version": "==0.41.0" }, "uvicorn-worker": { "hashes": [ @@ -1893,53 +1278,29 @@ }, "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": [ - "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": [ @@ -1983,11 +1344,11 @@ }, "botocore-stubs": { "hashes": [ - "sha256:3038388fb54db85ddccb1d2780cfe8fefed515a2c63bb98d877e6cbf338eb645", - "sha256:3ece9db3bfbf33152cbaff8f3360a791b936f3e55fd4b65f88bba4da2026ec09" + "sha256:9423110fb0e391834bd2ed44ae5f879d8cb370a444703d966d30842ce2bcb5f0", + "sha256:dbeac2f744df6b814ce83ec3f3777b299a015cbea57a2efc41c33b8c38265825" ], "markers": "python_version >= '3.9'", - "version": "==1.42.40" + "version": "==1.42.41" }, "celery-types": { "hashes": [ @@ -2138,101 +1499,115 @@ "toml" ], "hashes": [ - "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", - "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", - "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", - "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d", - "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14", - "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", - "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", - "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", - "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c", - "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b", - "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", - "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", - "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", - "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", - "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", - "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", - "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", - "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", - "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", - "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", - "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", - "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", - "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", - "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", - "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", - "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8", - "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4", - "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f", - "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", - "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", - "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", - "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", - "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", - "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", - "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", - "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", - "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0", - "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", - "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", - "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba", - "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", - "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", - "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", - "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4", - "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", - "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", - "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23", - "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7", - "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", - "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", - "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", - "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", - "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", - "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", - "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", - "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", - "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", - "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", - "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", - "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", - "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", - "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", - "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34", - "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", - "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", - "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", - "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222", - "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", - "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", - "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", - "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0", - "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301", - "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", - "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", - "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", - "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", - "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", - "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a", - "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", - "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", - "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", - "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", - "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", - "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", - "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", - "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1", - "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", - "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", - "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad", - "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", - "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", - "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb" + "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.3" + "version": "==7.13.4" }, "dill": { "hashes": [ @@ -2244,12 +1619,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": [ @@ -2265,11 +1640,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": [ @@ -2461,11 +1836,11 @@ }, "platformdirs": { "hashes": [ - "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", - "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31" + "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", + "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291" ], "markers": "python_version >= '3.10'", - "version": "==4.5.1" + "version": "==4.9.2" }, "pluggy": { "hashes": [ @@ -2503,21 +1878,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": [ @@ -2610,11 +1984,11 @@ }, "types-awscrt": { "hashes": [ - "sha256:08b13494f93f45c1a92eb264755fce50ed0d1dc75059abb5e31670feb9a09724", - "sha256:7e4364ac635f72bd57f52b093883640b1448a6eded0ecbac6e900bf4b1e4777b" + "sha256:3d6a29c1cca894b191be408f4d985a8e3a14d919785652dd3fa4ee558143e4bf", + "sha256:dc79705acd24094656b8105b8d799d7e273c8eac37c69137df580cd84beb54f6" ], "markers": "python_version >= '3.8'", - "version": "==0.31.1" + "version": "==0.31.2" }, "types-cachetools": { "hashes": [ diff --git a/codeforlife/__init__.py b/codeforlife/__init__.py index 9c7b31bd..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. - "SECRET_KEY", - "XTgWqMlZCMI_E5BvCArkif9nrJIIhe_6Ic6Q_UcWJDk=", - ) else: # pylint: disable-next=import-outside-toplevel import boto3 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/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/settings/custom.py b/codeforlife/settings/custom.py index 586209a5..13a66574 100644 --- a/codeforlife/settings/custom.py +++ b/codeforlife/settings/custom.py @@ -126,3 +126,10 @@ 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 diff --git a/codeforlife/settings/django.py b/codeforlife/settings/django.py index ff4ee774..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/ @@ -235,6 +238,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 +283,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/tasks/bigquery_test.py b/codeforlife/tasks/bigquery_test.py index 8b0e8a0f..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 @@ -180,7 +181,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.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( diff --git a/codeforlife/tests/api_client.py b/codeforlife/tests/api_client.py index 7c6a865c..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( @@ -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/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/exceptions.py b/codeforlife/tests/exceptions.py index a15d488b..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 + # pylint: disable-next=too-many-arguments,too-many-positional-arguments def run( cls, test_case: "TestCase", 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/auth/backends/otp_bypass_token_test.py b/codeforlife/user/auth/backends/otp_bypass_token_test.py index 5a98c4df..8a7b4a70 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/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/google_users.json b/codeforlife/user/fixtures/google_users.json index 6a075828..13cd2a2a 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,17 +11,15 @@ } }, { - "model": "common.userprofile", + "model": "user.userprofile", "pk": 34, "fields": { "user": 34, - "is_verified": true, - "google_refresh_token": "example", - "google_sub": "34" + "is_verified": true } }, { - "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/legacy.json b/codeforlife/user/fixtures/legacy.json new file mode 100644 index 00000000..ff6bb9dc --- /dev/null +++ b/codeforlife/user/fixtures/legacy.json @@ -0,0 +1,948 @@ +[ + { + "model": "user.userprofile", + "pk": 1, + "fields": { + "user": 2, + "otp_secret": null, + "last_otp_for_time": null, + "developer": true, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 2, + "fields": { + "user": 3, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 3, + "fields": { + "user": 4, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 4, + "fields": { + "user": 5, + "otp_secret": null, + "last_otp_for_time": null, + "developer": true, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 5, + "fields": { + "user": 6, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 6, + "fields": { + "user": 7, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 7, + "fields": { + "user": 8, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 8, + "fields": { + "user": 9, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 9, + "fields": { + "user": 10, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 10, + "fields": { + "user": 11, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 11, + "fields": { + "user": 12, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 12, + "fields": { + "user": 13, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 13, + "fields": { + "user": 14, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 14, + "fields": { + "user": 15, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 15, + "fields": { + "user": 16, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 16, + "fields": { + "user": 17, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 17, + "fields": { + "user": 18, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 18, + "fields": { + "user": 19, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 19, + "fields": { + "user": 20, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 20, + "fields": { + "user": 1, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": true + } + }, + { + "model": "user.userprofile", + "pk": 21, + "fields": { + "user": 21, + "otp_secret": null, + "last_otp_for_time": null, + "developer": false, + "is_verified": false + } + }, + { + "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/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 86f3cfb1..3c3b7a07 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", 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 bba29e65..e928c13c 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-01-19 14:38 +# 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 @@ -11,7 +11,11 @@ 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 @@ -21,52 +25,336 @@ class Migration(migrations.Migration): dependencies = [ ("auth", "0012_alter_user_first_name_max_length"), - ("common", "0058_userprofile_google_refresh_token_and_more"), ] 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" + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "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.fields.encrypted_text.EncryptedTextField( + associated_data="token", + db_column="token", + default=None, + help_text="The encrypted equivalent of the token.", + 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", @@ -91,7 +379,7 @@ class Migration(migrations.Migration): blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, - to="user.user", + to=settings.AUTH_USER_MODEL, ), ), ], @@ -102,7 +390,7 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name="OtpBypassToken", + name="Student", fields=[ ( "id", @@ -113,31 +401,42 @@ 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.fields.encrypted_text.EncryptedTextField( - associated_data="token", - db_column="token", - help_text="The encrypted equivalent of the token.", - 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", - models.ForeignKey( + "new_user", + models.OneToOneField( + blank=True, + null=True, on_delete=django.db.models.deletion.CASCADE, - related_name="otp_bypass_tokens", - to="user.user", + 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", ), ), ], - options={ - "verbose_name": "OTP bypass token", - "verbose_name_plural": "OTP bypass tokens", - }, ), migrations.CreateModel( - name="AuthFactor", + name="JoinReleaseStudent", fields=[ ( "id", @@ -148,71 +447,162 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("type", models.TextField(choices=[("otp", "one-time password")])), + ("action_type", models.CharField(max_length=64)), ( - "user", + "action_time", + models.DateTimeField(default=django.utils.timezone.now), + ), + ( + "student", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - related_name="auth_factors", - to="user.user", + related_name="student", + to="user.student", ), ), ], - options={ - "unique_together": {("user", "type")}, - }, ), migrations.CreateModel( - name="ContactableUser", + name="Independent", fields=[], options={ "proxy": True, "indexes": [], "constraints": [], }, - bases=("user.user",), - managers=[ + bases=("user.student",), + ), + migrations.CreateModel( + name="Teacher", + fields=[ ( - "objects", - codeforlife.user.models.user.contactable.ContactableUserManager(), + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("is_admin", models.BooleanField(default=False)), + ("blocked_time", models.DateTimeField(blank=True, null=True)), + ( + "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="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.CreateModel( - name="StudentUser", - fields=[], - options={ - "proxy": True, - "indexes": [], - "constraints": [], - }, - bases=("user.user",), - managers=[ - ("objects", codeforlife.user.models.user.student.StudentUserManager()), + 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_last_name", models.CharField(max_length=150)), + ("invited_teacher_email", models.EmailField(max_length=254)), + ("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", + 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="AdminSchoolTeacher", + name="NonSchoolTeacher", fields=[], options={ "proxy": True, "indexes": [], "constraints": [], }, - bases=("user.schoolteacher",), + bases=("user.teacher",), ), migrations.CreateModel( - name="NonAdminSchoolTeacher", + name="SchoolTeacher", fields=[], options={ "proxy": True, "indexes": [], "constraints": [], }, - bases=("user.schoolteacher",), + bases=("user.teacher",), ), migrations.CreateModel( - name="SessionAuthFactor", + name="UserProfile", fields=[ ( "id", @@ -223,26 +613,71 @@ class Migration(migrations.Migration): 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)), ( - "auth_factor", - models.ForeignKey( + "user", + models.OneToOneField( on_delete=django.db.models.deletion.CASCADE, - related_name="sessions", - to="user.authfactor", + 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="UserSession", + fields=[ ( - "session", + "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, - related_name="auth_factors", - to="user.session", + to=settings.AUTH_USER_MODEL, ), ), ], - options={ - "unique_together": {("session", "auth_factor")}, - }, ), migrations.CreateModel( name="GoogleUser", @@ -286,6 +721,68 @@ class Migration(migrations.Migration): ("objects", codeforlife.user.models.user.teacher.TeacherUserManager()), ], ), + migrations.CreateModel( + name="SessionAuthFactor", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "auth_factor", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="sessions", + to="user.authfactor", + ), + ), + ( + "session", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="auth_factors", + to="user.session", + ), + ), + ], + options={ + "unique_together": {("session", "auth_factor")}, + }, + ), + migrations.CreateModel( + name="AdminSchoolTeacher", + fields=[], + options={ + "proxy": True, + "indexes": [], + "constraints": [], + }, + bases=("user.schoolteacher",), + ), + migrations.CreateModel( + name="NonAdminSchoolTeacher", + fields=[], + options={ + "proxy": True, + "indexes": [], + "constraints": [], + }, + bases=("user.schoolteacher",), + ), + 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", fields=[], diff --git a/codeforlife/user/models/__init__.py b/codeforlife/user/models/__init__.py index f68e4461..bafb5754 100644 --- a/codeforlife/user/models/__init__.py +++ b/codeforlife/user/models/__init__.py @@ -5,6 +5,13 @@ from .auth_factor import AuthFactor from .klass import Class, class_name_validators +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/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 46f6dc21..8d5bcced 100644 --- a/codeforlife/user/models/klass.py +++ b/codeforlife/user/models/klass.py @@ -3,9 +3,13 @@ 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 ( @@ -13,6 +17,16 @@ UppercaseAsciiAlphanumericCharSetValidator, ) +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), MaxLengthValidator(5), @@ -25,3 +39,114 @@ special_chars="-_", ) ] + + +class ClassModelManager(models.Manager): + """Manager for Class model.""" + + def get_original_queryset(self): + """Get the original queryset without filtering.""" + return super().get_queryset() + + 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: "Teacher" + teacher = models.ForeignKey( # type: ignore[assignment] + "user.Teacher", + related_name="class_teacher", + on_delete=models.CASCADE, + ) + + 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", + on_delete=models.SET_NULL, + ) + + objects = ClassModelManager() + + def __str__(self): + return self.name + + 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." + ) + 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 " + # pylint: disable-next=no-member + + 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): + """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(TypedModelMeta): + verbose_name_plural = "classes" diff --git a/codeforlife/user/models/other.py b/codeforlife/user/models/other.py new file mode 100644 index 00000000..ea000bac --- /dev/null +++ b/codeforlife/user/models/other.py @@ -0,0 +1,302 @@ +""" +© 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. +""" + +import typing as t +from uuid import uuid4 + +from django.db import models +from django.utils import timezone + +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 .teacher import Teacher + from .user import User +else: + TypedModelMeta = object + + +class UserSession(models.Model): + """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}" + + +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: "Student" + student = models.ForeignKey( # type: ignore[assignment] + "user.Student", + related_name="student", + on_delete=models.CASCADE, + ) + + # either "release" or "join" + 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): + """ + 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: 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(TypedModelMeta): + 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: "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}" + + +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 + + invited_teacher_last_name: str + invited_teacher_last_name = models.CharField( # type: ignore[assignment] + max_length=150 + ) # Same as User model + + # 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 + + 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 504024f3..23a3552f 100644 --- a/codeforlife/user/models/otp_bypass_token.py +++ b/codeforlife/user/models/otp_bypass_token.py @@ -16,10 +16,11 @@ from ...models.fields import EncryptedTextField 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 @@ -42,7 +43,7 @@ class OtpBypassToken(EncryptedModel): # pylint: disable-next=missing-class-docstring,too-few-public-methods class Manager(EncryptedModel.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: @@ -72,13 +73,15 @@ def bulk_create(self, user: User): # type: ignore[override] objects: Manager = Manager() # type: ignore[assignment] - user = models.ForeignKey( - User, + user: "User" + user = models.ForeignKey( # type: ignore[assignment] + "user.User", related_name="otp_bypass_tokens", on_delete=models.CASCADE, ) - token = EncryptedTextField( + token: str + token = EncryptedTextField( # type: ignore[assignment] associated_data="token", verbose_name=_("token"), help_text=_("The encrypted equivalent of the token."), @@ -90,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/models/otp_bypass_token_test.py b/codeforlife/user/models/otp_bypass_token_test.py index a2d2d969..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.SECRET_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/models/school.py b/codeforlife/user/models/school.py index a934b0d4..40980fd0 100644 --- a/codeforlife/user/models/school.py +++ b/codeforlife/user/models/school.py @@ -3,12 +3,20 @@ 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] +import typing as t +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 +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( @@ -16,3 +24,80 @@ special_chars="'.", ) ] + + +class SchoolModelManager(models.Manager): + """Manager for School model.""" + + def get_original_queryset(self): + """Get the original queryset without filtering.""" + return super().get_queryset() + + def get_queryset(self): + """Filter out inactive schools by default.""" + return super().get_queryset().filter(is_active=True) + + +class School(models.Model): + """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: 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() + + def __str__(self): + return self.name + + def classes(self): + """Get all classes associated with the school.""" + 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): + """Get all admin teachers associated with the school.""" + teachers = self.teacher_school.all() + return ( + [teacher for teacher in teachers if teacher.is_admin] + if teachers + else None + ) + + 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 fd48bfea..4489fc1c 100644 --- a/codeforlife/user/models/student.py +++ b/codeforlife/user/models/student.py @@ -6,21 +6,141 @@ """ import typing as t +from uuid import uuid4 -from common.models import Student, StudentModelManager from django.db import models -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover + from datetime import datetime + 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, + 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, + ) + + # 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 + ) + + user_profile = UserProfile.objects.create(user=user) + + return Student.objects.create(user=user_profile, new_user=user) + + +class Student(models.Model): + """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: 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, + ) + # 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: 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): + if self.new_user is None: + return super().__str__() + + 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.""" - class_field: None + class_field: None # type: ignore[assignment] class Meta(TypedModelMeta): proxy = True @@ -31,4 +151,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 deleted file mode 100644 index 2bd39269..00000000 --- a/codeforlife/user/models/teacher.py +++ /dev/null @@ -1,207 +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 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 - -if t.TYPE_CHECKING: - from django_stubs_ext.db.models import TypedModelMeta -else: - TypedModelMeta = object - -AnyTeacher = t.TypeVar("AnyTeacher", bound=Teacher) - - -class SchoolTeacher(Teacher): - """A teacher that is in a school.""" - - school: School - - 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) - - objects: models.Manager["SchoolTeacher"] = Manager() - - @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.""" - 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.""" - 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] - - 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) - - objects: models.Manager["AdminSchoolTeacher"] = Manager() - - @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] - - 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) - - objects: models.Manager["NonAdminSchoolTeacher"] = Manager() - - -class NonSchoolTeacher(Teacher): - """A teacher that is not in a school.""" - - school: None - is_admin: t.Literal[False] - - 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) - - objects: models.Manager["NonSchoolTeacher"] = Manager() - - -# 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 95f252c8..08d19da7 100644 --- a/codeforlife/user/models/user/google.py +++ b/codeforlife/user/models/user/google.py @@ -7,21 +7,21 @@ 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 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 @@ -36,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}, @@ -79,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, @@ -103,7 +107,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 fc21e896..c61a2854 100644 --- a/codeforlife/user/models/user/independent.py +++ b/codeforlife/user/models/user/independent.py @@ -7,26 +7,25 @@ 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 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) @@ -50,11 +49,16 @@ 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 + 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, @@ -63,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/student.py b/codeforlife/user/models/user/student.py index 2b2bd29b..aa94c273 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() @@ -115,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/teacher.py b/codeforlife/user/models/user/teacher.py index a2bb45d0..8b6f4c40 100644 --- a/codeforlife/user/models/user/teacher.py +++ b/codeforlife/user/models/user/teacher.py @@ -7,41 +7,44 @@ 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 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, ): """Create a teacher-user.""" - # pylint: disable-next=import-outside-toplevel + # pylint: disable=import-outside-toplevel + from ..other import TotalActivity from ..teacher import Teacher + from .user import UserProfile + + # pylint: enable=import-outside-toplevel assert "username" not in extra_fields @@ -70,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 b7d4c1f5..6d938423 100644 --- a/codeforlife/user/models/user/user.py +++ b/codeforlife/user/models/user/user.py @@ -6,14 +6,17 @@ """ import typing as t -from datetime import datetime +from datetime import datetime, timedelta -from common.models import UserProfile +from django.conf import settings # 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 django.utils.translation import gettext_lazy as _ from pyotp import TOTP from ....models import AbstractBaseUser @@ -57,7 +60,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,20 +73,36 @@ 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 + # 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 ( - 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 @@ -117,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): @@ -184,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) @@ -203,3 +237,33 @@ 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): + """A user's profile.""" + + 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) + + # 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): + 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/user/serializers/user_test.py b/codeforlife/user/serializers/user_test.py index 72b94e86..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 @@ -25,7 +26,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 +48,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. @@ -56,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/signals/auth_factor_test.py b/codeforlife/user/signals/auth_factor_test.py index efc763ee..033e65ca 100644 --- a/codeforlife/user/signals/auth_factor_test.py +++ b/codeforlife/user/signals/auth_factor_test.py @@ -3,26 +3,29 @@ 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 diff --git a/codeforlife/user/views/klass_test.py b/codeforlife/user/views/klass_test.py index f9275a70..b7383cdb 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), ) @@ -68,8 +68,9 @@ def test_retrieve(self): user = StudentUser.objects.first() assert user - self.client.login_as(user, password="Password1") - self.client.retrieve(model=user.student.class_field) + self.client.login_as(user) + # 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..d9f50bb1 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), ) @@ -89,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 4643579e..c36d833e 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, @@ -93,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) @@ -116,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) @@ -134,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): @@ -170,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"}, ) @@ -182,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"}, ) @@ -194,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"}, ) @@ -206,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={ @@ -231,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) @@ -245,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) 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 diff --git a/settings.py b/settings.py index fe271014..f74c773a 100644 --- a/settings.py +++ b/settings.py @@ -12,11 +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. -SECRET_KEY = "XTgWqMlZCMI_E5BvCArkif9nrJIIhe_6Ic6Q_UcWJDk=" -# TODO: remove this when cfl-common is not longer installed -ENCRYPTION_KEY = SECRET_KEY - INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", @@ -26,9 +21,6 @@ "django.contrib.staticfiles", "django.contrib.sites", "codeforlife.user", - "game", # TODO: remove this. - "common", # TODO: remove this. - "portal", # TODO: remove this. ] MIDDLEWARE = [ diff --git a/setup.py b/setup.py index 39a06af2..ba30c913 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__ @@ -65,12 +66,18 @@ 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']}" + if "subdirectory" in package: + requirement += f"#subdirectory={package['subdirectory']}" + 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