From c5c11c271837f64a7abb38205245e91be4d5ecc8 Mon Sep 17 00:00:00 2001 From: Stefan Kairinos <118008817+SKairinos@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:07:26 +0000 Subject: [PATCH] New data model permissions (#29) * add foreign key id fields * add permissions * support checking admin teachers * replace permissions * remove unused requirements and fix test discovery * fix unit tests * add module docstrings * pylint disable * create unit tests * feedback --- Pipfile | 9 - Pipfile.lock | 828 +----------------- codeforlife/models/__init__.py | 14 +- codeforlife/permissions/__init__.py | 6 + codeforlife/permissions/is_self.py | 26 + codeforlife/tests/api.py | 18 +- .../user/auth/backends/email_and_password.py | 14 +- .../user/auth/backends/otp_bypass_token.py | 15 +- .../auth/backends/user_id_and_login_id.py | 2 - codeforlife/user/fixtures/users.json | 15 +- codeforlife/user/models/auth_factor.py | 1 + codeforlife/user/models/klass.py | 2 + codeforlife/user/models/otp_bypass_token.py | 1 + codeforlife/user/models/session.py | 1 + .../user/models/session_auth_factor.py | 2 + codeforlife/user/models/student.py | 2 + codeforlife/user/models/teacher.py | 1 + codeforlife/user/models/user.py | 7 +- codeforlife/user/permissions/__init__.py | 12 +- codeforlife/user/permissions/in_class.py | 43 + codeforlife/user/permissions/in_school.py | 41 + .../user/permissions/is_independent.py | 22 + .../user/permissions/is_school_member.py | 18 - .../user/permissions/is_school_teacher.py | 15 - codeforlife/user/permissions/is_student.py | 35 + codeforlife/user/permissions/is_teacher.py | 47 + codeforlife/user/serializers/user.py | 4 +- .../user/tests/permissions/__init__.py | 0 .../user/tests/permissions/test_in_class.py | 97 ++ .../user/tests/permissions/test_in_school.py | 29 + .../tests/permissions/test_is_independent.py | 42 + .../user/tests/permissions/test_is_student.py | 76 ++ .../user/tests/permissions/test_is_teacher.py | 137 +++ codeforlife/user/tests/views/test_klass.py | 1 + codeforlife/user/tests/views/test_school.py | 54 +- codeforlife/user/tests/views/test_user.py | 67 +- codeforlife/user/views/klass.py | 5 +- codeforlife/user/views/school.py | 5 +- codeforlife/user/views/user.py | 59 +- 39 files changed, 783 insertions(+), 990 deletions(-) create mode 100644 codeforlife/permissions/is_self.py create mode 100644 codeforlife/user/permissions/in_class.py create mode 100644 codeforlife/user/permissions/in_school.py create mode 100644 codeforlife/user/permissions/is_independent.py delete mode 100644 codeforlife/user/permissions/is_school_member.py delete mode 100644 codeforlife/user/permissions/is_school_teacher.py create mode 100644 codeforlife/user/permissions/is_student.py create mode 100644 codeforlife/user/permissions/is_teacher.py create mode 100644 codeforlife/user/tests/permissions/__init__.py create mode 100644 codeforlife/user/tests/permissions/test_in_class.py create mode 100644 codeforlife/user/tests/permissions/test_in_school.py create mode 100644 codeforlife/user/tests/permissions/test_is_independent.py create mode 100644 codeforlife/user/tests/permissions/test_is_student.py create mode 100644 codeforlife/user/tests/permissions/test_is_teacher.py diff --git a/Pipfile b/Pipfile index b66a30bb..7f76e7fb 100644 --- a/Pipfile +++ b/Pipfile @@ -13,15 +13,6 @@ django-cors-headers = "==4.1.0" pydantic = "==1.10.7" flask = "==2.2.3" pyotp = "==2.9.0" -importlib-metadata = "==4.13.0" # TODO: remove. needed by old portal -django-formtools = "==2.2" # TODO: remove. needed by old portal -django-otp = "==1.0.2" # TODO: remove. needed by old portal -# https://pypi.org/user/codeforlife/ -cfl-common = "==6.37.1" # TODO: remove -codeforlife-portal = "==6.37.1" # TODO: remove -aimmo = "==2.10.6" # TODO: remove -rapid-router = "==5.11.3" # TODO: remove -phonenumbers = "==8.12.12" # TODO: remove [dev-packages] black = "==23.1.0" diff --git a/Pipfile.lock b/Pipfile.lock index b9e388b9..40e060d1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b31238bbff04bf67a1f6e56c714ba00fc017cd6a0a4ab38a62f2bdb258f9667f" + "sha256": "860196ab795a6de76b8a385ea76f4b38f982839b4aa23ea3b16248af01b555a7" }, "pipfile-spec": 6, "requires": { @@ -16,14 +16,6 @@ ] }, "default": { - "aimmo": { - "hashes": [ - "sha256:b89f83586412320b147ea61b4277599732c10e7668fba5b2d0a383db6a173145", - "sha256:bd2841b24d7830096b7cc81bdf7548377d30602f1a1b3d9e9084a58b11557413" - ], - "index": "pypi", - "version": "==2.10.6" - }, "asgiref": { "hashes": [ "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e", @@ -32,134 +24,6 @@ "markers": "python_version >= '3.7'", "version": "==3.7.2" }, - "attrs": { - "hashes": [ - "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", - "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" - ], - "markers": "python_version >= '3.7'", - "version": "==23.1.0" - }, - "cachetools": { - "hashes": [ - "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2", - "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1" - ], - "markers": "python_version >= '3.7'", - "version": "==5.3.2" - }, - "certifi": { - "hashes": [ - "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", - "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" - ], - "markers": "python_version >= '3.6'", - "version": "==2023.11.17" - }, - "cfl-common": { - "hashes": [ - "sha256:24045d5550c741249a1d466cfb90149883454833accb48bbdb1aceec51e885c1", - "sha256:e28553af70dc4388fc73a6dfb4ece08e8518f21531d63213b448d24a5c0abef3" - ], - "index": "pypi", - "version": "==6.37.1" - }, - "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", @@ -168,30 +32,6 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, - "codeforlife-portal": { - "hashes": [ - "sha256:3c31ac0135af0cd78ec39e1b3e32ba98e514c05d944447e504002c000ce6b334", - "sha256:63a234390da9728139de7fbe8a4da9f4ca4b4b24d37c4bed248e5d49af563e53" - ], - "index": "pypi", - "version": "==6.37.1" - }, - "defusedxml": { - "hashes": [ - "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", - "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.7.1" - }, - "diff-match-patch": { - "hashes": [ - "sha256:953019cdb9c9d2c9e47b5b12bcff3cf4746fc4598eb406076fa1fc27e6a1f15c", - "sha256:dce43505fb7b1b317de7195579388df0746d90db07015ed47a85e5e44930ef93" - ], - "markers": "python_version >= '3.7'", - "version": "==20230430" - }, "django": { "hashes": [ "sha256:a477ab326ae7d8807dc25c186b951ab8c7648a3a23f9497763c37307a2b5ef87", @@ -200,13 +40,6 @@ "index": "pypi", "version": "==3.2.20" }, - "django-classy-tags": { - "hashes": [ - "sha256:25eb4f95afee396148683bfb4811b83b3f5729218d73ad0a3399271a6f9fcc49", - "sha256:d59d98bdf96a764dcf7a2929a86439d023b283a9152492811c7e44fc47555bc9" - ], - "version": "==2.0.0" - }, "django-cors-headers": { "hashes": [ "sha256:36a8d7a6dee6a85f872fe5916cc878a36d0812043866355438dfeda0b20b6b78", @@ -223,13 +56,6 @@ "index": "pypi", "version": "==7.3.1" }, - "django-csp": { - "hashes": [ - "sha256:01443a07723f9a479d498bd7bb63571aaa771e690f64bde515db6cdb76e8041a", - "sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727" - ], - "version": "==3.7" - }, "django-filter": { "hashes": [ "sha256:2fe15f78108475eda525692813205fa6f9e8c1caf1ae65daa5862d403c6dbf00", @@ -240,34 +66,19 @@ }, "django-formtools": { "hashes": [ - "sha256:304fa777b8ef9e0693ce7833f885cb89ba46b0e46fc23b01176900a93f46742f", - "sha256:c5272c03c1cd51b2375abf7397a199a3148a9fbbf2f100e186467a84025d13b2" - ], - "index": "pypi", - "version": "==2.2" - }, - "django-import-export": { - "hashes": [ - "sha256:2c1b16e1cf2ea5f62a165d8867e7c6dcff25673ab7201fd18aaf67c9ee90367e", - "sha256:78973202e93897326ab0411d64eaf89b72779fcb21ee9e5f64f3fb96571a5978" - ], - "markers": "python_version >= '3.8'", - "version": "==3.3.3" - }, - "django-js-reverse": { - "hashes": [ - "sha256:2a392d169f44e30b883c30dfcfd917a14167ce8fe196c99d2385b31c90d77aa0", - "sha256:8134c2ab6307c945edfa90671ca65e85d6c1754d48566bdd6464be259cc80c30" + "sha256:21f8d5dac737f1e636fa8a0a10969c1c32f525a6dfa27c29592827ba70d9643a", + "sha256:49ea8a64ddef4728a558bf5f8f622c0f4053b979edcf193bf00dd80432ab2f12" ], - "version": "==0.9.1" + "markers": "python_version >= '3.7'", + "version": "==2.4.1" }, "django-otp": { "hashes": [ - "sha256:8ba5ab9bd2738c7321376c349d7cce49cf4404e79f6804e0a3cc462a91728e18", - "sha256:f523fb9dec420f28a29d3e2ad72ac06f64588956ed4f2b5b430d8e957ebb8287" + "sha256:5277731bc05b6cdbf96aa84ac46018e30ed5fb248086053b0146f925de059060", + "sha256:8f4156a3c14ce2aaa31379385eadf388925cd50fc4b5d20a3b944f454c98ff7c" ], - "index": "pypi", - "version": "==1.0.2" + "markers": "python_version >= '3.7'", + "version": "==1.3.0" }, "django-phonenumber-field": { "hashes": [ @@ -277,46 +88,6 @@ "markers": "python_version >= '3.7'", "version": "==6.4.0" }, - "django-pipeline": { - "hashes": [ - "sha256:26f1d344a7bf39bc92c9dc520093471d912de53abd7d22ac715e77d779a831c8", - "sha256:56c299cec0e644e77d5f928f4cebfff804b919cc10ff5c0bfaa070ff57e8da44" - ], - "version": "==2.0.8" - }, - "django-preventconcurrentlogins": { - "hashes": [ - "sha256:9cb45fcd63edeec55e5ac29bbd2ee96974dc2a72d74ab88088dbf6a1f52978e9" - ], - "version": "==0.8.2" - }, - "django-ratelimit": { - "hashes": [ - "sha256:73223d860abd5c5d7b9a807fabb39a6220068129b514be8d78044b52607ab154", - "sha256:857e797f23de948b204a31dba9d88aea3ce731b7a5d926d0240c772e19b5486f" - ], - "markers": "python_version >= '3.4'", - "version": "==3.0.1" - }, - "django-recaptcha": { - "hashes": [ - "sha256:567784963fd5400feaf92e8951d8dbbbdb4b4c48a76e225d4baa63a2c9d2cd8c" - ], - "version": "==2.0.6" - }, - "django-sekizai": { - "hashes": [ - "sha256:5c5e16845d37ce822fc655ce79ec02715191b3d03330b550997bcb842cf24fdf", - "sha256:e829f09b0d6bf01ee5cde05de1fb3faf2fbc5df66dc4dc280fbaac224ca4336f" - ], - "version": "==2.0.0" - }, - "django-treebeard": { - "hashes": [ - "sha256:83aebc34a9f06de7daaec330d858d1c47887e81be3da77e3541fe7368196dd8a" - ], - "version": "==4.3.1" - }, "django-two-factor-auth": { "hashes": [ "sha256:3fac266d12472ac66475dd737bb18f2992484313bf56acf5a2eea5e824291ee6", @@ -333,29 +104,6 @@ "index": "pypi", "version": "==3.13.1" }, - "dnspython": { - "hashes": [ - "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", - "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "et-xmlfile": { - "hashes": [ - "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c", - "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada" - ], - "markers": "python_version >= '3.6'", - "version": "==1.1.0" - }, - "eventlet": { - "hashes": [ - "sha256:27ae41fad9deed9bbf4166f3e3b65acc15d524d42210a518e5877da85a6b8c5d", - "sha256:b36ec2ecc003de87fc87b93197d77fea528aa0f9204a34fdf3b2f8d0f01e017b" - ], - "version": "==0.31.0" - }, "flask": { "hashes": [ "sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d", @@ -364,100 +112,13 @@ "index": "pypi", "version": "==2.2.3" }, - "google-auth": { - "hashes": [ - "sha256:d5d66b8f4f6e3273740d7bb73ddefa6c2d1ff691704bd407d51c6b5800e7c97b", - "sha256:dfd7b44935d498e106c08883b2dac0ad36d8aa10402a6412e9a1c9d74b4773f1" - ], - "markers": "python_version >= '3.7'", - "version": "==2.25.1" - }, - "greenlet": { - "hashes": [ - "sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174", - "sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd", - "sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa", - "sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a", - "sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec", - "sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565", - "sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d", - "sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c", - "sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234", - "sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d", - "sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546", - "sha256:337322096d92808f76ad26061a8f5fccb22b0809bea39212cd6c406f6a7060d2", - "sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74", - "sha256:41bdeeb552d814bcd7fb52172b304898a35818107cc8778b5101423c9017b3de", - "sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd", - "sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9", - "sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3", - "sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846", - "sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2", - "sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353", - "sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8", - "sha256:6e6061bf1e9565c29002e3c601cf68569c450be7fc3f7336671af7ddb4657166", - "sha256:80ac992f25d10aaebe1ee15df45ca0d7571d0f70b645c08ec68733fb7a020206", - "sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b", - "sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d", - "sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe", - "sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997", - "sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445", - "sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0", - "sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96", - "sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884", - "sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6", - "sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1", - "sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619", - "sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94", - "sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4", - "sha256:b489c36d1327868d207002391f662a1d163bdc8daf10ab2e5f6e41b9b96de3b1", - "sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63", - "sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd", - "sha256:b9934adbd0f6e476f0ecff3c94626529f344f57b38c9a541f87098710b18af0a", - "sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376", - "sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57", - "sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16", - "sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e", - "sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc", - "sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a", - "sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c", - "sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5", - "sha256:dc4d815b794fd8868c4d67602692c21bf5293a75e4b607bb92a11e821e2b859a", - "sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72", - "sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9", - "sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9", - "sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e", - "sha256:f33f3258aae89da191c6ebaa3bc517c6c4cbc9b9f689e5d8452f7aedbb913fa8", - "sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65", - "sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064", - "sha256:fa24255ae3c0ab67e613556375a4341af04a084bd58764731972bcbc8baeba36" - ], - "markers": "python_version >= '3.7'", - "version": "==3.0.1" - }, - "hypothesis": { - "hashes": [ - "sha256:6a3471ff74864ab04a0650c75500ef15f2f4a901d49ccbb7cbec668365736688", - "sha256:989162a9e0715c624b99ad9b2b4206765879b40eb51eef17b1e37de3e898370a" - ], - "markers": "python_version >= '3.6'", - "version": "==5.41.3" - }, - "idna": { - "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" - ], - "markers": "python_version >= '3.5'", - "version": "==3.6" - }, "importlib-metadata": { "hashes": [ - "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116", - "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d" + "sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7", + "sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67" ], - "index": "pypi", - "version": "==4.13.0" + "markers": "python_version < '3.10'", + "version": "==7.0.0" }, "itsdangerous": { "hashes": [ @@ -475,32 +136,6 @@ "markers": "python_version >= '3.7'", "version": "==3.1.2" }, - "kubernetes": { - "hashes": [ - "sha256:5854b0c508e8d217ca205591384ab58389abdae608576f9c9afc35a3c76a366c", - "sha256:e3db6800abf7e36c38d2629b5cb6b74d10988ee0cba6fba45595a7cbe60c0042" - ], - "markers": "python_version >= '3.6'", - "version": "==26.1.0" - }, - "libsass": { - "hashes": [ - "sha256:081e256ab3c5f3f09c7b8dea3bf3bf5e64a97c6995fd9eea880639b3f93a9f9a", - "sha256:3ab5ad18e47db560f4f0c09e3d28cf3bb1a44711257488ac2adad69f4f7f8425", - "sha256:5fb2297a4754a6c8e25cfe5c015a3b51a2b6b9021b333f989bb8ce9d60eb5828", - "sha256:65455a2728b696b62100eb5932604aa13a29f4ac9a305d95773c14aaa7200aaf", - "sha256:89c5ce497fcf3aba1dd1b19aae93b99f68257e5f2026b731b00a872f13324c7f", - "sha256:f1efc1b612299c88aec9e39d6ca0c266d360daa5b19d9430bdeaffffa86993f9" - ], - "markers": "python_version >= '3.6'", - "version": "==0.22.0" - }, - "markuppy": { - "hashes": [ - "sha256:1adee2c0a542af378fe84548ff6f6b0168f3cb7f426b46961038a2bcfaad0d5f" - ], - "version": "==1.14" - }, "markupsafe": { "hashes": [ "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", @@ -567,193 +202,6 @@ "markers": "python_version >= '3.7'", "version": "==2.1.3" }, - "more-itertools": { - "hashes": [ - "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced", - "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713" - ], - "markers": "python_version >= '3.5'", - "version": "==8.7.0" - }, - "numpy": { - "hashes": [ - "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f", - "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61", - "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7", - "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400", - "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef", - "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2", - "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d", - "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc", - "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835", - "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706", - "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5", - "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4", - "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6", - "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463", - "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a", - "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f", - "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e", - "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e", - "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694", - "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8", - "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64", - "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d", - "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc", - "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254", - "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2", - "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1", - "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810", - "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9" - ], - "markers": "python_version >= '3.8'", - "version": "==1.24.4" - }, - "oauthlib": { - "hashes": [ - "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", - "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" - ], - "markers": "python_version >= '3.6'", - "version": "==3.2.2" - }, - "odfpy": { - "hashes": [ - "sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec", - "sha256:fc3b8d1bc098eba4a0fda865a76d9d1e577c4ceec771426bcb169a82c5e9dfe0" - ], - "version": "==1.4.1" - }, - "openpyxl": { - "hashes": [ - "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", - "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" - ], - "version": "==3.1.2" - }, - "pandas": { - "hashes": [ - "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682", - "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc", - "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b", - "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089", - "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5", - "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26", - "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210", - "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b", - "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641", - "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd", - "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78", - "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b", - "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e", - "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061", - "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0", - "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e", - "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8", - "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d", - "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0", - "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c", - "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183", - "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df", - "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8", - "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f", - "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02" - ], - "markers": "python_version >= '3.8'", - "version": "==2.0.3" - }, - "pgeocode": { - "hashes": [ - "sha256:07995d4cd2d7fec1f82afb14d6025e83bbc156b6f225fa3e0b3417da2ec020c8", - "sha256:60fc2bad60aa161c3cf46ace4fde607b77e016b1e2a25470534163305499e55e" - ], - "markers": "python_version >= '3.8'", - "version": "==0.4.0" - }, - "phonenumbers": { - "hashes": [ - "sha256:23944f9e628f32a975d3b221b6d76e6ba8ae618d53cb3d82fc23d9e100a59b29", - "sha256:70aa98a50ba7bc7f6bf17851f806c927107e7c44e7d21eb46bdbec07b99d23ae" - ], - "index": "pypi", - "version": "==8.12.12" - }, - "pillow": { - "hashes": [ - "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d", - "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de", - "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616", - "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839", - "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099", - "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a", - "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219", - "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106", - "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b", - "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412", - "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b", - "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7", - "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2", - "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7", - "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14", - "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f", - "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27", - "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57", - "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262", - "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28", - "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610", - "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172", - "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273", - "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e", - "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d", - "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818", - "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f", - "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9", - "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01", - "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7", - "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651", - "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312", - "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80", - "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666", - "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061", - "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b", - "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992", - "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593", - "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4", - "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db", - "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba", - "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd", - "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e", - "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212", - "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb", - "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2", - "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34", - "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256", - "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f", - "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2", - "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38", - "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996", - "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a", - "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793" - ], - "markers": "python_version >= '3.8'", - "version": "==10.1.0" - }, - "pyasn1": { - "hashes": [ - "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.1" - }, - "pyasn1-modules": { - "hashes": [ - "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c", - "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==0.3.0" - }, "pydantic": { "hashes": [ "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e", @@ -796,22 +244,6 @@ "index": "pypi", "version": "==1.10.7" }, - "pyhamcrest": { - "hashes": [ - "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", - "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" - ], - "markers": "python_version >= '3.5'", - "version": "==2.0.2" - }, - "pyjwt": { - "hashes": [ - "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd", - "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14" - ], - "markers": "python_version >= '3.7'", - "version": "==2.6.0" - }, "pyotp": { "hashes": [ "sha256:346b6642e0dbdde3b4ff5a930b664ca82abfa116356ed48cc42c7d6590d36f63", @@ -827,14 +259,6 @@ ], "version": "==0.20220715.0" }, - "python-dateutil": { - "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" - }, "pytz": { "hashes": [ "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b", @@ -842,41 +266,6 @@ ], "version": "==2023.3.post1" }, - "pyyaml": { - "hashes": [ - "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", - "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", - "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", - "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", - "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", - "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", - "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", - "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", - "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", - "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", - "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", - "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", - "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", - "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", - "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", - "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", - "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", - "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", - "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", - "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", - "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", - "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", - "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", - "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", - "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", - "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", - "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", - "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", - "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==5.4.1" - }, "qrcode": { "hashes": [ "sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a", @@ -885,112 +274,6 @@ "markers": "python_version >= '3.7'", "version": "==7.4.2" }, - "rapid-router": { - "hashes": [ - "sha256:b3b0b9dd449775aaac8b6dcc05601f8e0d3d1805100ba80a086cc8a3c2661526", - "sha256:bb88bb75e0f743ebedcfd44fbbfba5e1f63e5464c2d7d7bd02d3c838486f78ac" - ], - "index": "pypi", - "version": "==5.11.3" - }, - "reportlab": { - "hashes": [ - "sha256:0b94e4f65a5f77a631cc010c9a7892d69e33f3251b760639dcc76420e138ce95", - "sha256:11a71c314183532d889ad4b3941f61c3fe4bfdda769c768a7f02d93cb69dd1bb", - "sha256:149718c3eaee937f28094325f0dd9ae1add3172c2dacbb93ff5403f37c9d3c57", - "sha256:21d6b6bcdecee9c7ce047156d0553a30d736b8172629e4c0fcacab35ba261f3b", - "sha256:269c59e508df08be498ab9e5278addb2cc16989677a03f800b17f8a31f8c5cc7", - "sha256:36568d3cb4101a210c4d821d9101635c2ef6e06bd649335938c01eb197f50c5d", - "sha256:3cb0da4975dbade6cc2ea6b0b0b17578af266dc3f669e959648f3306af993369", - "sha256:48eadd93237c7e2739525c74cf6615dd6c1a767c839f4b0d7c12167dc0b09911", - "sha256:57add04824bca89a130f9d428ace1b003cce4061386e0ec2a1b45b554ffe7aa3", - "sha256:58ea3471b9b4b8e7952bd357e8487789da11213470be328ffb3e5b7d7690c2c7", - "sha256:5a460f4c0c30bdf9d7bef46a816671a4386a9253670a53d35c694c666544261f", - "sha256:6172481e8acffcf72042653e977281fbd807a41705a39456d92d2606d8b8c5e2", - "sha256:65b441e22d8fe93154567a30662d8539e639b78142815afcaf92b388846eb3c1", - "sha256:6ea46fef07c588fef84d1164d4788fef322b39feb2bfb2df0a0706181dff79b8", - "sha256:6f75d33f7a3720cf47371ab63ced0f0ebd1aeb6db19386ae92f8977a09be9611", - "sha256:6fdac930dfdc6227720545ec44fdb396e92d53ec227a6f5ae58cc8cb9a6cbe89", - "sha256:701290747662d2b3be49fc0de33898ecc9ce3fafe0e2887d406e24693465e5ae", - "sha256:753485bb2b18cbd11340e227e4aaf9bde3bb64f83406dfa011e92ad0231a42c9", - "sha256:7b690bc30f58931b0abd47635d93a43a82d67972e83a6511cc8adbcd7da25310", - "sha256:7efdf68e97e8fea8683bfc17f25747fefbda729b9018bc2e3221658ac41ee0bd", - "sha256:7ff89011b5ee30209b3106641e3b7b4959f10aa6e9d6f3030205123c178f605d", - "sha256:8260c002e4845a5af65908d5ee2099bcc25a16c7646c5c417fa27f1e4b844bc1", - "sha256:8e4983486d419daa45cade40874bb869976e27ba11f77fb4b9ae32417284ade7", - "sha256:8f00175f8e12e6f7d3a01309de6d7008fac94a2cdce6837ad066f0961472c9e5", - "sha256:9f869286fcefa7f8e89e38448309891ff110ad74f58a7317ec204f3d4b8ad5f5", - "sha256:a0330322c6c8123745ac7667fcc6ae3e0de3b73c15bdfaa28c788a9eaa0f50da", - "sha256:a043cff1781ddb2a0ba0e8e760a79fc5be2430957c4f2a1f51bd4528cc53178f", - "sha256:a477f652e6c417ad40387a8498d9ad827421006f156aab16f67adc9b81699a72", - "sha256:a4dbc28fede7f504b9ac65ce9cbea35585e999d63f9fa68bc73f5a75b4929302", - "sha256:afb418409e0d323c6cb5e3be7ea4d14dfbf8a07eb03ab0b0062904cacf819878", - "sha256:b0d91663d450c11404ec189ebc5a4abdf20f7c4eca5954a920427cdbf5601525", - "sha256:ba6f533b262f4ee1636b754992bb2fb349df0500d765ac9be014a375c047f4db", - "sha256:bbdbba1ec3498b17eefca14d424ee90bb95b53e1423ecb22f1c17733c3406559", - "sha256:ca8eb7a6607f8a664187a330bab9f8d11c9f81ed885e063dfbb29a130944a72a", - "sha256:cca2d4c783f985b91b98e80d09ac79b6ed3f317a729cba5ba86edfe5eb9a2d9c", - "sha256:d59e62faa03003be81aa14d37ac34ea110e5ac59c8678fd4c0daa7d8b8f42096", - "sha256:d95fc8bc177a009053548c6d851a513b2147c465a5e8fea82287ea22d6825c4e", - "sha256:dbddadca6f08212732e83a60e30a42cfc7d2695892cedea208b3c3e7131c9993", - "sha256:e13a4e81761636591f5b60104f6e1eec70832ffd9aa781db68d7ebb576970d4b", - "sha256:e28a8d9cf462e2b4c9e71abd0630f9ec245d88b976b283b0dbb4602c9ddb3938", - "sha256:e5949f3b4e207fa7901c0cc3b49470b2a3372617a47dfbc892db31c2b56af296", - "sha256:e98965c6e60d76ff63989d9400ae8e65efd67c665d785b377f438f166a57c053", - "sha256:f1993a68c0edc45895d3df350d01b0456efe79aaf309cef777762742be501f2a", - "sha256:faeebde62f0f6ad86985bec5685411260393d2eb7ba907972da56af586b644e8", - "sha256:ff09a0a1e5cef05309ac09dfc5185e8151d927bcf45470d2f540c96260f8a355" - ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==3.6.13" - }, - "requests": { - "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" - ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" - }, - "requests-oauthlib": { - "hashes": [ - "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", - "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.3.1" - }, - "rsa": { - "hashes": [ - "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", - "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21" - ], - "markers": "python_version >= '3.6' and python_version < '4'", - "version": "==4.9" - }, - "setuptools": { - "hashes": [ - "sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8", - "sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592" - ], - "markers": "python_version >= '3.7'", - "version": "==62.1.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "sortedcontainers": { - "hashes": [ - "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", - "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" - ], - "version": "==2.4.0" - }, "sqlparse": { "hashes": [ "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3", @@ -999,52 +282,13 @@ "markers": "python_version >= '3.5'", "version": "==0.4.4" }, - "tablib": { - "extras": [ - "html", - "ods", - "xls", - "xlsx", - "yaml" - ], - "hashes": [ - "sha256:9821caa9eca6062ff7299fa645e737aecff982e6b2b42046928a6413c8dabfd9", - "sha256:f6661dfc45e1d4f51fa8a6239f9c8349380859a5bfaa73280645f046d6c96e33" - ], - "markers": "python_version >= '3.8'", - "version": "==3.5.0" - }, "typing-extensions": { "hashes": [ - "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", - "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" - ], - "markers": "python_version >= '3.8'", - "version": "==4.8.0" - }, - "tzdata": { - "hashes": [ - "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a", - "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda" - ], - "markers": "python_version >= '2'", - "version": "==2023.3" - }, - "urllib3": { - "hashes": [ - "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", - "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" - ], - "markers": "python_version >= '3.8'", - "version": "==2.1.0" - }, - "websocket-client": { - "hashes": [ - "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6", - "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588" + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" ], "markers": "python_version >= '3.8'", - "version": "==1.7.0" + "version": "==4.9.0" }, "werkzeug": { "hashes": [ @@ -1054,20 +298,6 @@ "markers": "python_version >= '3.8'", "version": "==3.0.1" }, - "xlrd": { - "hashes": [ - "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", - "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" - ], - "version": "==2.0.1" - }, - "xlwt": { - "hashes": [ - "sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e", - "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88" - ], - "version": "==1.3.0" - }, "zipp": { "hashes": [ "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31", @@ -1088,11 +318,11 @@ }, "astroid": { "hashes": [ - "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca", - "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e" + "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91", + "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c" ], "markers": "python_full_version >= '3.8.0'", - "version": "==3.0.1" + "version": "==3.0.2" }, "attrs": { "hashes": [ @@ -1325,11 +555,11 @@ }, "isort": { "hashes": [ - "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", - "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" + "sha256:56a51732c25f94ca96f6721be206dd96a95f42950502eb26c1015d333bc6edb7", + "sha256:aaed790b463e8703fb1eddb831dfa8e8616bacde2c083bd557ef73c8189b7263" ], "markers": "python_full_version >= '3.8.0'", - "version": "==5.12.0" + "version": "==5.13.1" }, "mccabe": { "hashes": [ @@ -1390,11 +620,11 @@ }, "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": [ @@ -1539,11 +769,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", - "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" ], "markers": "python_version >= '3.8'", - "version": "==4.8.0" + "version": "==4.9.0" }, "urllib3": { "hashes": [ diff --git a/codeforlife/models/__init__.py b/codeforlife/models/__init__.py index 4618e0cb..a20c73ae 100644 --- a/codeforlife/models/__init__.py +++ b/codeforlife/models/__init__.py @@ -88,7 +88,7 @@ def get_queryset(self): ) def filter(self, *args, **kwargs): - """A stub that return our custom queryset.""" + """A stub that returns our custom queryset.""" return t.cast( WarehouseModel.QuerySet, @@ -96,7 +96,7 @@ def filter(self, *args, **kwargs): ) def exclude(self, *args, **kwargs): - """A stub that return our custom queryset.""" + """A stub that returns our custom queryset.""" return t.cast( WarehouseModel.QuerySet, @@ -104,13 +104,21 @@ def exclude(self, *args, **kwargs): ) def all(self): - """A stub that return our custom queryset.""" + """A stub that returns our custom queryset.""" return t.cast( WarehouseModel.QuerySet, super().all(), ) + def none(self): + """A stub that returns our custom queryset.""" + + return t.cast( + WarehouseModel.QuerySet, + super().none(), + ) + objects: Manager = Manager() # Default for how long to wait before a model is deleted. diff --git a/codeforlife/permissions/__init__.py b/codeforlife/permissions/__init__.py index cc21e16d..8fb9e67c 100644 --- a/codeforlife/permissions/__init__.py +++ b/codeforlife/permissions/__init__.py @@ -1 +1,7 @@ +""" +© Ocado Group +Created on 14/12/2023 at 14:04:57(+00:00). +""" + from .is_cron_request_from_google import IsCronRequestFromGoogle +from .is_self import IsSelf diff --git a/codeforlife/permissions/is_self.py b/codeforlife/permissions/is_self.py new file mode 100644 index 00000000..174f2dff --- /dev/null +++ b/codeforlife/permissions/is_self.py @@ -0,0 +1,26 @@ +""" +© Ocado Group +Created on 12/12/2023 at 15:08:08(+00:00). +""" + +from rest_framework.permissions import BasePermission +from rest_framework.request import Request +from rest_framework.views import APIView + + +class IsSelf(BasePermission): + """Request's user must be the selected user.""" + + def __init__(self, keyword: str = "pk"): + """Initialize permission. + + Args: + keyword: The key for the url kwargs that contains the user's primary + key. + """ + + super().__init__() + self.keyword = keyword + + def has_permission(self, request: Request, view: APIView): + return request.user.pk == view.kwargs[self.keyword] diff --git a/codeforlife/tests/api.py b/codeforlife/tests/api.py index b68eb10a..c20637ae 100644 --- a/codeforlife/tests/api.py +++ b/codeforlife/tests/api.py @@ -38,8 +38,9 @@ def generic( status_code_assertion: StatusCodeAssertion = None, **extra, ): - wsgi_response = super().generic( - method, path, data, content_type, secure, **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 @@ -49,13 +50,18 @@ def generic( elif isinstance(status_code_assertion, int): expected_status_code = status_code_assertion status_code_assertion = ( - lambda status_code: status_code == expected_status_code + # pylint: disable-next=unnecessary-lambda-assignment + lambda status_code: status_code + == expected_status_code ) + + # pylint: disable=no-member assert status_code_assertion( - wsgi_response.status_code - ), f"Unexpected status code: {wsgi_response.status_code}." + response.status_code + ), f"Unexpected status code: {response.status_code}." + # pylint: enable=no-member - return wsgi_response + return response def login(self, **credentials): assert super().login( diff --git a/codeforlife/user/auth/backends/email_and_password.py b/codeforlife/user/auth/backends/email_and_password.py index 934fa29b..3b48ef8b 100644 --- a/codeforlife/user/auth/backends/email_and_password.py +++ b/codeforlife/user/auth/backends/email_and_password.py @@ -1,31 +1,35 @@ import typing as t from django.contrib.auth.backends import BaseBackend +from django.http.request import HttpRequest -from ....request import WSGIRequest from ...models import User class EmailAndPasswordBackend(BaseBackend): + """Authenticates if the password belongs to the anon user's email.""" + def authenticate( self, - request: WSGIRequest, + request: t.Optional[HttpRequest], email: t.Optional[str] = None, password: t.Optional[str] = None, **kwargs ): if email is None or password is None: - return + return None try: user = User.objects.get(email=email) if user.check_password(password): return user except User.DoesNotExist: - return + return None + + return None def get_user(self, user_id: int): try: return User.objects.get(id=user_id) except User.DoesNotExist: - return + return None diff --git a/codeforlife/user/auth/backends/otp_bypass_token.py b/codeforlife/user/auth/backends/otp_bypass_token.py index 3e20cfde..cd98bd7c 100644 --- a/codeforlife/user/auth/backends/otp_bypass_token.py +++ b/codeforlife/user/auth/backends/otp_bypass_token.py @@ -1,26 +1,29 @@ import typing as t from django.contrib.auth.backends import BaseBackend +from django.http.request import HttpRequest -from ....request import WSGIRequest from ...models import AuthFactor, User class OtpBypassTokenBackend(BaseBackend): + """Authenticates if the OTP bypass token belongs to the identified user.""" + def authenticate( self, - request: WSGIRequest, + request: t.Optional[HttpRequest], token: t.Optional[str] = None, **kwargs, ): if ( - token is None + request is None + or token is None or not isinstance(request.user, User) or not request.user.session.session_auth_factors.filter( auth_factor__type=AuthFactor.Type.OTP ).exists() ): - return + return None for otp_bypass_token in request.user.otp_bypass_tokens.all(): if otp_bypass_token.check_token(token): @@ -31,8 +34,10 @@ def authenticate( return request.user + return None + def get_user(self, user_id: int): try: return User.objects.get(id=user_id) except User.DoesNotExist: - return + return None diff --git a/codeforlife/user/auth/backends/user_id_and_login_id.py b/codeforlife/user/auth/backends/user_id_and_login_id.py index 15da47ca..26066f6e 100644 --- a/codeforlife/user/auth/backends/user_id_and_login_id.py +++ b/codeforlife/user/auth/backends/user_id_and_login_id.py @@ -1,7 +1,5 @@ import typing as t -from common.helpers.generators import get_hashed_login_id -from common.models import Student from django.contrib.auth.backends import BaseBackend from ....request import WSGIRequest diff --git a/codeforlife/user/fixtures/users.json b/codeforlife/user/fixtures/users.json index 2d3e67c0..9a623c60 100644 --- a/codeforlife/user/fixtures/users.json +++ b/codeforlife/user/fixtures/users.json @@ -4,10 +4,11 @@ "pk": 1, "fields": { "last_saved_at": "2023-01-01 00:00:00.0+00:00", + "is_active": true, "first_name": "John", "last_name": "Doe", "email": "john.doe@codeforlife.com", - "password": "password", + "password": "pbkdf2_sha256$260000$a2nFLqpwD88sOeZ7wsQskW$WIJACyDluEJWKMPsO/jawrR0sHXHmYebDJoyUihKJxU=", "teacher": 1 } }, @@ -16,10 +17,11 @@ "pk": 2, "fields": { "last_saved_at": "2023-01-01 00:00:00.0+00:00", + "is_active": true, "first_name": "Jane", "last_name": "Doe", "email": "jane.doe@codeforlife.com", - "password": "password", + "password": "pbkdf2_sha256$260000$a2nFLqpwD88sOeZ7wsQskW$WIJACyDluEJWKMPsO/jawrR0sHXHmYebDJoyUihKJxU=", "teacher": 2 } }, @@ -28,8 +30,9 @@ "pk": 3, "fields": { "last_saved_at": "2023-01-01 00:00:00.0+00:00", + "is_active": true, "first_name": "SpongeBob", - "password": "password", + "password": "pbkdf2_sha256$260000$a2nFLqpwD88sOeZ7wsQskW$WIJACyDluEJWKMPsO/jawrR0sHXHmYebDJoyUihKJxU=", "student": 1 } }, @@ -38,8 +41,9 @@ "pk": 4, "fields": { "last_saved_at": "2023-01-01 00:00:00.0+00:00", + "is_active": true, "first_name": "Patrick", - "password": "password", + "password": "pbkdf2_sha256$260000$a2nFLqpwD88sOeZ7wsQskW$WIJACyDluEJWKMPsO/jawrR0sHXHmYebDJoyUihKJxU=", "student": 2 } }, @@ -48,10 +52,11 @@ "pk": 5, "fields": { "last_saved_at": "2023-01-01 00:00:00.0+00:00", + "is_active": true, "first_name": "Indiana", "last_name": "Jones", "email": "indiana.jones@codeforlife.com", - "password": "password" + "password": "pbkdf2_sha256$260000$a2nFLqpwD88sOeZ7wsQskW$WIJACyDluEJWKMPsO/jawrR0sHXHmYebDJoyUihKJxU=" } } ] \ No newline at end of file diff --git a/codeforlife/user/models/auth_factor.py b/codeforlife/user/models/auth_factor.py index cb1d3b05..abe2b872 100644 --- a/codeforlife/user/models/auth_factor.py +++ b/codeforlife/user/models/auth_factor.py @@ -27,6 +27,7 @@ class Type(models.TextChoices): OTP = "otp", _("one-time password") + user_id: int user: "_user.User" = models.ForeignKey( # type: ignore[assignment] "user.User", related_name="auth_factors", diff --git a/codeforlife/user/models/klass.py b/codeforlife/user/models/klass.py index 578d7c0f..c640bbbe 100644 --- a/codeforlife/user/models/klass.py +++ b/codeforlife/user/models/klass.py @@ -47,12 +47,14 @@ class Manager(WarehouseModel.Manager["Class"]): ], ) + teacher_id: int teacher: "_teacher.Teacher" = models.ForeignKey( # type: ignore[assignment] "user.Teacher", related_name="classes", on_delete=models.CASCADE, ) + school_id: int school: "_school.School" = models.ForeignKey( # type: ignore[assignment] "user.School", related_name="classes", diff --git a/codeforlife/user/models/otp_bypass_token.py b/codeforlife/user/models/otp_bypass_token.py index 6fd303c8..1be993a1 100644 --- a/codeforlife/user/models/otp_bypass_token.py +++ b/codeforlife/user/models/otp_bypass_token.py @@ -77,6 +77,7 @@ def key(otp_bypass_token: OtpBypassToken): objects: Manager = Manager() + user_id: int user: "_user.User" = models.ForeignKey( # type: ignore[assignment] "user.User", related_name="otp_bypass_tokens", diff --git a/codeforlife/user/models/session.py b/codeforlife/user/models/session.py index d15c3753..6bebfd46 100644 --- a/codeforlife/user/models/session.py +++ b/codeforlife/user/models/session.py @@ -29,6 +29,7 @@ class Session(AbstractBaseSession): session_auth_factors: QuerySet["_session_auth_factor.SessionAuthFactor"] + user_id: t.Optional[int] user: t.Optional[ "_user.User" ] = models.OneToOneField( # type: ignore[assignment] diff --git a/codeforlife/user/models/session_auth_factor.py b/codeforlife/user/models/session_auth_factor.py index c0eb764a..fc89400b 100644 --- a/codeforlife/user/models/session_auth_factor.py +++ b/codeforlife/user/models/session_auth_factor.py @@ -19,12 +19,14 @@ class SessionAuthFactor(models.Model): pending, the user is not authenticated. """ + session_id: int session: "_session.Session" = models.ForeignKey( # type: ignore[assignment] "user.Session", related_name="session_auth_factors", on_delete=models.CASCADE, ) + auth_factor_id: int auth_factor: "_auth_factor.AuthFactor" = ( models.ForeignKey( # type: ignore[assignment] "user.AuthFactor", diff --git a/codeforlife/user/models/student.py b/codeforlife/user/models/student.py index 63b938dd..dea45142 100644 --- a/codeforlife/user/models/student.py +++ b/codeforlife/user/models/student.py @@ -111,6 +111,7 @@ def bulk_create_users( user: "_user.User" + school_id: int school: "_school.School" = models.ForeignKey( # type: ignore[assignment] "user.School", related_name="students", @@ -118,6 +119,7 @@ def bulk_create_users( on_delete=models.CASCADE, ) + klass_id: str klass: "_class.Class" = models.ForeignKey( # type: ignore[assignment] "user.Class", related_name="students", diff --git a/codeforlife/user/models/teacher.py b/codeforlife/user/models/teacher.py index 2d1d78ac..2710af84 100644 --- a/codeforlife/user/models/teacher.py +++ b/codeforlife/user/models/teacher.py @@ -44,6 +44,7 @@ def create_user(self, teacher: t.Dict[str, t.Any], **fields): user: "_user.User" classes: QuerySet["_class.Class"] + school_id: t.Optional[int] school: t.Optional[ "_school.School" ] = models.ForeignKey( # type: ignore[assignment] diff --git a/codeforlife/user/models/user.py b/codeforlife/user/models/user.py index 5206df39..698c07a1 100644 --- a/codeforlife/user/models/user.py +++ b/codeforlife/user/models/user.py @@ -173,6 +173,7 @@ def create_superuser(self, password: str, first_name: str, **fields): ), ) + teacher_id: t.Optional[int] teacher: t.Optional[ "_teacher.Teacher" ] = models.OneToOneField( # type: ignore[assignment] @@ -182,6 +183,7 @@ def create_superuser(self, password: str, first_name: str, **fields): on_delete=models.CASCADE, ) + student_id: t.Optional[int] student: t.Optional[ "_student.Student" ] = models.OneToOneField( # type: ignore[assignment] @@ -261,7 +263,10 @@ def is_authenticated(self): """Check if the user has any pending auth factors.""" try: - return not self.session.session_auth_factors.exists() + return ( + self.is_active + and not self.session.session_auth_factors.exists() + ) except _session.Session.DoesNotExist: return False diff --git a/codeforlife/user/permissions/__init__.py b/codeforlife/user/permissions/__init__.py index cb7689b5..66153640 100644 --- a/codeforlife/user/permissions/__init__.py +++ b/codeforlife/user/permissions/__init__.py @@ -1,2 +1,10 @@ -from .is_school_member import IsSchoolMember -from .is_school_teacher import IsSchoolTeacher +""" +© Ocado Group +Created on 14/12/2023 at 14:05:06(+00:00). +""" + +from .in_class import InClass +from .in_school import InSchool +from .is_independent import IsIndependent +from .is_student import IsStudent +from .is_teacher import IsTeacher diff --git a/codeforlife/user/permissions/in_class.py b/codeforlife/user/permissions/in_class.py new file mode 100644 index 00000000..2a0229d6 --- /dev/null +++ b/codeforlife/user/permissions/in_class.py @@ -0,0 +1,43 @@ +""" +© Ocado Group +Created on 12/12/2023 at 15:18:10(+00:00). +""" + +import typing as t + +from rest_framework.permissions import BasePermission +from rest_framework.request import Request +from rest_framework.views import APIView + +from ..models import User + + +class InClass(BasePermission): + """Request's user must be in a class.""" + + def __init__(self, class_id: t.Optional[str] = None): + """Initialize permission. + + Args: + class_id: A class' ID. If None, check if user is in any class. + Else, check if user is in the specific class. + """ + + super().__init__() + self.class_id = class_id + + def has_permission(self, request: Request, view: APIView): + user = request.user + if isinstance(user, User): + if user.teacher is not None: + classes = user.teacher.classes + if self.class_id is not None: + classes = classes.filter(id=self.class_id) + return classes.exists() + + if user.student is not None: + if self.class_id is None: + return True + return user.student.klass_id == self.class_id + + return False diff --git a/codeforlife/user/permissions/in_school.py b/codeforlife/user/permissions/in_school.py new file mode 100644 index 00000000..6a0beded --- /dev/null +++ b/codeforlife/user/permissions/in_school.py @@ -0,0 +1,41 @@ +""" +© Ocado Group +Created on 12/12/2023 at 15:18:27(+00:00). +""" + +import typing as t + +from rest_framework.permissions import BasePermission +from rest_framework.request import Request +from rest_framework.views import APIView + +from ..models import User + + +class InSchool(BasePermission): + """Request's user must be in a school.""" + + def __init__(self, school_id: t.Optional[int] = None): + """Initialize permission. + + Args: + school_id: A school's ID. If None, check if user is in any school. + Else, check if user is in the specific school. + """ + + super().__init__() + self.school_id = school_id + + def has_permission(self, request: Request, view: APIView): + def in_school(school_id: int): + return self.school_id is None or self.school_id == school_id + + user = request.user + return isinstance(user, User) and ( + ( + user.teacher is not None + and user.teacher.school_id is not None + and in_school(user.teacher.school_id) + ) + or (user.student is not None and in_school(user.student.school_id)) + ) diff --git a/codeforlife/user/permissions/is_independent.py b/codeforlife/user/permissions/is_independent.py new file mode 100644 index 00000000..637a85ae --- /dev/null +++ b/codeforlife/user/permissions/is_independent.py @@ -0,0 +1,22 @@ +""" +© Ocado Group +Created on 12/12/2023 at 13:55:47(+00:00). +""" + +from rest_framework.permissions import BasePermission +from rest_framework.request import Request +from rest_framework.views import APIView + +from ..models import User + + +class IsIndependent(BasePermission): + """Request's user must be independent.""" + + def has_permission(self, request: Request, view: APIView): + user = request.user + return ( + isinstance(user, User) + and user.teacher is None + and user.student is None + ) diff --git a/codeforlife/user/permissions/is_school_member.py b/codeforlife/user/permissions/is_school_member.py deleted file mode 100644 index 43894cdf..00000000 --- a/codeforlife/user/permissions/is_school_member.py +++ /dev/null @@ -1,18 +0,0 @@ -from rest_framework.permissions import BasePermission -from rest_framework.request import Request -from rest_framework.views import View - -from ..models import User - - -class IsSchoolMember(BasePermission): - def has_permission(self, request: Request, view: View): - user = request.user - return isinstance(user, User) and ( - (user.is_teacher and user.teacher.school is not None) - or ( - user.student is not None - # TODO: should be user.student.school is not None - and user.student.class_field is not None - ) - ) diff --git a/codeforlife/user/permissions/is_school_teacher.py b/codeforlife/user/permissions/is_school_teacher.py deleted file mode 100644 index ece94675..00000000 --- a/codeforlife/user/permissions/is_school_teacher.py +++ /dev/null @@ -1,15 +0,0 @@ -from rest_framework.permissions import BasePermission -from rest_framework.request import Request -from rest_framework.views import View - -from ..models import User - - -class IsSchoolTeacher(BasePermission): - def has_permission(self, request: Request, view: View): - user = request.user - return ( - isinstance(user, User) - and user.is_teacher - and user.teacher.school is not None - ) diff --git a/codeforlife/user/permissions/is_student.py b/codeforlife/user/permissions/is_student.py new file mode 100644 index 00000000..1f217087 --- /dev/null +++ b/codeforlife/user/permissions/is_student.py @@ -0,0 +1,35 @@ +""" +© Ocado Group +Created on 12/12/2023 at 13:55:40(+00:00). +""" + +import typing as t + +from rest_framework.permissions import BasePermission +from rest_framework.request import Request +from rest_framework.views import APIView + +from ..models import User + + +class IsStudent(BasePermission): + """Request's user must be a student.""" + + def __init__(self, student_id: t.Optional[int] = None): + """Initialize permission. + + Args: + student_id: A student's ID. If None, check if the user is any + student. Else, check if the user is the specific student. + """ + + super().__init__() + self.student_id = student_id + + def has_permission(self, request: Request, view: APIView): + user = request.user + return ( + isinstance(user, User) + and user.student_id is not None + and (self.student_id is None or user.student_id == self.student_id) + ) diff --git a/codeforlife/user/permissions/is_teacher.py b/codeforlife/user/permissions/is_teacher.py new file mode 100644 index 00000000..7355b5e2 --- /dev/null +++ b/codeforlife/user/permissions/is_teacher.py @@ -0,0 +1,47 @@ +""" +© Ocado Group +Created on 12/12/2023 at 13:55:22(+00:00). +""" + +import typing as t + +from rest_framework.permissions import BasePermission +from rest_framework.request import Request +from rest_framework.views import APIView + +from ..models import Teacher, User + + +class IsTeacher(BasePermission): + """Request's user must be a teacher.""" + + def __init__( + self, + teacher_id: t.Optional[int] = None, + is_admin: t.Optional[bool] = None, + ): + """Initialize permission. + + Args: + teacher_id: A teacher's ID. If None, check if the user is any + teacher. Else, check if the user is the specific teacher. + is_admin: If the teacher is an admin. If None, don't check if the + teacher is an admin. Else, check if the teacher is (not) an + admin. + """ + + super().__init__() + self.teacher_id = teacher_id + self.is_admin = is_admin + + def has_permission(self, request: Request, view: APIView): + user = request.user + return ( + isinstance(user, User) + and user.teacher_id is not None + and (self.teacher_id is None or user.teacher_id == self.teacher_id) + and ( + self.is_admin is None + or t.cast(Teacher, user.teacher).is_admin == self.is_admin + ) + ) diff --git a/codeforlife/user/serializers/user.py b/codeforlife/user/serializers/user.py index 5b3e9934..da78c7c6 100644 --- a/codeforlife/user/serializers/user.py +++ b/codeforlife/user/serializers/user.py @@ -6,8 +6,8 @@ class UserSerializer(serializers.ModelSerializer): - student = StudentSerializer(source="new_student") - teacher = TeacherSerializer(source="new_teacher") + student = StudentSerializer() + teacher = TeacherSerializer() class Meta: model = User diff --git a/codeforlife/user/tests/permissions/__init__.py b/codeforlife/user/tests/permissions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/codeforlife/user/tests/permissions/test_in_class.py b/codeforlife/user/tests/permissions/test_in_class.py new file mode 100644 index 00000000..baf0e14c --- /dev/null +++ b/codeforlife/user/tests/permissions/test_in_class.py @@ -0,0 +1,97 @@ +""" +© Ocado Group +Created on 14/12/2023 at 14:26:20(+00:00). +""" + +from rest_framework.views import APIView + +from ....tests import APITestCase +from ...permissions import InClass + + +class TestInClass(APITestCase): + """ + Naming convention: + test_{class_id}__{user_type} + + class_id: The id of a class. Options: + - any_class: Any class. + - in_class: A specific class the user is in. + - not_in_class: A specific class the user is not in. + + user_type: The type of user. Options: + - non_class_teacher: A teacher not in a class. + - class_teacher: A teacher in a class. + - student: A student. + - indy: An independent. + """ + + def test_any_class__non_class_teacher(self): + """ + Teacher without a class is not in any class. + """ + + raise NotImplementedError() # TODO + + def test_any_class__class_teacher(self): + """ + Teacher with a class is in any class. + """ + + raise NotImplementedError() # TODO + + def test_any_class__student(self): + """ + Student is in any class. + """ + + raise NotImplementedError() # TODO + + def test_any_class__indy(self): + """ + Independent is not in any class. + """ + + raise NotImplementedError() # TODO + + def test_not_in_class__non_class_teacher(self): + """ + Teacher without a class is not in a specific class. + """ + + raise NotImplementedError() # TODO + + def test_not_in_class__class_teacher(self): + """ + Teacher with a class is not in a specific class. + """ + + raise NotImplementedError() # TODO + + def test_not_in_class__student(self): + """ + Student is not in a specific class. + """ + + raise NotImplementedError() # TODO + + def test_not_in_class__indy(self): + """ + Independent is not in a specific class. + """ + + raise NotImplementedError() # TODO + + def test_in_class__class_teacher(self): + """ + Teacher with a class is in a specific class. + """ + + raise NotImplementedError() # TODO + + def test_in_class__student(self): + """ + Student is in a specific class. + """ + + raise NotImplementedError() # TODO diff --git a/codeforlife/user/tests/permissions/test_in_school.py b/codeforlife/user/tests/permissions/test_in_school.py new file mode 100644 index 00000000..4ef30fe7 --- /dev/null +++ b/codeforlife/user/tests/permissions/test_in_school.py @@ -0,0 +1,29 @@ +""" +© Ocado Group +Created on 14/12/2023 at 14:26:20(+00:00). +""" + +from rest_framework.views import APIView + +from ....tests import APITestCase +from ...permissions import InSchool + + +class TestInSchool(APITestCase): + """ + Naming convention: + test_{school_id}__{user_type} + + school_id: The id of a school. Options: + - any_school: Any school. + - in_school: A specific school the user is in. + - not_in_school: A specific school the user is not in. + + user_type: The type of user. Options: + - non_school_teacher: A teacher not in a school. + - school_teacher: A teacher in a school. + - student: A student. + - indy: An independent. + """ + + # TODO: define unit tests diff --git a/codeforlife/user/tests/permissions/test_is_independent.py b/codeforlife/user/tests/permissions/test_is_independent.py new file mode 100644 index 00000000..6270fafe --- /dev/null +++ b/codeforlife/user/tests/permissions/test_is_independent.py @@ -0,0 +1,42 @@ +""" +© Ocado Group +Created on 14/12/2023 at 14:26:20(+00:00). +""" + +from rest_framework.views import APIView + +from ....tests import APITestCase +from ...permissions import IsIndependent + + +class TestIsIndependent(APITestCase): + """ + Naming convention: + test_{user_type} + + user_type: The type of user. Options: + - teacher: A teacher. + - student: A student. + - indy: An independent. + """ + + def test_teacher(self): + """ + Teacher is not any independent. + """ + + raise NotImplementedError() # TODO + + def test_student(self): + """ + Student is not any independent. + """ + + raise NotImplementedError() # TODO + + def test_indy(self): + """ + Independent is any independent. + """ + + raise NotImplementedError() # TODO diff --git a/codeforlife/user/tests/permissions/test_is_student.py b/codeforlife/user/tests/permissions/test_is_student.py new file mode 100644 index 00000000..b2aa4583 --- /dev/null +++ b/codeforlife/user/tests/permissions/test_is_student.py @@ -0,0 +1,76 @@ +""" +© Ocado Group +Created on 14/12/2023 at 14:26:20(+00:00). +""" + +from rest_framework.views import APIView + +from ....tests import APITestCase +from ...permissions import IsStudent + + +class TestIsStudent(APITestCase): + """ + Naming convention: + test_{user_type}__{student_id} + + user_type: The type of user. Options: + - teacher: A teacher. + - student: A student. + - indy: An independent. + + student_id: The ID of a student. Options: + - any_student: User is any student. + - not_any_student: User is not any student. + - specific_student: User is a specific student. + - not_specific_student User is not a specific student. + """ + + def test_teacher__not_any_student(self): + """ + Teacher is not any student. + """ + + raise NotImplementedError() # TODO + + def test_teacher__not_specific_student(self): + """ + Teacher is not a specific student. + """ + + raise NotImplementedError() # TODO + + def test_indy__not_any_student(self): + """ + Independent is not any student. + """ + + raise NotImplementedError() # TODO + + def test_indy__not_specific_student(self): + """ + Independent is not a specific student. + """ + + raise NotImplementedError() # TODO + + def test_student__any_student(self): + """ + Student is any student. + """ + + raise NotImplementedError() # TODO + + def test_student__specific_student(self): + """ + Student is a specific student. + """ + + raise NotImplementedError() # TODO + + def test_student__not_specific_student(self): + """ + Student is not a specific student. + """ + + raise NotImplementedError() # TODO diff --git a/codeforlife/user/tests/permissions/test_is_teacher.py b/codeforlife/user/tests/permissions/test_is_teacher.py new file mode 100644 index 00000000..feec8a4c --- /dev/null +++ b/codeforlife/user/tests/permissions/test_is_teacher.py @@ -0,0 +1,137 @@ +""" +© Ocado Group +Created on 14/12/2023 at 14:26:20(+00:00). +""" + +from rest_framework.test import APIRequestFactory +from rest_framework.views import APIView + +from ....tests import APITestCase +from ...models import User +from ...permissions import IsTeacher + + +class TestIsTeacher(APITestCase): + # TODO: test that students and independents do not get permission. + """ + Naming convention: + test_{teacher_id}__{is_admin} + + teacher_id: The id of a teacher. Options: + - id: A specific teacher. + - none: Any teacher. + + is_admin: A flag for if the teacher is an admin. Options: + - true: Teacher is an admin. + - false: Teacher is a non-admin. + - none: Teacher is an admin or a non-admin. + """ + + fixtures = [ + "users", + "teachers", + "schools", + "classes", + "students", + ] + + def setUp(self): + self.user__1 = User.objects.get(pk=1) + self.user__2 = User.objects.get(pk=2) + + assert self.user__1.teacher and self.user__1.teacher.is_admin + assert self.user__2.teacher and not self.user__2.teacher.is_admin + + self.user__1__teacher = self.user__1.teacher + self.user__2__teacher = self.user__2.teacher + + request_factory = APIRequestFactory() + self.request__1 = request_factory.get("/") + self.request__1.user = self.user__1 + self.request__2 = request_factory.get("/") + self.request__2.user = self.user__2 + + def test_none__none(self): + """ + Is any teacher. + """ + + assert IsTeacher( + teacher_id=None, + is_admin=None, + ).has_permission(self.request__1, APIView()) + + def test_none__true(self): + """ + Is any admin teacher. + """ + + assert IsTeacher( + teacher_id=None, + is_admin=True, + ).has_permission(self.request__1, APIView()) + + assert not IsTeacher( + teacher_id=None, + is_admin=True, + ).has_permission(self.request__2, APIView()) + + def test_none__false(self): + """ + Is any non-admin teacher. + """ + + assert not IsTeacher( + teacher_id=None, + is_admin=False, + ).has_permission(self.request__1, APIView()) + + assert IsTeacher( + teacher_id=None, + is_admin=False, + ).has_permission(self.request__2, APIView()) + + def test_id__none(self): + """ + Is a specific teacher. + """ + + assert IsTeacher( + teacher_id=self.user__1__teacher.id, + is_admin=None, + ).has_permission(self.request__1, APIView()) + + assert not IsTeacher( + teacher_id=self.user__2__teacher.id, + is_admin=None, + ).has_permission(self.request__1, APIView()) + + def test_id__true(self): + """ + Is a specific admin teacher. + """ + + assert IsTeacher( + teacher_id=self.user__1__teacher.id, + is_admin=True, + ).has_permission(self.request__1, APIView()) + + assert not IsTeacher( + teacher_id=self.user__2__teacher.id, + is_admin=True, + ).has_permission(self.request__2, APIView()) + + def test_id__false(self): + """ + Is a specific non-admin teacher. + """ + + assert not IsTeacher( + teacher_id=self.user__1__teacher.id, + is_admin=False, + ).has_permission(self.request__1, APIView()) + + assert IsTeacher( + teacher_id=self.user__2__teacher.id, + is_admin=False, + ).has_permission(self.request__2, APIView()) diff --git a/codeforlife/user/tests/views/test_klass.py b/codeforlife/user/tests/views/test_klass.py index 5118ca5c..76073833 100644 --- a/codeforlife/user/tests/views/test_klass.py +++ b/codeforlife/user/tests/views/test_klass.py @@ -19,6 +19,7 @@ def _login_student(self): password="Password1", ) + # pylint: disable-next=pointless-string-statement """ Retrieve naming convention: test_retrieve__{user_type}__{same_school}__{in_class} diff --git a/codeforlife/user/tests/views/test_school.py b/codeforlife/user/tests/views/test_school.py index 50d332ee..b961be62 100644 --- a/codeforlife/user/tests/views/test_school.py +++ b/codeforlife/user/tests/views/test_school.py @@ -4,7 +4,7 @@ from rest_framework.permissions import IsAuthenticated from ....tests import APIClient, APITestCase -from ...models import Class, School, Student, Teacher, User, UserProfile +from ...models import Class, School, Student, Teacher, User from ...serializers import SchoolSerializer from ...views import SchoolViewSet @@ -18,48 +18,13 @@ class TestSchoolViewSet(APITestCase): https://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions """ - # TODO: replace this setup with data fixtures. - def setUp(self): - school = School.objects.create( - name="ExampleSchool", - country="UK", - ) - - user = User.objects.create( - first_name="Example", - last_name="Teacher", - email="example.teacher@codeforlife.com", - username="example.teacher@codeforlife.com", - ) - - user_profile = UserProfile.objects.create(user=user) - - teacher = Teacher.objects.create( - user=user_profile, - new_user=user, - school=school, - ) - - klass = Class.objects.create( - name="ExampleClass", - teacher=teacher, - created_by=teacher, - ) - - user = User.objects.create( - first_name="Example", - last_name="Student", - email="example.student@codeforlife.com", - username="example.student@codeforlife.com", - ) - - user_profile = UserProfile.objects.create(user=user) - - Student.objects.create( - class_field=klass, - user=user_profile, - new_user=user, - ) + fixtures = [ + "users", + "teachers", + "schools", + "classes", + "students", + ] def _login_teacher(self): return self.client.login_teacher( @@ -80,6 +45,7 @@ def _login_indy_student(self): password="Password1", ) + # pylint: disable-next=pointless-string-statement """ Retrieve naming convention: test_retrieve__{user_type}__{same_school} @@ -172,6 +138,7 @@ def test_retrieve__student__not_same_school(self): status_code_assertion=status.HTTP_404_NOT_FOUND, ) + # pylint: disable-next=pointless-string-statement """ List naming convention: test_list__{user_type} @@ -224,6 +191,7 @@ def test_list__student(self): self._list_schools([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..654a8e4e 100644 --- a/codeforlife/user/tests/views/test_user.py +++ b/codeforlife/user/tests/views/test_user.py @@ -1,10 +1,15 @@ +""" +© Ocado Group +Created on 14/12/2023 at 14:05:48(+00:00). +""" + import typing as t from rest_framework import status from rest_framework.permissions import IsAuthenticated from ....tests import APIClient, APITestCase -from ...models import Class, School, Student, Teacher, User, UserProfile +from ...models import User from ...serializers import UserSerializer from ...views import UserViewSet @@ -18,60 +23,25 @@ class TestUserViewSet(APITestCase): https://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions """ - # TODO: replace this setup with data fixtures. - def setUp(self): - school = School.objects.create( - name="ExampleSchool", - country="UK", - ) - - user = User.objects.create( - first_name="Example", - last_name="Teacher", - email="example.teacher@codeforlife.com", - username="example.teacher@codeforlife.com", - ) - - user_profile = UserProfile.objects.create(user=user) - - teacher = Teacher.objects.create( - user=user_profile, - new_user=user, - school=school, - ) - - klass = Class.objects.create( - name="ExampleClass", - teacher=teacher, - created_by=teacher, - ) - - user = User.objects.create( - first_name="Example", - last_name="Student", - email="example.student@codeforlife.com", - username="example.student@codeforlife.com", - ) - - user_profile = UserProfile.objects.create(user=user) - - Student.objects.create( - class_field=klass, - user=user_profile, - new_user=user, - ) + fixtures = [ + "users", + "teachers", + "schools", + "classes", + "students", + ] def _login_teacher(self): return self.client.login_teacher( - email="maxplanck@codeforlife.com", - password="Password1", + email="jane.doe@codeforlife.com", + password="password", is_admin=False, ) def _login_admin_teacher(self): return self.client.login_teacher( - email="alberteinstein@codeforlife.com", - password="Password1", + email="john.doe@codeforlife.com", + password="password", is_admin=True, ) @@ -87,6 +57,7 @@ def _login_indy_student(self): password="Password1", ) + # pylint: disable-next=pointless-string-statement """ Retrieve naming convention: test_retrieve__{user_type}__{other_user_type}__{same_school}__{same_class} @@ -466,6 +437,7 @@ def test_retrieve__indy_student__student(self): status_code_assertion=status.HTTP_404_NOT_FOUND, ) + # pylint: disable-next=pointless-string-statement """ List naming convention: test_list__{user_type}__{filters} @@ -540,6 +512,7 @@ def test_list__indy_student(self): self._list_users([user]) + # pylint: disable-next=pointless-string-statement """ General tests that apply to all actions. """ diff --git a/codeforlife/user/views/klass.py b/codeforlife/user/views/klass.py index bc863e88..ff9a0f20 100644 --- a/codeforlife/user/views/klass.py +++ b/codeforlife/user/views/klass.py @@ -2,15 +2,16 @@ from rest_framework.viewsets import ModelViewSet from ..models import Class, User -from ..permissions import IsSchoolMember +from ..permissions import InSchool from ..serializers import ClassSerializer +# pylint: disable-next=missing-class-docstring,too-many-ancestors class ClassViewSet(ModelViewSet): http_method_names = ["get"] lookup_field = "access_code" serializer_class = ClassSerializer - permission_classes = [IsAuthenticated, IsSchoolMember] + permission_classes = [IsAuthenticated, InSchool] def get_queryset(self): user: User = self.request.user diff --git a/codeforlife/user/views/school.py b/codeforlife/user/views/school.py index 0c12d9cd..dc9fb657 100644 --- a/codeforlife/user/views/school.py +++ b/codeforlife/user/views/school.py @@ -2,14 +2,15 @@ from rest_framework.viewsets import ModelViewSet from ..models import School, User -from ..permissions import IsSchoolMember +from ..permissions import InSchool from ..serializers import SchoolSerializer +# pylint: disable-next=missing-class-docstring,too-many-ancestors class SchoolViewSet(ModelViewSet): http_method_names = ["get"] serializer_class = SchoolSerializer - permission_classes = [IsAuthenticated, IsSchoolMember] + permission_classes = [IsAuthenticated, InSchool] def get_queryset(self): user: User = self.request.user diff --git a/codeforlife/user/views/user.py b/codeforlife/user/views/user.py index 25b0ba37..000b32be 100644 --- a/codeforlife/user/views/user.py +++ b/codeforlife/user/views/user.py @@ -1,3 +1,8 @@ +""" +© Ocado Group +Created on 14/12/2023 at 14:06:05(+00:00). +""" + from rest_framework.viewsets import ModelViewSet from ..filters import UserFilterSet @@ -5,33 +10,39 @@ from ..serializers import UserSerializer +# pylint: disable-next=missing-class-docstring,too-many-ancestors class UserViewSet(ModelViewSet): http_method_names = ["get"] serializer_class = UserSerializer filterset_class = UserFilterSet def get_queryset(self): - user: User = self.request.user - if user.is_student: - if user.student.class_field is None: - return User.objects.filter(id=user.id) - - return User.objects.filter( - new_student__class_field=user.student.class_field - ) - - teachers = User.objects.filter( - new_teacher__school=user.teacher.school_id - ) - students = ( - User.objects.filter( - # TODO: add school foreign key to student model. - new_student__class_field__teacher__school=user.teacher.school_id, - ) - if user.teacher.is_admin - else User.objects.filter( - new_student__class_field__teacher=user.teacher - ) - ) - - return teachers | students + user = self.request.user + if not isinstance(user, User): + return User.objects.none() + + if user.student is not None: + return User.objects.filter(student__klass_id=user.student.klass_id) + + if user.teacher is not None: + teachers = User.objects.none() + students = User.objects.none() + + if user.teacher.school_id is not None: + teachers = User.objects.filter( + teacher__school_id=user.teacher.school_id + ) + + students = ( + User.objects.filter( + student__school_id=user.teacher.school_id + ) + if user.teacher.is_admin + else User.objects.filter( + student__klass__teacher=user.teacher + ) + ) + + return teachers | students + + return User.objects.filter(id=user.id)