From 9fd9129b2b96b1f64363ca6dfd9a9c796be176a7 Mon Sep 17 00:00:00 2001 From: Stefan Kairinos Date: Tue, 23 Jan 2024 13:17:40 +0000 Subject: [PATCH] fix: model view set test case (#60) * add .venv * fix: add code checking and type hints * data access layer * quick save * tidy up * add partial_update and destroy * base model serializer * previous_values_are_unequal * feedback * feedback * minor fixes --- .vscode/launch.json | 17 + .vscode/settings.json | 30 + Pipfile | 9 +- Pipfile.lock | 862 ++++++++++++++------ codeforlife/models/__init__.py | 23 +- codeforlife/models/base.py | 27 + codeforlife/models/signals/pre_save.py | 48 +- codeforlife/serializers/__init__.py | 6 + codeforlife/serializers/base.py | 21 + codeforlife/tests/__init__.py | 11 +- codeforlife/tests/api.py | 281 ------- codeforlife/tests/cron.py | 17 +- codeforlife/tests/model_view_set.py | 587 +++++++++++++ codeforlife/user/serializers/klass.py | 32 +- codeforlife/user/serializers/school.py | 26 +- codeforlife/user/serializers/student.py | 23 +- codeforlife/user/serializers/teacher.py | 16 +- codeforlife/user/serializers/user.py | 67 +- codeforlife/user/tests/views/test_klass.py | 29 +- codeforlife/user/tests/views/test_school.py | 76 +- codeforlife/user/tests/views/test_user.py | 140 ++-- codeforlife/user/urls.py | 7 +- codeforlife/user/views/user.py | 7 +- 23 files changed, 1593 insertions(+), 769 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 codeforlife/models/base.py create mode 100644 codeforlife/serializers/__init__.py create mode 100644 codeforlife/serializers/base.py delete mode 100644 codeforlife/tests/api.py create mode 100644 codeforlife/tests/model_view_set.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..af15cbc6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Pytest", + "type": "python", + "request": "test", + "justMyCode": false, + "presentation": { + "hidden": true + } + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 962594af..73dc8602 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,39 @@ { + "isort.path": [ + ".venv/bin/python", + "-m", + "isort" + ], + "isort.args": [ + "--settings-file=pyproject.toml" + ], + "black-formatter.path": [ + ".venv/bin/python", + "-m", + "black" + ], "black-formatter.args": [ "--config", "pyproject.toml" ], + "mypy-type-checker.path": [ + ".venv/bin/python", + "-m", + "mypy" + ], + "mypy-type-checker.args": [ + "--config-file=pyproject.toml" + ], + "pylint.path": [ + ".venv/bin/python", + "-m", + "pylint" + ], + "pylint.args": [ + "--rcfile=pyproject.toml" + ], "python.testing.pytestArgs": [ + "-n=auto", "-c=pyproject.toml", "." ], diff --git a/Pipfile b/Pipfile index f29865cd..41c323b1 100644 --- a/Pipfile +++ b/Pipfile @@ -26,11 +26,18 @@ phonenumbers = "==8.12.12" # TODO: remove [dev-packages] black = "==23.1.0" pytest = "==7.2.1" +pytest-env = "==0.8.1" +pytest-xdist = {version = "==3.5.0", extras = ["psutil"]} pytest-django = "==4.5.2" django-extensions = "==3.2.1" pyparsing = "==3.0.9" pydot = "==1.4.2" -pytest-env = "==0.8.1" +pylint = "==3.0.2" +pylint-django = "==2.5.5" +isort = "==5.13.2" +mypy = "==1.6.1" +django-stubs = {version = "==4.2.6", extras = ["compatible-mypy"]} +djangorestframework-stubs = {version = "==3.14.4", extras = ["compatible-mypy"]} [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 830b2c3a..b61a49d7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4e77ad0e846637e2f1e9ce14086a2f878f3efb1df90745b14d7aa156b4a02bd9" + "sha256": "cebe0c74bd469338901f1ab8b0870fd43e10e67f5db29ac344272dc62625b8a6" }, "pipfile-spec": 6, "requires": { @@ -34,27 +34,27 @@ }, "attrs": { "hashes": [ - "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", - "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" ], "markers": "python_version >= '3.7'", - "version": "==23.1.0" + "version": "==23.2.0" }, "cachetools": { "hashes": [ - "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590", - "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b" + "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2", + "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1" ], "markers": "python_version >= '3.7'", - "version": "==5.3.1" + "version": "==5.3.2" }, "certifi": { "hashes": [ - "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", - "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" ], "markers": "python_version >= '3.6'", - "version": "==2023.7.22" + "version": "==2023.11.17" }, "cfl-common": { "hashes": [ @@ -66,99 +66,99 @@ }, "charset-normalizer": { "hashes": [ - "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843", - "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786", - "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e", - "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8", - "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4", - "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa", - "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d", - "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82", - "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7", - "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895", - "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d", - "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a", - "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382", - "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678", - "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b", - "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e", - "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741", - "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4", - "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596", - "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9", - "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69", - "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c", - "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77", - "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13", - "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459", - "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e", - "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7", - "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908", - "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a", - "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f", - "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8", - "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482", - "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d", - "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d", - "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545", - "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34", - "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86", - "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6", - "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe", - "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e", - "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc", - "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7", - "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd", - "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c", - "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557", - "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a", - "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89", - "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078", - "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e", - "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4", - "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403", - "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0", - "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89", - "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115", - "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9", - "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05", - "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a", - "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec", - "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56", - "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38", - "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479", - "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c", - "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e", - "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd", - "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186", - "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455", - "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c", - "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65", - "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78", - "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287", - "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df", - "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43", - "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1", - "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7", - "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989", - "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a", - "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63", - "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884", - "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649", - "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810", - "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828", - "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4", - "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2", - "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd", - "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5", - "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe", - "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293", - "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e", - "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e", - "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8" + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.0" + "version": "==3.3.2" }, "click": { "hashes": [ @@ -248,11 +248,11 @@ }, "django-import-export": { "hashes": [ - "sha256:88ecaf06be06bd95d97cf34f3c911c56c012a7a81712a8956740e5bfc2465162", - "sha256:d02e31908c965d512cc6f7ef6e72935177647b15d3846050d0f094177fca0d86" + "sha256:12edb7ba1f7f9b392d0257a2ae68086020e53ab2c7bd2b21a56ec17fbb83826b", + "sha256:b1b7385627ed61063cd9764e8c19cce3ce8945626f7953262df8162b0feec376" ], "markers": "python_version >= '3.8'", - "version": "==3.3.1" + "version": "==3.3.6" }, "django-js-reverse": { "hashes": [ @@ -366,79 +366,75 @@ }, "google-auth": { "hashes": [ - "sha256:6864247895eea5d13b9c57c9e03abb49cb94ce2dc7c58e91cba3248c7477c9e3", - "sha256:a8f4608e65c244ead9e0538f181a96c6e11199ec114d41f1d7b1bffa96937bda" + "sha256:3f445c8ce9b61ed6459aad86d8ccdba4a9afed841b2d1451a11ef4db08957424", + "sha256:97327dbbf58cccb58fc5a1712bba403ae76668e64814eb30f7316f7e27126b81" ], "markers": "python_version >= '3.7'", - "version": "==2.23.3" + "version": "==2.26.2" }, "greenlet": { "hashes": [ - "sha256:02a807b2a58d5cdebb07050efe3d7deaf915468d112dfcf5e426d0564aa3aa4a", - "sha256:0b72b802496cccbd9b31acea72b6f87e7771ccfd7f7927437d592e5c92ed703c", - "sha256:0d3f83ffb18dc57243e0151331e3c383b05e5b6c5029ac29f754745c800f8ed9", - "sha256:10b5582744abd9858947d163843d323d0b67be9432db50f8bf83031032bc218d", - "sha256:123910c58234a8d40eaab595bc56a5ae49bdd90122dde5bdc012c20595a94c14", - "sha256:1482fba7fbed96ea7842b5a7fc11d61727e8be75a077e603e8ab49d24e234383", - "sha256:19834e3f91f485442adc1ee440171ec5d9a4840a1f7bd5ed97833544719ce10b", - "sha256:1d363666acc21d2c204dd8705c0e0457d7b2ee7a76cb16ffc099d6799744ac99", - "sha256:211ef8d174601b80e01436f4e6905aca341b15a566f35a10dd8d1e93f5dbb3b7", - "sha256:269d06fa0f9624455ce08ae0179430eea61085e3cf6457f05982b37fd2cefe17", - "sha256:2e7dcdfad252f2ca83c685b0fa9fba00e4d8f243b73839229d56ee3d9d219314", - "sha256:334ef6ed8337bd0b58bb0ae4f7f2dcc84c9f116e474bb4ec250a8bb9bd797a66", - "sha256:343675e0da2f3c69d3fb1e894ba0a1acf58f481f3b9372ce1eb465ef93cf6fed", - "sha256:37f60b3a42d8b5499be910d1267b24355c495064f271cfe74bf28b17b099133c", - "sha256:38ad562a104cd41e9d4644f46ea37167b93190c6d5e4048fcc4b80d34ecb278f", - "sha256:3c0d36f5adc6e6100aedbc976d7428a9f7194ea79911aa4bf471f44ee13a9464", - "sha256:3fd2b18432e7298fcbec3d39e1a0aa91ae9ea1c93356ec089421fabc3651572b", - "sha256:4a1a6244ff96343e9994e37e5b4839f09a0207d35ef6134dce5c20d260d0302c", - "sha256:4cd83fb8d8e17633ad534d9ac93719ef8937568d730ef07ac3a98cb520fd93e4", - "sha256:527cd90ba3d8d7ae7dceb06fda619895768a46a1b4e423bdb24c1969823b8362", - "sha256:56867a3b3cf26dc8a0beecdb4459c59f4c47cdd5424618c08515f682e1d46692", - "sha256:621fcb346141ae08cb95424ebfc5b014361621b8132c48e538e34c3c93ac7365", - "sha256:63acdc34c9cde42a6534518e32ce55c30f932b473c62c235a466469a710bfbf9", - "sha256:6512592cc49b2c6d9b19fbaa0312124cd4c4c8a90d28473f86f92685cc5fef8e", - "sha256:6672fdde0fd1a60b44fb1751a7779c6db487e42b0cc65e7caa6aa686874e79fb", - "sha256:6a5b2d4cdaf1c71057ff823a19d850ed5c6c2d3686cb71f73ae4d6382aaa7a06", - "sha256:6a68d670c8f89ff65c82b936275369e532772eebc027c3be68c6b87ad05ca695", - "sha256:6bb36985f606a7c49916eff74ab99399cdfd09241c375d5a820bb855dfb4af9f", - "sha256:73b2f1922a39d5d59cc0e597987300df3396b148a9bd10b76a058a2f2772fc04", - "sha256:7709fd7bb02b31908dc8fd35bfd0a29fc24681d5cc9ac1d64ad07f8d2b7db62f", - "sha256:8060b32d8586e912a7b7dac2d15b28dbbd63a174ab32f5bc6d107a1c4143f40b", - "sha256:80dcd3c938cbcac986c5c92779db8e8ce51a89a849c135172c88ecbdc8c056b7", - "sha256:813720bd57e193391dfe26f4871186cf460848b83df7e23e6bef698a7624b4c9", - "sha256:831d6f35037cf18ca5e80a737a27d822d87cd922521d18ed3dbc8a6967be50ce", - "sha256:871b0a8835f9e9d461b7fdaa1b57e3492dd45398e87324c047469ce2fc9f516c", - "sha256:952256c2bc5b4ee8df8dfc54fc4de330970bf5d79253c863fb5e6761f00dda35", - "sha256:96d9ea57292f636ec851a9bb961a5cc0f9976900e16e5d5647f19aa36ba6366b", - "sha256:9a812224a5fb17a538207e8cf8e86f517df2080c8ee0f8c1ed2bdaccd18f38f4", - "sha256:9adbd8ecf097e34ada8efde9b6fec4dd2a903b1e98037adf72d12993a1c80b51", - "sha256:9de687479faec7db5b198cc365bc34addd256b0028956501f4d4d5e9ca2e240a", - "sha256:a048293392d4e058298710a54dfaefcefdf49d287cd33fb1f7d63d55426e4355", - "sha256:aa15a2ec737cb609ed48902b45c5e4ff6044feb5dcdfcf6fa8482379190330d7", - "sha256:abe1ef3d780de56defd0c77c5ba95e152f4e4c4e12d7e11dd8447d338b85a625", - "sha256:ad6fb737e46b8bd63156b8f59ba6cdef46fe2b7db0c5804388a2d0519b8ddb99", - "sha256:b1660a15a446206c8545edc292ab5c48b91ff732f91b3d3b30d9a915d5ec4779", - "sha256:b505fcfc26f4148551826a96f7317e02c400665fa0883fe505d4fcaab1dabfdd", - "sha256:b822fab253ac0f330ee807e7485769e3ac85d5eef827ca224feaaefa462dc0d0", - "sha256:bdd696947cd695924aecb3870660b7545a19851f93b9d327ef8236bfc49be705", - "sha256:bdfaeecf8cc705d35d8e6de324bf58427d7eafb55f67050d8f28053a3d57118c", - "sha256:be557119bf467d37a8099d91fbf11b2de5eb1fd5fc5b91598407574848dc910f", - "sha256:c6b5ce7f40f0e2f8b88c28e6691ca6806814157ff05e794cdd161be928550f4c", - "sha256:c94e4e924d09b5a3e37b853fe5924a95eac058cb6f6fb437ebb588b7eda79870", - "sha256:cc3e2679ea13b4de79bdc44b25a0c4fcd5e94e21b8f290791744ac42d34a0353", - "sha256:d1e22c22f7826096ad503e9bb681b05b8c1f5a8138469b255eb91f26a76634f2", - "sha256:d5539f6da3418c3dc002739cb2bb8d169056aa66e0c83f6bacae0cd3ac26b423", - "sha256:d55db1db455c59b46f794346efce896e754b8942817f46a1bada2d29446e305a", - "sha256:e09dea87cc91aea5500262993cbd484b41edf8af74f976719dd83fe724644cd6", - "sha256:e52a712c38e5fb4fd68e00dc3caf00b60cb65634d50e32281a9d6431b33b4af1", - "sha256:e693e759e172fa1c2c90d35dea4acbdd1d609b6936115d3739148d5e4cd11947", - "sha256:ecf94aa539e97a8411b5ea52fc6ccd8371be9550c4041011a091eb8b3ca1d810", - "sha256:f351479a6914fd81a55c8e68963609f792d9b067fb8a60a042c585a621e0de4f", - "sha256:f47932c434a3c8d3c86d865443fadc1fbf574e9b11d6650b656e602b1797908a" + "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67", + "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6", + "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257", + "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4", + "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676", + "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61", + "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc", + "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca", + "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7", + "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728", + "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305", + "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6", + "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379", + "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414", + "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04", + "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a", + "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf", + "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491", + "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559", + "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e", + "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274", + "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb", + "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b", + "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9", + "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b", + "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be", + "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506", + "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405", + "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113", + "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f", + "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5", + "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230", + "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d", + "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f", + "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a", + "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e", + "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61", + "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6", + "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d", + "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71", + "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22", + "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2", + "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3", + "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067", + "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc", + "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881", + "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3", + "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e", + "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac", + "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53", + "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0", + "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b", + "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83", + "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41", + "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c", + "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf", + "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", + "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], "markers": "python_version >= '3.7'", - "version": "==3.0.0" + "version": "==3.0.3" }, "hypothesis": { "hashes": [ @@ -450,11 +446,11 @@ }, "idna": { "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" ], "markers": "python_version >= '3.5'", - "version": "==3.4" + "version": "==3.6" }, "importlib-metadata": { "hashes": [ @@ -474,11 +470,11 @@ }, "jinja2": { "hashes": [ - "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", - "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", + "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" ], "markers": "python_version >= '3.7'", - "version": "==3.1.2" + "version": "==3.1.3" }, "kubernetes": { "hashes": [ @@ -492,6 +488,7 @@ "hashes": [ "sha256:081e256ab3c5f3f09c7b8dea3bf3bf5e64a97c6995fd9eea880639b3f93a9f9a", "sha256:3ab5ad18e47db560f4f0c09e3d28cf3bb1a44711257488ac2adad69f4f7f8425", + "sha256:5fb2297a4754a6c8e25cfe5c015a3b51a2b6b9021b333f989bb8ce9d60eb5828", "sha256:65455a2728b696b62100eb5932604aa13a29f4ac9a305d95773c14aaa7200aaf", "sha256:89c5ce497fcf3aba1dd1b19aae93b99f68257e5f2026b731b00a872f13324c7f", "sha256:f1efc1b612299c88aec9e39d6ca0c266d360daa5b19d9430bdeaffffa86993f9" @@ -684,71 +681,85 @@ }, "pillow": { "hashes": [ - "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff", - "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f", - "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21", - "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635", - "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a", - "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f", - "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1", - "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d", - "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db", - "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849", - "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7", - "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876", - "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3", - "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317", - "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91", - "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d", - "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b", - "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd", - "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed", - "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500", - "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7", - "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a", - "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a", - "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0", - "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf", - "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f", - "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1", - "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088", - "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971", - "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a", - "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205", - "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54", - "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08", - "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21", - "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d", - "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08", - "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e", - "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf", - "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b", - "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145", - "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2", - "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d", - "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d", - "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf", - "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad", - "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d", - "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1", - "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4", - "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2", - "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19", - "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37", - "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4", - "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68", - "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1" + "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8", + "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39", + "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac", + "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869", + "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e", + "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04", + "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9", + "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e", + "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe", + "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef", + "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56", + "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa", + "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f", + "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f", + "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e", + "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a", + "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2", + "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2", + "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5", + "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a", + "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2", + "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213", + "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563", + "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591", + "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c", + "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2", + "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb", + "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757", + "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0", + "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452", + "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad", + "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01", + "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f", + "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5", + "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61", + "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e", + "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b", + "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068", + "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9", + "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588", + "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483", + "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f", + "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67", + "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7", + "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311", + "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6", + "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72", + "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6", + "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129", + "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13", + "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67", + "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c", + "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516", + "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e", + "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e", + "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364", + "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023", + "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1", + "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04", + "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d", + "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a", + "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7", + "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb", + "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4", + "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e", + "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1", + "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48", + "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868" ], "markers": "python_version >= '3.8'", - "version": "==10.0.1" + "version": "==10.2.0" }, "pyasn1": { "hashes": [ - "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57", - "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde" + "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58", + "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==0.5.0" + "version": "==0.5.1" }, "pyasn1-modules": { "hashes": [ @@ -1020,43 +1031,43 @@ }, "typing-extensions": { "hashes": [ - "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", - "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" ], "markers": "python_version >= '3.8'", - "version": "==4.8.0" + "version": "==4.9.0" }, "tzdata": { "hashes": [ - "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a", - "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda" + "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3", + "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9" ], "markers": "python_version >= '2'", - "version": "==2023.3" + "version": "==2023.4" }, "urllib3": { "hashes": [ - "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2", - "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564" + "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", + "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" ], - "markers": "python_version >= '3.7'", - "version": "==2.0.6" + "markers": "python_version >= '3.8'", + "version": "==2.1.0" }, "websocket-client": { "hashes": [ - "sha256:084072e0a7f5f347ef2ac3d8698a5e0b4ffbfcab607628cadabc650fc9a83a24", - "sha256:b3324019b3c28572086c4a319f91d1dcd44e6e11cd340232978c684a7650d0df" + "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6", + "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588" ], "markers": "python_version >= '3.8'", - "version": "==1.6.4" + "version": "==1.7.0" }, "werkzeug": { "hashes": [ - "sha256:3ffff4dcc32db52ef3cc94dff3000a3c2846890f3a5a51800a27b909c5e770f0", - "sha256:cbb2600f7eabe51dbc0502f58be0b3e1b96b893b05695ea2b35b43d4de2d9962" + "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc", + "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10" ], "markers": "python_version >= '3.8'", - "version": "==3.0.0" + "version": "==3.0.1" }, "xlrd": { "hashes": [ @@ -1090,13 +1101,21 @@ "markers": "python_version >= '3.7'", "version": "==3.7.2" }, + "astroid": { + "hashes": [ + "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91", + "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.2" + }, "attrs": { "hashes": [ - "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", - "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" ], "markers": "python_version >= '3.7'", - "version": "==23.1.0" + "version": "==23.2.0" }, "black": { "hashes": [ @@ -1129,6 +1148,110 @@ "index": "pypi", "version": "==23.1.0" }, + "certifi": { + "hashes": [ + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.11.17" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, "click": { "hashes": [ "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", @@ -1137,6 +1260,14 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, + "dill": { + "hashes": [ + "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", + "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + ], + "markers": "python_version < '3.11'", + "version": "==0.3.7" + }, "django": { "hashes": [ "sha256:a477ab326ae7d8807dc25c186b951ab8c7648a3a23f9497763c37307a2b5ef87", @@ -1153,13 +1284,59 @@ "index": "pypi", "version": "==3.2.1" }, + "django-stubs": { + "extras": [ + "compatible-mypy" + ], + "hashes": [ + "sha256:2fcd257884a68dfa02de41ee5410ec805264d9b07d9b5b119e4dea82c7b8345e", + "sha256:e60b43de662a199db4b15c803c06669e0ac5035614af291cbd3b91591f7dcc94" + ], + "index": "pypi", + "version": "==4.2.6" + }, + "django-stubs-ext": { + "hashes": [ + "sha256:45a5d102417a412e3606e3c358adb4744988a92b7b58ccf3fd64bddd5d04d14c", + "sha256:519342ac0849cda1559746c9a563f03ff99f636b0ebe7c14b75e816a00dfddc3" + ], + "markers": "python_version >= '3.8'", + "version": "==4.2.7" + }, + "djangorestframework-stubs": { + "extras": [ + "compatible-mypy" + ], + "hashes": [ + "sha256:5be8275dd05d6629b3d1688929586ef7b6bc66b4f3f728b5e0389305f07c7a7f", + "sha256:8ee8719bfeb647b92cc200e15b3cc9813d2e4468c8190777a55a121542a4b2d4" + ], + "index": "pypi", + "version": "==3.14.4" + }, "exceptiongroup": { "hashes": [ - "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", - "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" + "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", + "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" ], "markers": "python_version < '3.11'", - "version": "==1.1.3" + "version": "==1.2.0" + }, + "execnet": { + "hashes": [ + "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41", + "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.2" + }, + "idna": { + "hashes": [ + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + ], + "markers": "python_version >= '3.5'", + "version": "==3.6" }, "iniconfig": { "hashes": [ @@ -1169,6 +1346,55 @@ "markers": "python_version >= '3.7'", "version": "==2.0.0" }, + "isort": { + "hashes": [ + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" + ], + "index": "pypi", + "version": "==5.13.2" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy": { + "hashes": [ + "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7", + "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e", + "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c", + "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169", + "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208", + "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0", + "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1", + "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1", + "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7", + "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45", + "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143", + "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5", + "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f", + "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd", + "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245", + "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f", + "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332", + "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30", + "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183", + "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f", + "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85", + "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46", + "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71", + "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660", + "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb", + "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c", + "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a" + ], + "index": "pypi", + "version": "==1.6.1" + }, "mypy-extensions": { "hashes": [ "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", @@ -1187,19 +1413,19 @@ }, "pathspec": { "hashes": [ - "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", - "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" ], - "markers": "python_version >= '3.7'", - "version": "==0.11.2" + "markers": "python_version >= '3.8'", + "version": "==0.12.1" }, "platformdirs": { "hashes": [ - "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", - "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e" + "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", + "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" ], - "markers": "python_version >= '3.7'", - "version": "==3.11.0" + "markers": "python_version >= '3.8'", + "version": "==4.1.0" }, "pluggy": { "hashes": [ @@ -1209,6 +1435,27 @@ "markers": "python_version >= '3.8'", "version": "==1.3.0" }, + "psutil": { + "hashes": [ + "sha256:032f4f2c909818c86cea4fe2cc407f1c0f0cde8e6c6d702b28b8ce0c0d143340", + "sha256:0bd41bf2d1463dfa535942b2a8f0e958acf6607ac0be52265ab31f7923bcd5e6", + "sha256:1132704b876e58d277168cd729d64750633d5ff0183acf5b3c986b8466cd0284", + "sha256:1d4bc4a0148fdd7fd8f38e0498639ae128e64538faa507df25a20f8f7fb2341c", + "sha256:3c4747a3e2ead1589e647e64aad601981f01b68f9398ddf94d01e3dc0d1e57c7", + "sha256:3f02134e82cfb5d089fddf20bb2e03fd5cd52395321d1c8458a9e58500ff417c", + "sha256:44969859757f4d8f2a9bd5b76eba8c3099a2c8cf3992ff62144061e39ba8568e", + "sha256:4c03362e280d06bbbfcd52f29acd79c733e0af33d707c54255d21029b8b32ba6", + "sha256:5794944462509e49d4d458f4dbfb92c47539e7d8d15c796f141f474010084056", + "sha256:b27f8fdb190c8c03914f908a4555159327d7481dac2f01008d483137ef3311a9", + "sha256:c727ca5a9b2dd5193b8644b9f0c883d54f1248310023b5ad3e92036c5e2ada68", + "sha256:e469990e28f1ad738f65a42dcfc17adaed9d0f325d55047593cb9033a0ab63df", + "sha256:ea36cc62e69a13ec52b2f625c27527f6e4479bca2b340b7a452af55b34fcbe2e", + "sha256:f37f87e4d73b79e6c5e749440c3113b81d1ee7d26f21c19c47371ddea834f414", + "sha256:fe361f743cb3389b8efda21980d93eb55c1f1e3898269bc9a2a1d0bb7b1f6508", + "sha256:fe8b7f07948f1304497ce4f4684881250cd859b16d06a1dc4d7941eeb6233bfe" + ], + "version": "==5.9.7" + }, "pydot": { "hashes": [ "sha256:248081a39bcb56784deb018977e428605c1c758f10897a339fce1dd728ff007d", @@ -1217,6 +1464,30 @@ "index": "pypi", "version": "==1.4.2" }, + "pylint": { + "hashes": [ + "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496", + "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda" + ], + "index": "pypi", + "version": "==3.0.2" + }, + "pylint-django": { + "hashes": [ + "sha256:2f339e4bf55776958283395c5139c37700c91bd5ef1d8251ef6ac88b5abbba9b", + "sha256:5abd5c2228e0e5e2a4cb6d0b4fc1d1cef1e773d0be911412f4dd4fc1a1a440b7" + ], + "index": "pypi", + "version": "==2.5.5" + }, + "pylint-plugin-utils": { + "hashes": [ + "sha256:ae11664737aa2effbf26f973a9e0b6779ab7106ec0adc5fe104b0907ca04e507", + "sha256:d3cebf68a38ba3fba23a873809155562571386d4c1b03e5b4c4cc26c3eee93e4" + ], + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==0.8.2" + }, "pyparsing": { "hashes": [ "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", @@ -1249,6 +1520,17 @@ "index": "pypi", "version": "==0.8.1" }, + "pytest-xdist": { + "extras": [ + "psutil" + ], + "hashes": [ + "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a", + "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24" + ], + "index": "pypi", + "version": "==3.5.0" + }, "pytz": { "hashes": [ "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b", @@ -1256,6 +1538,14 @@ ], "version": "==2023.3.post1" }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "markers": "python_version >= '3.7'", + "version": "==2.31.0" + }, "sqlparse": { "hashes": [ "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3", @@ -1272,13 +1562,51 @@ "markers": "python_version < '3.11'", "version": "==2.0.1" }, + "tomlkit": { + "hashes": [ + "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", + "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.12.3" + }, + "types-pytz": { + "hashes": [ + "sha256:1999a123a3dc0e39a2ef6d19f3f8584211de9e6a77fe7a0259f04a524e90a5cf", + "sha256:cc23d0192cd49c8f6bba44ee0c81e4586a8f30204970fc0894d209a6b08dab9a" + ], + "version": "==2023.3.1.1" + }, + "types-pyyaml": { + "hashes": [ + "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062", + "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24" + ], + "version": "==6.0.12.12" + }, + "types-requests": { + "hashes": [ + "sha256:0e1c731c17f33618ec58e022b614a1a2ecc25f7dc86800b36ef341380402c612", + "sha256:da997b3b6a72cc08d09f4dba9802fdbabc89104b35fe24ee588e674037689354" + ], + "markers": "python_version >= '3.8'", + "version": "==2.31.0.20240106" + }, "typing-extensions": { "hashes": [ - "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", - "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + ], + "markers": "python_version >= '3.8'", + "version": "==4.9.0" + }, + "urllib3": { + "hashes": [ + "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", + "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" ], "markers": "python_version >= '3.8'", - "version": "==4.8.0" + "version": "==2.1.0" } } } diff --git a/codeforlife/models/__init__.py b/codeforlife/models/__init__.py index 9322030d..4335ad17 100644 --- a/codeforlife/models/__init__.py +++ b/codeforlife/models/__init__.py @@ -1,21 +1,6 @@ -"""Helpers for module "django.db.models". -https://docs.djangoproject.com/en/3.2/ref/models/ +""" +© Ocado Group +Created on 19/01/2024 at 15:20:45(+00:00). """ -import typing as t - -from django.db.models import Model as _Model - - -class Model(_Model): - """A base class for all Django models. - - Args: - _Model (django.db.models.Model): Django's model class. - """ - - id: int - pk: int - - -AnyModel = t.TypeVar("AnyModel", bound=Model) +from .base import * diff --git a/codeforlife/models/base.py b/codeforlife/models/base.py new file mode 100644 index 00000000..2e19fde1 --- /dev/null +++ b/codeforlife/models/base.py @@ -0,0 +1,27 @@ +""" +© Ocado Group +Created on 19/01/2024 at 15:18:48(+00:00). + +Base model for all Django models. +""" + +import typing as t + +from django.db.models import Model as _Model +from django_stubs_ext.db.models import TypedModelMeta + +Id = t.TypeVar("Id") + + +class Model(_Model, t.Generic[Id]): + """A base class for all Django models.""" + + id: Id + pk: Id + + # pylint: disable-next=missing-class-docstring,too-few-public-methods + class Meta(TypedModelMeta): + abstract = True + + +AnyModel = t.TypeVar("AnyModel", bound=Model) diff --git a/codeforlife/models/signals/pre_save.py b/codeforlife/models/signals/pre_save.py index 75d43a9d..306fea05 100644 --- a/codeforlife/models/signals/pre_save.py +++ b/codeforlife/models/signals/pre_save.py @@ -4,9 +4,12 @@ import typing as t -from .. import AnyModel +from django.db.models import Model + from . import UpdateFields, _has_update_fields +AnyModel = t.TypeVar("AnyModel", bound=Model) + def was_created(instance: AnyModel): """Check if the instance was created. @@ -37,30 +40,55 @@ def has_update_fields(actual: UpdateFields, expected: UpdateFields): return _has_update_fields(actual, expected) -def has_previous_values( +def check_previous_values( instance: AnyModel, predicates: t.Dict[str, t.Callable[[t.Any, t.Any], bool]], ): - """Check if the previous values are as expected. + """Check if the previous values are as expected. If the model has not been + created yet, the previous values are None. Args: instance: The current instance. predicates: A predicate for each field. It accepts the arguments (previous_value, value) and returns True if the values are as expected. - Raises: - ValueError: If arg 'instance' has not been created yet. - Returns: If all the previous values are as expected. """ - if not was_created(instance): - raise ValueError("Arg 'instance' has not been created yet.") + if was_created(instance): + previous_instance = instance.__class__.objects.get(pk=instance.pk) + + def get_previous_value(field: str): + return getattr(previous_instance, field) - previous_instance = instance.__class__.objects.get(pk=instance.pk) + else: + # pylint: disable-next=unused-argument + def get_previous_value(field: str): + return None return all( - predicate(previous_instance[field], instance[field]) + predicate(get_previous_value(field), getattr(instance, field)) for field, predicate in predicates.items() ) + + +def previous_values_are_unequal(instance: AnyModel, fields: t.Set[str]): + """Check if all the previous values are not equal to the current values. If + the model has not been created yet, the previous values are None. + + Args: + instance: The current instance. + fields: The fields that should not be equal. + + Returns: + If all the previous values are not equal to the current values. + """ + + def predicate(v1, v2): + return v1 != v2 + + return check_previous_values( + instance, + {field: predicate for field in fields}, + ) diff --git a/codeforlife/serializers/__init__.py b/codeforlife/serializers/__init__.py new file mode 100644 index 00000000..4a685c70 --- /dev/null +++ b/codeforlife/serializers/__init__.py @@ -0,0 +1,6 @@ +""" +© Ocado Group +Created on 20/01/2024 at 11:19:12(+00:00). +""" + +from .base import * diff --git a/codeforlife/serializers/base.py b/codeforlife/serializers/base.py new file mode 100644 index 00000000..8f3ea630 --- /dev/null +++ b/codeforlife/serializers/base.py @@ -0,0 +1,21 @@ +""" +© Ocado Group +Created on 20/01/2024 at 11:19:24(+00:00). + +Base model serializers. +""" + +import typing as t + +from django.db.models import Model +from rest_framework.serializers import ModelSerializer as _ModelSerializer + +AnyModel = t.TypeVar("AnyModel", bound=Model) + + +class ModelSerializer(_ModelSerializer[AnyModel], t.Generic[AnyModel]): + """Base model serializer for all model serializers.""" + + # pylint: disable-next=useless-parent-delegation + def update(self, instance, validated_data: t.Dict[str, t.Any]): + return super().update(instance, validated_data) diff --git a/codeforlife/tests/__init__.py b/codeforlife/tests/__init__.py index 80ebc8c4..2584eb67 100644 --- a/codeforlife/tests/__init__.py +++ b/codeforlife/tests/__init__.py @@ -1,2 +1,9 @@ -from .api import APITestCase, APIClient -from .cron import CronTestCase, CronTestClient +""" +© Ocado Group +Created on 19/01/2024 at 17:17:23(+00:00). + +Custom test cases. +""" + +from .cron import CronTestCase +from .model_view_set import ModelViewSetTestCase diff --git a/codeforlife/tests/api.py b/codeforlife/tests/api.py deleted file mode 100644 index b68eb10a..00000000 --- a/codeforlife/tests/api.py +++ /dev/null @@ -1,281 +0,0 @@ -import typing as t -from unittest.mock import patch - -from django.db.models import Model -from django.db.models.query import QuerySet -from django.urls import reverse -from django.utils import timezone -from django.utils.http import urlencode -from pyotp import TOTP -from rest_framework.response import Response -from rest_framework.serializers import ModelSerializer -from rest_framework.test import APIClient as _APIClient -from rest_framework.test import APITestCase as _APITestCase -from rest_framework.viewsets import ModelViewSet - -from ..user.models import AuthFactor, User - -AnyModelViewSet = t.TypeVar("AnyModelViewSet", bound=ModelViewSet) -AnyModelSerializer = t.TypeVar("AnyModelSerializer", bound=ModelSerializer) -AnyModel = t.TypeVar("AnyModel", bound=Model) - - -class APIClient(_APIClient): - StatusCodeAssertion = t.Optional[t.Union[int, t.Callable[[int], bool]]] - ListFilters = t.Optional[t.Dict[str, str]] - - @staticmethod - def status_code_is_ok(status_code: int): - return 200 <= status_code < 300 - - def generic( - self, - method, - path, - data="", - content_type="application/octet-stream", - secure=False, - status_code_assertion: StatusCodeAssertion = None, - **extra, - ): - wsgi_response = super().generic( - method, path, data, content_type, secure, **extra - ) - - # Use a custom kwarg to handle the common case of checking the - # response's status code. - if status_code_assertion is None: - status_code_assertion = self.status_code_is_ok - elif isinstance(status_code_assertion, int): - expected_status_code = status_code_assertion - status_code_assertion = ( - lambda status_code: status_code == expected_status_code - ) - assert status_code_assertion( - wsgi_response.status_code - ), f"Unexpected status code: {wsgi_response.status_code}." - - return wsgi_response - - def login(self, **credentials): - assert super().login( - **credentials - ), f"Failed to login with credentials: {credentials}." - - user = User.objects.get(session=self.session.session_key) - - if user.session.session_auth_factors.filter( - auth_factor__type=AuthFactor.Type.OTP - ).exists(): - now = timezone.now() - otp = TOTP(user.otp_secret).at(now) - with patch.object(timezone, "now", return_value=now): - assert super().login( - otp=otp - ), f'Failed to login with OTP "{otp}" at {now}.' - - assert user.is_authenticated, "Failed to authenticate user." - - return user - - def login_teacher(self, is_admin: bool, **credentials): - user = self.login(**credentials) - assert user.teacher - assert user.teacher.school - assert is_admin == user.teacher.is_admin - return user - - def login_student(self, **credentials): - user = self.login(**credentials) - assert user.student - assert user.student.class_field.teacher.school - return user - - def login_indy_student(self, **credentials): - user = self.login(**credentials) - assert user.student - assert not user.student.class_field - return user - - @staticmethod - def assert_data_equals_model( - data: t.Dict[str, t.Any], - model: AnyModel, - model_serializer_class: t.Type[AnyModelSerializer], - ): - assert ( - data == model_serializer_class(model).data - ), "Data does not equal serialized model." - - def retrieve( - self, - basename: str, - model: AnyModel, - model_serializer_class: t.Type[AnyModelSerializer], - status_code_assertion: StatusCodeAssertion = None, - model_view_set_class: t.Type[AnyModelViewSet] = None, - **kwargs, - ): - lookup_field = ( - "pk" - if model_view_set_class is None - else model_view_set_class.lookup_field - ) - - response: Response = self.get( - reverse( - f"{basename}-detail", - kwargs={lookup_field: getattr(model, lookup_field)}, - ), - status_code_assertion=status_code_assertion, - **kwargs, - ) - - if self.status_code_is_ok(response.status_code): - self.assert_data_equals_model( - response.json(), - model, - model_serializer_class, - ) - - return response - - def list( - self, - basename: str, - models: t.Iterable[AnyModel], - model_serializer_class: t.Type[AnyModelSerializer], - status_code_assertion: StatusCodeAssertion = None, - filters: ListFilters = None, - **kwargs, - ): - model_class: t.Type[AnyModel] = model_serializer_class.Meta.model - assert model_class.objects.difference( - model_class.objects.filter(pk__in=[model.pk for model in models]) - ).exists(), "List must exclude some models for a valid test." - - response: Response = self.get( - f"{reverse(f'{basename}-list')}?{urlencode(filters or {})}", - status_code_assertion=status_code_assertion, - **kwargs, - ) - - if self.status_code_is_ok(response.status_code): - for data, model in zip(response.json()["data"], models): - self.assert_data_equals_model( - data, - model, - model_serializer_class, - ) - - return response - - -class APITestCase(_APITestCase): - client: APIClient - client_class = APIClient - - def get_other_user( - self, - user: User, - other_users: QuerySet[User], - is_teacher: bool, - ): - """ - Get a different user. - """ - - other_user = other_users.first() - assert other_user - assert user != other_user - assert other_user.is_teacher if is_teacher else other_user.is_student - return other_user - - def get_other_school_user( - self, - user: User, - other_users: QuerySet[User], - is_teacher: bool, - ): - """ - Get a different user that is in a school. - - the provided user does not have to be in a school. - - the other user has to be in a school. - """ - - other_user = self.get_other_user(user, other_users, is_teacher) - assert ( - other_user.teacher.school - if is_teacher - else other_user.student.class_field.teacher.school - ) - return other_user - - def get_another_school_user( - self, - user: User, - other_users: QuerySet[User], - is_teacher: bool, - same_school: bool, - same_class: bool = None, - ): - """ - Get a different user that is also in a school. - - the provided user has to be in a school. - - the other user has to be in a school. - """ - - other_user = self.get_other_school_user(user, other_users, is_teacher) - - school = ( - user.teacher.school - if user.teacher - else user.student.class_field.teacher.school - ) - assert school - - other_school = ( - other_user.teacher.school - if is_teacher - else other_user.student.class_field.teacher.school - ) - assert other_school - - if same_school: - assert school == other_school - - # Cannot assert that 2 teachers are in the same class since a class - # can only have 1 teacher. - if not (user.is_teacher and other_user.is_teacher): - # At this point, same_class needs to be set. - assert same_class is not None, "same_class must be set." - - # If one of the users is a teacher. - if user.is_teacher or is_teacher: - # Get the teacher. - teacher = other_user if is_teacher else user - - # Get the student's class' teacher. - class_teacher = ( - user if is_teacher else other_user - ).student.class_field.teacher.new_user - - # Assert the teacher is the class' teacher. - assert ( - teacher == class_teacher - if same_class - else teacher != class_teacher - ) - # Else, both users are students. - else: - assert ( - user.student.class_field - == other_user.student.class_field - if same_class - else user.student.class_field - != other_user.student.class_field - ) - else: - assert school != other_school - - return other_user diff --git a/codeforlife/tests/cron.py b/codeforlife/tests/cron.py index a510ff6e..661a1c77 100644 --- a/codeforlife/tests/cron.py +++ b/codeforlife/tests/cron.py @@ -1,11 +1,20 @@ -from .api import APIClient, APITestCase +""" +© Ocado Group +Created on 20/01/2024 at 09:52:43(+00:00). +""" +from rest_framework.test import APIClient, APITestCase + + +class CronClient(APIClient): + """Base client for all CRON jobs.""" -class CronTestClient(APIClient): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs, HTTP_X_APPENGINE_CRON="true") class CronTestCase(APITestCase): - client: CronTestClient - client_class = CronTestClient + """Base test case for all CRON jobs.""" + + client: CronClient + client_class = CronClient diff --git a/codeforlife/tests/model_view_set.py b/codeforlife/tests/model_view_set.py new file mode 100644 index 00000000..5bce60e9 --- /dev/null +++ b/codeforlife/tests/model_view_set.py @@ -0,0 +1,587 @@ +""" +© Ocado Group +Created on 19/01/2024 at 17:06:45(+00:00). + +Base test case for all model view sets. +""" + +import typing as t +from datetime import datetime +from unittest.mock import patch + +from django.db.models import Model +from django.db.models.query import QuerySet +from django.urls import reverse as _reverse +from django.utils import timezone +from django.utils.http import urlencode +from pyotp import TOTP +from rest_framework.response import Response +from rest_framework.serializers import ModelSerializer +from rest_framework.test import APIClient, APITestCase +from rest_framework.viewsets import ModelViewSet + +from ..user.models import AuthFactor, User + +AnyModelViewSet = t.TypeVar("AnyModelViewSet", bound=ModelViewSet) +AnyModelSerializer = t.TypeVar("AnyModelSerializer", bound=ModelSerializer) +AnyModel = t.TypeVar("AnyModel", bound=Model) + + +class ModelViewSetClient( + APIClient, + t.Generic[AnyModelViewSet, AnyModelSerializer, AnyModel], +): + """ + An API client that helps make requests to a model view set and assert their + responses. + """ + + Data = t.Dict[str, t.Any] + + _test_case: "ModelViewSetTestCase[AnyModelViewSet, AnyModelSerializer, AnyModel]" + + @property + def _model_class(self): + """Shortcut to get model class.""" + + # pylint: disable-next=no-member + return self._test_case.get_model_class() + + @property + def _model_serializer_class(self): + """Shortcut to get model serializer class.""" + + # pylint: disable-next=no-member + return self._test_case.get_model_serializer_class() + + @property + def _model_view_set_class(self): + """Shortcut to get model view set class.""" + + # pylint: disable-next=no-member + return self._test_case.get_model_view_set_class() + + StatusCodeAssertion = t.Optional[t.Union[int, t.Callable[[int], bool]]] + ListFilters = t.Optional[t.Dict[str, str]] + + @staticmethod + def status_code_is_ok(status_code: int): + """Check if the status code is greater than or equal to 200 and less + than 300. + + Args: + status_code: The status code to check. + + Returns: + A flag designating if the status code is OK. + """ + + return 200 <= status_code < 300 + + def assert_data_equals_model( + self, + data: Data, + model: AnyModel, + contains_subset: bool = False, + ): + # pylint: disable=line-too-long + """Check if the data equals the current state of the model instance. + + Args: + data: The data to check. + model: The model instance. + model_serializer_class: The serializer used to serialize the model's data. + contains_subset: A flag designating whether the data is a subset of the serialized model. + + Returns: + A flag designating if the data equals the current state of the model + instance. + """ + # pylint: enable=line-too-long + + def parse_data(data): + if isinstance(data, list): + return [parse_data(value) for value in data] + if isinstance(data, dict): + return {key: parse_data(value) for key, value in data.items()} + if isinstance(data, datetime): + return data.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + return data + + actual_data = parse_data(self._model_serializer_class(model).data) + + if contains_subset: + # pylint: disable-next=no-member + self._test_case.assertDictContainsSubset( + data, + actual_data, + "Data is not a subset of serialized model.", + ) + else: + # pylint: disable-next=no-member + self._test_case.assertDictEqual( + data, + actual_data, + "Data does not equal serialized model.", + ) + + def reverse( + self, + action: str, + model: t.Optional[AnyModel] = None, + **kwargs, + ): + """Get the reverse URL for the model view set's action. + + Args: + action: The name of the action. + model: The model to look up. + + Returns: + The reversed URL. + """ + + reverse_kwargs = kwargs.pop("kwargs", {}) + if model is not None: + reverse_kwargs[self._model_view_set_class.lookup_field] = getattr( + model, + self._model_view_set_class.lookup_field, + ) + + return _reverse( + viewname=kwargs.pop( + "viewname", + # pylint: disable-next=no-member + f"{self._test_case.basename}-{action}", + ), + kwargs=reverse_kwargs, + **kwargs, + ) + + # pylint: disable-next=too-many-arguments + def generic( + self, + method, + path, + data="", + content_type="application/octet-stream", + secure=False, + status_code_assertion: StatusCodeAssertion = None, + **extra, + ): + response = t.cast( + Response, + super().generic( + method, + path, + data, + content_type, + secure, + **extra, + ), + ) + + # Use a custom kwarg to handle the common case of checking the + # response's status code. + if status_code_assertion is None: + status_code_assertion = self.status_code_is_ok + elif isinstance(status_code_assertion, int): + expected_status_code = status_code_assertion + status_code_assertion = ( + # pylint: disable-next=unnecessary-lambda-assignment + lambda status_code: status_code + == expected_status_code + ) + + # pylint: disable-next=no-member + status_code = response.status_code + assert status_code_assertion( + status_code + ), f"Unexpected status code: {status_code}." + + return response + + def create( + self, + data: Data, + status_code_assertion: StatusCodeAssertion = None, + **kwargs, + ): + """Create a model. + + Args: + data: The values for each field. + status_code_assertion: The expected status code. + + Returns: + The HTTP response. + """ + + response: Response = self.post( + self.reverse("list"), + status_code_assertion=status_code_assertion, + **kwargs, + ) + + if self.status_code_is_ok(response.status_code): + # pylint: disable-next=no-member + self._test_case.assertDictContainsSubset( + data, + response.json(), # type: ignore[attr-defined] + ) + + return response + + def retrieve( + self, + model: AnyModel, + status_code_assertion: StatusCodeAssertion = None, + **kwargs, + ): + """Retrieve a model. + + Args: + model: The model to retrieve. + status_code_assertion: The expected status code. + + Returns: + The HTTP response. + """ + + response: Response = self.get( + self.reverse("detail", model), + status_code_assertion=status_code_assertion, + **kwargs, + ) + + if self.status_code_is_ok(response.status_code): + self.assert_data_equals_model( + response.json(), # type: ignore[attr-defined] + model, + ) + + return response + + def list( + self, + models: t.Iterable[AnyModel], + status_code_assertion: StatusCodeAssertion = None, + filters: ListFilters = None, + **kwargs, + ): + """Retrieve a list of models. + + Args: + models: The model list to retrieve. + status_code_assertion: The expected status code. + filters: The filters to apply to the list. + + Returns: + The HTTP response. + """ + + assert self._model_class.objects.difference( + self._model_class.objects.filter( + pk__in=[model.pk for model in models] + ) + ).exists(), "List must exclude some models for a valid test." + + response: Response = self.get( + f"{self.reverse('list')}?{urlencode(filters or {})}", + status_code_assertion=status_code_assertion, + **kwargs, + ) + + if self.status_code_is_ok(response.status_code): + for data, model in zip(response.json()["data"], models): # type: ignore[attr-defined] + self.assert_data_equals_model(data, model) + + return response + + def partial_update( + self, + model: AnyModel, + data: Data, + status_code_assertion: StatusCodeAssertion = None, + **kwargs, + ): + """Partially update a model. + + Args: + model: The model to partially update. + data: The values for each field. + status_code_assertion: The expected status code. + + Returns: + The HTTP response. + """ + + response: Response = self.patch( + self.reverse("detail", model), + data=data, + status_code_assertion=status_code_assertion, + **kwargs, + ) + + if self.status_code_is_ok(response.status_code): + model.refresh_from_db() + self.assert_data_equals_model( + response.json(), # type: ignore[attr-defined] + model, + contains_subset=True, + ) + + return response + + def destroy( + self, + model: AnyModel, + status_code_assertion: StatusCodeAssertion = None, + **kwargs, + ): + """Destroy a model. + + Args: + model: The model to destroy. + status_code_assertion: The expected status code. + + Returns: + The HTTP response. + """ + + response: Response = self.delete( + self.reverse("detail", model), + status_code_assertion=status_code_assertion, + **kwargs, + ) + + # TODO: add standard post-destroy assertions. + + return response + + def login(self, **credentials): + assert super().login( + **credentials + ), f"Failed to login with credentials: {credentials}." + + user = User.objects.get(session=self.session.session_key) + + if user.session.session_auth_factors.filter( + auth_factor__type=AuthFactor.Type.OTP + ).exists(): + now = timezone.now() + otp = TOTP(user.otp_secret).at(now) + with patch.object(timezone, "now", return_value=now): + assert super().login( + otp=otp + ), f'Failed to login with OTP "{otp}" at {now}.' + + assert user.is_authenticated, "Failed to authenticate user." + + return user + + def login_teacher(self, is_admin: bool, **credentials): + """Log in a user and assert they are a teacher. + + Args: + is_admin: Whether or not the teacher is an admin. + + Returns: + The teacher-user. + """ + + user = self.login(**credentials) + assert user.teacher + assert user.teacher.school + assert is_admin == user.teacher.is_admin + return user + + def login_student(self, **credentials): + """Log in a user and assert they are a student. + + Returns: + The student-user. + """ + + user = self.login(**credentials) + assert user.student + assert user.student.class_field.teacher.school + return user + + def login_indy(self, **credentials): + """Log in an independent and assert they are a student. + + Returns: + The independent-user. + """ + + user = self.login(**credentials) + assert user.student + assert not user.student.class_field + return user + + +class ModelViewSetTestCase( + APITestCase, + t.Generic[AnyModelViewSet, AnyModelSerializer, AnyModel], +): + """Base for all model view set test cases.""" + + basename: str + client: ModelViewSetClient[ # type: ignore[assignment] + AnyModelViewSet, + AnyModelSerializer, + AnyModel, + ] + client_class = ModelViewSetClient # type: ignore[assignment] + + def _pre_setup(self): + super()._pre_setup() + # pylint: disable-next=protected-access + self.client._test_case = self + + @classmethod + def _get_generic_args( + cls, + ) -> t.Tuple[ + t.Type[AnyModelViewSet], + t.Type[AnyModelSerializer], + t.Type[AnyModel], + ]: + # pylint: disable-next=no-member + return t.get_args(cls.__orig_bases__[0]) # type: ignore[attr-defined,return-value] + + @classmethod + def get_model_view_set_class(cls): + """Get the model view set's class. + + Returns: + The model view set's class. + """ + + return cls._get_generic_args()[0] + + @classmethod + def get_model_serializer_class(cls): + """Get the model serializer's class. + + Returns: + The model serializer's class. + """ + + return cls._get_generic_args()[1] + + @classmethod + def get_model_class(cls): + """Get the model view set's class. + + Returns: + The model view set's class. + """ + + return cls._get_generic_args()[2] + + def get_other_user( + self, + user: User, + other_users: QuerySet[User], + is_teacher: bool, + ): + """ + Get a different user. + """ + + other_user = other_users.first() + assert other_user + assert user != other_user + assert other_user.is_teacher if is_teacher else other_user.is_student + return other_user + + def get_other_school_user( + self, + user: User, + other_users: QuerySet[User], + is_teacher: bool, + ): + """ + Get a different user that is in a school. + - the provided user does not have to be in a school. + - the other user has to be in a school. + """ + + other_user = self.get_other_user(user, other_users, is_teacher) + assert ( + other_user.teacher.school + if is_teacher + else other_user.student.class_field.teacher.school + ) + return other_user + + def get_another_school_user( + self, + user: User, + other_users: QuerySet[User], + is_teacher: bool, + same_school: bool, + same_class: t.Optional[bool] = None, + ): + """ + Get a different user that is also in a school. + - the provided user has to be in a school. + - the other user has to be in a school. + """ + + other_user = self.get_other_school_user(user, other_users, is_teacher) + + school = ( + user.teacher.school + if user.teacher + else user.student.class_field.teacher.school # type: ignore[union-attr] + ) + assert school + + other_school = ( + other_user.teacher.school + if is_teacher + else other_user.student.class_field.teacher.school + ) + assert other_school + + if same_school: + assert school == other_school + + # Cannot assert that 2 teachers are in the same class since a class + # can only have 1 teacher. + if not (user.is_teacher and other_user.is_teacher): + # At this point, same_class needs to be set. + assert same_class is not None, "same_class must be set." + + # If one of the users is a teacher. + if user.is_teacher or is_teacher: + # Get the teacher. + teacher = other_user if is_teacher else user + + # Get the student's class' teacher. + class_teacher = ( + user if is_teacher else other_user + ).student.class_field.teacher.new_user + + # Assert the teacher is the class' teacher. + assert ( + teacher == class_teacher + if same_class + else teacher != class_teacher + ) + # Else, both users are students. + else: + assert ( + user.student.class_field # type: ignore[union-attr] + == other_user.student.class_field + if same_class + else user.student.class_field # type: ignore[union-attr] + != other_user.student.class_field + ) + else: + assert school != other_school + + return other_user diff --git a/codeforlife/user/serializers/klass.py b/codeforlife/user/serializers/klass.py index 9b9a81cd..1398db3b 100644 --- a/codeforlife/user/serializers/klass.py +++ b/codeforlife/user/serializers/klass.py @@ -1,15 +1,35 @@ -from rest_framework import serializers +""" +© Ocado Group +Created on 20/01/2024 at 11:28:29(+00:00). +""" +from ...serializers import ModelSerializer from ..models import Class -class ClassSerializer(serializers.ModelSerializer): +# pylint: disable-next=missing-class-docstring +class ClassSerializer(ModelSerializer[Class]): + # pylint: disable-next=missing-class-docstring,too-few-public-methods class Meta: model = Class - fields = "__all__" + fields = [ + "id", + "teacher", + "school", + "name", + "read_classmates_data", + "receive_requests_until", + ] extra_kwargs = { "id": {"read_only": True}, - "access_code": {"read_only": True}, - "creation_time": {"read_only": True}, - "created_by": {"read_only": True}, + } + + def to_representation(self, instance): + return { + "id": instance.access_code, + "name": instance.name, + "read_classmates_data": instance.classmates_data_viewable, + "receive_requests_until": instance.accept_requests_until, + "teacher": instance.teacher.pk, + "school": instance.teacher.school.pk, } diff --git a/codeforlife/user/serializers/school.py b/codeforlife/user/serializers/school.py index e61b3be3..52163149 100644 --- a/codeforlife/user/serializers/school.py +++ b/codeforlife/user/serializers/school.py @@ -1,13 +1,31 @@ -from rest_framework import serializers +""" +© Ocado Group +Created on 20/01/2024 at 11:28:19(+00:00). +""" +from ...serializers import ModelSerializer from ..models import School -class SchoolSerializer(serializers.ModelSerializer): +# pylint: disable-next=missing-class-docstring +class SchoolSerializer(ModelSerializer[School]): + # pylint: disable-next=missing-class-docstring,too-few-public-methods class Meta: model = School - fields = "__all__" + fields = [ + "id", + "name", + "country", + "uk_county", + ] extra_kwargs = { "id": {"read_only": True}, - "creation_time": {"read_only": True}, + } + + def to_representation(self, instance): + return { + "id": instance.id, + "name": instance.name, + "country": str(instance.country), + "uk_county": instance.county, } diff --git a/codeforlife/user/serializers/student.py b/codeforlife/user/serializers/student.py index 660e03f7..e1d1f07c 100644 --- a/codeforlife/user/serializers/student.py +++ b/codeforlife/user/serializers/student.py @@ -1,12 +1,29 @@ -from rest_framework import serializers +""" +© Ocado Group +Created on 20/01/2024 at 11:27:56(+00:00). +""" +from ...serializers import ModelSerializer from ..models import Student -class StudentSerializer(serializers.ModelSerializer): +# pylint: disable-next=missing-class-docstring +class StudentSerializer(ModelSerializer[Student]): + # pylint: disable-next=missing-class-docstring,too-few-public-methods class Meta: model = Student - fields = "__all__" + fields = [ + "id", + "klass", + "school", + ] extra_kwargs = { "id": {"read_only": True}, } + + def to_representation(self, instance): + return { + "id": instance.id, + "klass": instance.class_field.access_code, + "school": instance.class_field.teacher.school.pk, + } diff --git a/codeforlife/user/serializers/teacher.py b/codeforlife/user/serializers/teacher.py index 160c63a7..cf15e29f 100644 --- a/codeforlife/user/serializers/teacher.py +++ b/codeforlife/user/serializers/teacher.py @@ -1,12 +1,22 @@ -from rest_framework import serializers +""" +© Ocado Group +Created on 20/01/2024 at 11:27:43(+00:00). +""" +from ...serializers import ModelSerializer from ..models import Teacher -class TeacherSerializer(serializers.ModelSerializer): +# pylint: disable-next=missing-class-docstring +class TeacherSerializer(ModelSerializer[Teacher]): + # pylint: disable-next=missing-class-docstring,too-few-public-methods class Meta: model = Teacher - fields = "__all__" + fields = [ + "id", + "school", + "is_admin", + ] extra_kwargs = { "id": {"read_only": True}, } diff --git a/codeforlife/user/serializers/user.py b/codeforlife/user/serializers/user.py index 5b3e9934..e14ac819 100644 --- a/codeforlife/user/serializers/user.py +++ b/codeforlife/user/serializers/user.py @@ -1,18 +1,73 @@ -from rest_framework import serializers +""" +© Ocado Group +Created on 19/01/2024 at 11:06:00(+00:00). +""" -from ..models import User +from ...serializers import ModelSerializer +from ..models import User, Student, Teacher from .student import StudentSerializer from .teacher import TeacherSerializer -class UserSerializer(serializers.ModelSerializer): - student = StudentSerializer(source="new_student") - teacher = TeacherSerializer(source="new_teacher") +# pylint: disable-next=missing-class-docstring +class UserSerializer(ModelSerializer[User]): + student = StudentSerializer( + source="new_student", + read_only=True, + ) + teacher = TeacherSerializer( + source="new_teacher", + read_only=True, + ) + + # pylint: disable-next=missing-class-docstring,too-few-public-methods class Meta: model = User - fields = "__all__" + fields = [ + "student", + "teacher", + "id", + "password", + "first_name", + "last_name", + "email", + "is_active", + "date_joined", + ] extra_kwargs = { "id": {"read_only": True}, "password": {"write_only": True}, + "is_active": {"read_only": True}, + "date_joined": {"read_only": True}, + } + + def to_representation(self, instance): + try: + student = ( + StudentSerializer(instance.new_student).data + if instance.new_student and instance.new_student.class_field + else None + ) + except Student.DoesNotExist: + student = None + + try: + teacher = ( + TeacherSerializer(instance.new_teacher).data + if instance.new_teacher + else None + ) + except Teacher.DoesNotExist: + teacher = None + + return { + "id": instance.id, + "first_name": instance.first_name, + "last_name": instance.last_name, + "email": instance.email, + "is_active": instance.is_active, + "date_joined": instance.date_joined, + "student": student, + "teacher": teacher, } diff --git a/codeforlife/user/tests/views/test_klass.py b/codeforlife/user/tests/views/test_klass.py index 87ba9d6b..9db22791 100644 --- a/codeforlife/user/tests/views/test_klass.py +++ b/codeforlife/user/tests/views/test_klass.py @@ -1,10 +1,17 @@ -from ....tests import APIClient, APITestCase +""" +© Ocado Group +Created on 20/01/2024 at 09:48:30(+00:00). +""" + +from ....tests import ModelViewSetTestCase from ...models import Class from ...serializers import ClassSerializer from ...views import ClassViewSet -class TestClassViewSet(APITestCase): +class TestClassViewSet( + ModelViewSetTestCase[ClassViewSet, ClassSerializer, Class] +): """ Base naming convention: test_{action} @@ -13,12 +20,15 @@ class TestClassViewSet(APITestCase): https://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions """ + basename = "class" + def _login_student(self): return self.client.login_student( email="leonardodavinci@codeforlife.com", password="Password1", ) + # pylint: disable-next=pointless-string-statement """ Retrieve naming convention: test_retrieve__{user_type}__{same_school}__{in_class} @@ -39,19 +49,6 @@ def _login_student(self): - not_in_class: The user is not in the class. """ - def _retrieve_class( - self, - klass: Class, - status_code_assertion: APIClient.StatusCodeAssertion = None, - ): - return self.client.retrieve( - "class", - klass, - ClassSerializer, - status_code_assertion, - ClassViewSet, - ) - def test_retrieve__student__same_school__in_class(self): """ Student can retrieve a class from the same school and a class they are @@ -60,6 +57,6 @@ def test_retrieve__student__same_school__in_class(self): user = self._login_student() - self._retrieve_class(user.student.class_field) + self.client.retrieve(user.student.class_field) # TODO: other retrieve and list tests diff --git a/codeforlife/user/tests/views/test_school.py b/codeforlife/user/tests/views/test_school.py index 50d332ee..ed6c792b 100644 --- a/codeforlife/user/tests/views/test_school.py +++ b/codeforlife/user/tests/views/test_school.py @@ -1,15 +1,20 @@ -import typing as t +""" +© Ocado Group +Created on 20/01/2024 at 09:47:30(+00:00). +""" from rest_framework import status from rest_framework.permissions import IsAuthenticated -from ....tests import APIClient, APITestCase +from ....tests import ModelViewSetTestCase from ...models import Class, School, Student, Teacher, User, UserProfile from ...serializers import SchoolSerializer from ...views import SchoolViewSet -class TestSchoolViewSet(APITestCase): +class TestSchoolViewSet( + ModelViewSetTestCase[SchoolViewSet, SchoolSerializer, School] +): """ Base naming convention: test_{action} @@ -18,6 +23,8 @@ class TestSchoolViewSet(APITestCase): https://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions """ + basename = "school" + # TODO: replace this setup with data fixtures. def setUp(self): school = School.objects.create( @@ -74,12 +81,13 @@ def _login_student(self): password="Password1", ) - def _login_indy_student(self): - return self.client.login_indy_student( + def _login_indy(self): + return self.client.login_indy( email="indianajones@codeforlife.com", password="Password1", ) + # pylint: disable-next=pointless-string-statement """ Retrieve naming convention: test_retrieve__{user_type}__{same_school} @@ -95,32 +103,17 @@ def _login_indy_student(self): - not_same_school: The other user is not from the same school. """ - def _retrieve_school( - self, - school: School, - status_code_assertion: APIClient.StatusCodeAssertion = None, - ): - return self.client.retrieve( - "school", - school, - SchoolSerializer, - status_code_assertion, - ) - def test_retrieve__indy_student(self): """ Independent student cannot retrieve any school. """ - self._login_indy_student() + self._login_indy() school = School.objects.first() assert school - self._retrieve_school( - school, - status_code_assertion=status.HTTP_403_FORBIDDEN, - ) + self.client.retrieve(school, status.HTTP_403_FORBIDDEN) def test_retrieve__teacher__same_school(self): """ @@ -129,7 +122,7 @@ def test_retrieve__teacher__same_school(self): user = self._login_teacher() - self._retrieve_school(user.teacher.school) + self.client.retrieve(user.teacher.school) def test_retrieve__student__same_school(self): """ @@ -138,7 +131,7 @@ def test_retrieve__student__same_school(self): user = self._login_student() - self._retrieve_school(user.student.class_field.teacher.school) + self.client.retrieve(user.student.class_field.teacher.school) def test_retrieve__teacher__not_same_school(self): """ @@ -150,10 +143,7 @@ def test_retrieve__teacher__not_same_school(self): school = School.objects.exclude(id=user.teacher.school.id).first() assert school - self._retrieve_school( - school, - status_code_assertion=status.HTTP_404_NOT_FOUND, - ) + self.client.retrieve(school, status.HTTP_404_NOT_FOUND) def test_retrieve__student__not_same_school(self): """ @@ -167,11 +157,9 @@ def test_retrieve__student__not_same_school(self): ).first() assert school - self._retrieve_school( - school, - status_code_assertion=status.HTTP_404_NOT_FOUND, - ) + self.client.retrieve(school, status.HTTP_404_NOT_FOUND) + # pylint: disable-next=pointless-string-statement """ List naming convention: test_list__{user_type} @@ -182,29 +170,14 @@ def test_retrieve__student__not_same_school(self): - indy_student: A non-school student. """ - def _list_schools( - self, - schools: t.Iterable[School], - status_code_assertion: APIClient.StatusCodeAssertion = None, - ): - return self.client.list( - "school", - schools, - SchoolSerializer, - status_code_assertion, - ) - def test_list__indy_student(self): """ Independent student cannot list any schools. """ - self._login_indy_student() + self._login_indy() - self._list_schools( - [], - status_code_assertion=status.HTTP_403_FORBIDDEN, - ) + self.client.list([], status.HTTP_403_FORBIDDEN) def test_list__teacher(self): """ @@ -213,7 +186,7 @@ def test_list__teacher(self): user = self._login_teacher() - self._list_schools([user.teacher.school]) + self.client.list([user.teacher.school]) def test_list__student(self): """ @@ -222,8 +195,9 @@ def test_list__student(self): user = self._login_student() - self._list_schools([user.student.class_field.teacher.school]) + self.client.list([user.student.class_field.teacher.school]) + # pylint: disable-next=pointless-string-statement """ General tests that apply to all actions. """ diff --git a/codeforlife/user/tests/views/test_user.py b/codeforlife/user/tests/views/test_user.py index 4cb9a1c9..7ca529ed 100644 --- a/codeforlife/user/tests/views/test_user.py +++ b/codeforlife/user/tests/views/test_user.py @@ -1,15 +1,19 @@ -import typing as t +""" +© Ocado Group +Created on 19/01/2024 at 17:15:56(+00:00). +""" from rest_framework import status from rest_framework.permissions import IsAuthenticated -from ....tests import APIClient, APITestCase +from ....tests import ModelViewSetTestCase from ...models import Class, School, Student, Teacher, User, UserProfile from ...serializers import UserSerializer from ...views import UserViewSet -class TestUserViewSet(APITestCase): +# pylint: disable-next=too-many-ancestors,too-many-public-methods +class TestUserViewSet(ModelViewSetTestCase[UserViewSet, UserSerializer, User]): """ Base naming convention: test_{action} @@ -18,6 +22,8 @@ class TestUserViewSet(APITestCase): https://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions """ + basename = "user" + # TODO: replace this setup with data fixtures. def setUp(self): school = School.objects.create( @@ -81,12 +87,13 @@ def _login_student(self): password="Password1", ) - def _login_indy_student(self): - return self.client.login_indy_student( + def _login_indy(self): + return self.client.login_indy( email="indianajones@codeforlife.com", password="Password1", ) + # pylint: disable-next=pointless-string-statement """ Retrieve naming convention: test_retrieve__{user_type}__{other_user_type}__{same_school}__{same_class} @@ -111,18 +118,6 @@ def _login_indy_student(self): - not_same_class: The other user is not from the same class. """ - def _retrieve_user( - self, - user: User, - status_code_assertion: APIClient.StatusCodeAssertion = None, - ): - return self.client.retrieve( - "user", - user, - UserSerializer, - status_code_assertion, - ) - def test_retrieve__teacher__self(self): """ Teacher can retrieve their own user data. @@ -130,7 +125,7 @@ def test_retrieve__teacher__self(self): user = self._login_teacher() - self._retrieve_user(user) + self.client.retrieve(user) def test_retrieve__student__self(self): """ @@ -139,16 +134,16 @@ def test_retrieve__student__self(self): user = self._login_student() - self._retrieve_user(user) + self.client.retrieve(user) def test_retrieve__indy_student__self(self): """ Independent student can retrieve their own user data. """ - user = self._login_indy_student() + user = self._login_indy() - self._retrieve_user(user) + self.client.retrieve(user) def test_retrieve__teacher__teacher__same_school(self): """ @@ -166,7 +161,7 @@ def test_retrieve__teacher__teacher__same_school(self): same_school=True, ) - self._retrieve_user(other_user) + self.client.retrieve(other_user) def test_retrieve__teacher__student__same_school__same_class(self): """ @@ -186,7 +181,7 @@ def test_retrieve__teacher__student__same_school__same_class(self): same_class=True, ) - self._retrieve_user(other_user) + self.client.retrieve(other_user) def test_retrieve__teacher__student__same_school__not_same_class(self): """ @@ -206,10 +201,7 @@ def test_retrieve__teacher__student__same_school__not_same_class(self): same_class=False, ) - self._retrieve_user( - other_user, - status_code_assertion=status.HTTP_404_NOT_FOUND, - ) + self.client.retrieve(other_user, status.HTTP_404_NOT_FOUND) def test_retrieve__admin_teacher__student__same_school__same_class(self): """ @@ -229,7 +221,7 @@ def test_retrieve__admin_teacher__student__same_school__same_class(self): same_class=True, ) - self._retrieve_user(other_user) + self.client.retrieve(other_user) def test_retrieve__admin_teacher__student__same_school__not_same_class( self, @@ -251,11 +243,11 @@ def test_retrieve__admin_teacher__student__same_school__not_same_class( same_class=False, ) - self._retrieve_user(other_user) + self.client.retrieve(other_user) def test_retrieve__student__teacher__same_school__same_class(self): """ - Student cannot retrieve a teacher from the same school and class. + Student can retrieve a teacher from the same school and class. """ user = self._login_student() @@ -271,10 +263,7 @@ def test_retrieve__student__teacher__same_school__same_class(self): same_class=True, ) - self._retrieve_user( - other_user, - status_code_assertion=status.HTTP_404_NOT_FOUND, - ) + self.client.retrieve(other_user) def test_retrieve__student__teacher__same_school__not_same_class(self): """ @@ -294,10 +283,7 @@ def test_retrieve__student__teacher__same_school__not_same_class(self): same_class=False, ) - self._retrieve_user( - other_user, - status_code_assertion=status.HTTP_404_NOT_FOUND, - ) + self.client.retrieve(other_user, status.HTTP_404_NOT_FOUND) def test_retrieve__student__student__same_school__same_class(self): """ @@ -317,7 +303,7 @@ def test_retrieve__student__student__same_school__same_class(self): same_class=True, ) - self._retrieve_user(other_user) + self.client.retrieve(other_user) def test_retrieve__student__student__same_school__not_same_class(self): """ @@ -339,10 +325,7 @@ def test_retrieve__student__student__same_school__not_same_class(self): same_class=False, ) - self._retrieve_user( - other_user, - status_code_assertion=status.HTTP_404_NOT_FOUND, - ) + self.client.retrieve(other_user, status.HTTP_404_NOT_FOUND) def test_retrieve__teacher__teacher__not_same_school(self): """ @@ -360,10 +343,7 @@ def test_retrieve__teacher__teacher__not_same_school(self): same_school=False, ) - self._retrieve_user( - other_user, - status_code_assertion=status.HTTP_404_NOT_FOUND, - ) + self.client.retrieve(other_user, status.HTTP_404_NOT_FOUND) def test_retrieve__teacher__student__not_same_school(self): """ @@ -381,10 +361,7 @@ def test_retrieve__teacher__student__not_same_school(self): same_school=False, ) - self._retrieve_user( - other_user, - status_code_assertion=status.HTTP_404_NOT_FOUND, - ) + self.client.retrieve(other_user, status.HTTP_404_NOT_FOUND) def test_retrieve__student__teacher__not_same_school(self): """ @@ -402,10 +379,7 @@ def test_retrieve__student__teacher__not_same_school(self): same_school=False, ) - self._retrieve_user( - other_user, - status_code_assertion=status.HTTP_404_NOT_FOUND, - ) + self.client.retrieve(other_user, status.HTTP_404_NOT_FOUND) def test_retrieve__student__student__not_same_school(self): """ @@ -423,17 +397,14 @@ def test_retrieve__student__student__not_same_school(self): same_school=False, ) - self._retrieve_user( - other_user, - status_code_assertion=status.HTTP_404_NOT_FOUND, - ) + self.client.retrieve(other_user, status.HTTP_404_NOT_FOUND) def test_retrieve__indy_student__teacher(self): """ Independent student cannot retrieve a teacher. """ - user = self._login_indy_student() + user = self._login_indy() other_user = self.get_other_school_user( user, @@ -441,17 +412,14 @@ def test_retrieve__indy_student__teacher(self): is_teacher=True, ) - self._retrieve_user( - other_user, - status_code_assertion=status.HTTP_404_NOT_FOUND, - ) + self.client.retrieve(other_user, status.HTTP_404_NOT_FOUND) def test_retrieve__indy_student__student(self): """ Independent student cannot retrieve a student. """ - user = self._login_indy_student() + user = self._login_indy() other_user = self.get_other_school_user( user, @@ -461,11 +429,9 @@ def test_retrieve__indy_student__student(self): is_teacher=False, ) - self._retrieve_user( - other_user, - status_code_assertion=status.HTTP_404_NOT_FOUND, - ) + self.client.retrieve(other_user, status.HTTP_404_NOT_FOUND) + # pylint: disable-next=pointless-string-statement """ List naming convention: test_list__{user_type}__{filters} @@ -478,28 +444,14 @@ def test_retrieve__indy_student__student(self): filters: Any search params used to dynamically filter the list. """ - def _list_users( - self, - users: t.Iterable[User], - status_code_assertion: APIClient.StatusCodeAssertion = None, - filters: APIClient.ListFilters = None, - ): - return self.client.list( - "user", - users, - UserSerializer, - status_code_assertion, - filters, - ) - def test_list__teacher(self): """ - Teacher can list all the users in the same school. + Teacher can list all the users in the same class. """ user = self._login_teacher() - self._list_users( + self.client.list( User.objects.filter(new_teacher__school=user.teacher.school) | User.objects.filter( new_student__class_field__teacher__school=user.teacher.school, @@ -517,29 +469,37 @@ def test_list__teacher__students_in_class(self): klass = user.teacher.class_teacher.first() assert klass - self._list_users( + self.client.list( User.objects.filter(new_student__class_field=klass), filters={"students_in_class": klass.id}, ) def test_list__student(self): """ - Student can list only themself. + Student can list all users in their class. """ user = self._login_student() - self._list_users([user]) + self.client.list( + [ + user.student.class_field.teacher.new_user, + *User.objects.filter( + new_student__class_field=user.student.class_field + ), + ] + ) def test_list__indy_student(self): """ Independent student can list only themself. """ - user = self._login_indy_student() + user = self._login_indy() - self._list_users([user]) + self.client.list([user]) + # pylint: disable-next=pointless-string-statement """ General tests that apply to all actions. """ diff --git a/codeforlife/user/urls.py b/codeforlife/user/urls.py index e797e5a4..a203c079 100644 --- a/codeforlife/user/urls.py +++ b/codeforlife/user/urls.py @@ -1,13 +1,10 @@ -from django.urls import include, path from rest_framework.routers import DefaultRouter -from .views import ClassViewSet, UserViewSet, SchoolViewSet +from .views import ClassViewSet, SchoolViewSet, UserViewSet router = DefaultRouter() router.register("classes", ClassViewSet, basename="class") router.register("users", UserViewSet, basename="user") router.register("schools", SchoolViewSet, basename="school") -urlpatterns = [ - path("", include(router.urls)), -] +urlpatterns = router.urls diff --git a/codeforlife/user/views/user.py b/codeforlife/user/views/user.py index 25b0ba37..9efa496c 100644 --- a/codeforlife/user/views/user.py +++ b/codeforlife/user/views/user.py @@ -16,10 +16,15 @@ def get_queryset(self): if user.student.class_field is None: return User.objects.filter(id=user.id) - return User.objects.filter( + teachers = User.objects.filter( + new_teacher=user.student.class_field.teacher + ) + students = User.objects.filter( new_student__class_field=user.student.class_field ) + return teachers | students + teachers = User.objects.filter( new_teacher__school=user.teacher.school_id )