diff --git a/backend/Pipfile b/backend/Pipfile index a3aebb38..56b0c24c 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -28,7 +28,7 @@ google-cloud-logging = "==1.*" google-auth = "==2.*" google-cloud-container = "==2.3.0" "django-anymail[amazon_ses]" = "==7.0.*" -codeforlife = {ref = "v0.8.0", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} +codeforlife = {ref = "v0.8.4", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} django = "==3.2.20" djangorestframework = "==3.13.1" django-cors-headers = "==4.1.0" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 65945d09..bff9243b 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "14a2ab3615122f1a23dd9ebedbbfd73df5a9e66a9162984b02a67e95b6f17293" + "sha256": "a303e645554c35938800ea83c5654e6a5a99df0c49298176ddefab6b8badadb1" }, "pipfile-spec": 6, "requires": { @@ -66,84 +66,99 @@ }, "charset-normalizer": { "hashes": [ - "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96", - "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c", - "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710", - "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706", - "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020", - "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252", - "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad", - "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329", - "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a", - "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f", - "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6", - "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4", - "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a", - "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46", - "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2", - "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23", - "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace", - "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd", - "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982", - "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10", - "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2", - "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea", - "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09", - "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5", - "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149", - "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489", - "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9", - "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80", - "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592", - "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3", - "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6", - "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed", - "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c", - "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200", - "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a", - "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e", - "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d", - "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6", - "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623", - "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669", - "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3", - "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa", - "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9", - "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2", - "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f", - "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1", - "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4", - "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a", - "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8", - "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3", - "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029", - "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f", - "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959", - "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22", - "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7", - "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952", - "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346", - "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e", - "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d", - "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299", - "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd", - "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a", - "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3", - "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037", - "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94", - "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c", - "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858", - "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a", - "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449", - "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c", - "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918", - "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1", - "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c", - "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", - "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" + "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" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.2.0" + "version": "==3.3.0" }, "click": { "hashes": [ @@ -155,7 +170,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "5fb23069bb2ca1ccd9a1e9e5d3db1841d2758fd1" + "ref": "becfb0c3deb9057264a5acfbb9d4590f45f01057" }, "codeforlife-portal": { "hashes": [ @@ -232,6 +247,14 @@ "index": "pypi", "version": "==3.7" }, + "django-filter": { + "hashes": [ + "sha256:2fe15f78108475eda525692813205fa6f9e8c1caf1ae65daa5862d403c6dbf00", + "sha256:d12d8e0fc6d3eb26641e553e5d53b191eb8cec611427d4bdce0becb1f7c172b5" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, "django-formtools": { "hashes": [ "sha256:304fa777b8ef9e0693ce7833f885cb89ba46b0e46fc23b01176900a93f46742f", @@ -375,11 +398,11 @@ }, "google-auth": { "hashes": [ - "sha256:9800802266366a2a87890fb2d04923fc0c0d4368af0b86db18edd94a62386ea1", - "sha256:d38bdf4fa1e7c5a35e574861bce55784fd08afadb4e48f99f284f1e487ce702d" + "sha256:6864247895eea5d13b9c57c9e03abb49cb94ce2dc7c58e91cba3248c7477c9e3", + "sha256:a8f4608e65c244ead9e0538f181a96c6e11199ec114d41f1d7b1bffa96937bda" ], "index": "pypi", - "version": "==2.23.1" + "version": "==2.23.3" }, "google-cloud-container": { "hashes": [ @@ -407,81 +430,79 @@ }, "googleapis-common-protos": { "hashes": [ - "sha256:69f9bbcc6acde92cab2db95ce30a70bd2b81d20b12eff3f1aabaffcbe8a93918", - "sha256:e73ebb404098db405ba95d1e1ae0aa91c3e15a71da031a2eeb6b2e23e7bc3708" + "sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0", + "sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b" ], "markers": "python_version >= '3.7'", - "version": "==1.60.0" + "version": "==1.61.0" }, "greenlet": { "hashes": [ - "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a", - "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a", - "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1", - "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43", - "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33", - "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8", - "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088", - "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca", - "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343", - "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645", - "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db", - "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df", - "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3", - "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86", - "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2", - "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a", - "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf", - "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7", - "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394", - "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40", - "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3", - "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6", - "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74", - "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0", - "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3", - "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91", - "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5", - "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9", - "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417", - "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8", - "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b", - "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6", - "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb", - "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73", - "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b", - "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df", - "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9", - "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f", - "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0", - "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857", - "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a", - "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249", - "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30", - "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292", - "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b", - "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d", - "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b", - "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c", - "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca", - "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7", - "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75", - "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae", - "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47", - "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b", - "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470", - "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c", - "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564", - "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9", - "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099", - "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0", - "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5", - "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19", - "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1", - "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526" + "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" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.0.2" + "markers": "python_version >= '3.7'", + "version": "==3.0.0" }, "grpc-google-iam-v1": { "hashes": [ @@ -607,39 +628,40 @@ }, "libcst": { "hashes": [ - "sha256:0138068baf09561268c7f079373bda45f0e2b606d2d19df1307ca8a5134fc465", - "sha256:119ba709f1dcb785a4458cf36cedb51d6f9cb2eec0acd7bb171f730eac7cb6ce", - "sha256:1adcfa7cafb6a0d39a1a0bec541355608038b45815e0c5019c95f91921d42884", - "sha256:37187337f979ba426d8bfefc08008c3c1b09b9e9f9387050804ed2da88107570", - "sha256:414350df5e334ddf0db1732d63da44e81b734d45abe1c597b5e5c0dd46aa4156", - "sha256:440887e5f82efb299f2e98d4bfa5663851a878cfc0efed652ab8c50205191436", - "sha256:47dba43855e9c7b06d8b256ee81f0ebec6a4f43605456519577e09dfe4b4288c", - "sha256:4840a3de701778f0a19582bb3085c61591329153f801dc25da84689a3733960b", - "sha256:4b4e336f6d68456017671cdda8ddebf9caebce8052cc21a3f494b03d7bd28386", - "sha256:5599166d5fec40e18601fb8868519dde99f77b6e4ad6074958018f9545da7abd", - "sha256:5e3293e77657ba62533553bb9f0c5fb173780e164c65db1ea2a3e0d03944a284", - "sha256:600c4d3a9a2f75d5a055fed713a5a4d812709947909610aa6527abe08a31896f", - "sha256:6caa33430c0c7a0fcad921b0deeec61ddb96796b6f88dca94966f6db62065f4f", - "sha256:80423311f09fc5fc3270ede44d30d9d8d3c2d3dd50dbf703a581ca7346949fa6", - "sha256:8420926791b0b6206cb831a7ec73d26ae820e65bdf07ce9813c7754c7722c07a", - "sha256:8c50541c3fd6b1d5a3765c4bb5ee8ecbba9d0e798e48f79fd5adf3b6752de4d0", - "sha256:8d31ce2790eab59c1bd8e33fe72d09cfc78635c145bdc3f08296b360abb5f443", - "sha256:967c66fabd52102954207bf1541312b467afc210fdf7033f32da992fb6c2372c", - "sha256:9a4931feceab171e6fce73de94e13880424367247dad6ff2b49cabfec733e144", - "sha256:9d6dec2a3c443792e6af7c36fadc256e4ea586214c76b52f0d18118811dbe351", - "sha256:a6b5aea04c35e13109edad3cf83bc6dcd74309b150a781d2189eecb288b73a87", - "sha256:ae49dcbfadefb82e830d41d9f0a1db0af3b771224768f431f1b7b3a9803ed7e3", - "sha256:ae7f4e71d714f256b5f2ff98b5a9effba0f9dff4d779d8f35d7eb157bef78f59", - "sha256:b0533de4e35396c61aeb3a6266ac30369a855910c2385aaa902ff4aabd60d409", - "sha256:b666a605f4205c8357696f3b6571a38f6a8537cdcbb8f357587d35168298af34", - "sha256:b97f652b15c50e91df411a9c8d5e6f75882b30743a49b387dcedd3f68ed94d75", - "sha256:c90c74a8a314f0774f045122323fb60bacba79cbf5f71883c0848ecd67179541", - "sha256:d237e9164a43caa7d6765ee560412264484e7620c546a2ee10a8d01bd56884e0", - "sha256:ddd4e0eeec499d1c824ab545e62e957dbbd69a16bc4273208817638eb7d6b3c6", - "sha256:f2cb687e1514625e91024e50a5d2e485c0ad3be24f199874ebf32b5de0346150" + "sha256:003e5e83a12eed23542c4ea20fdc8de830887cc03662432bb36f84f8c4841b81", + "sha256:0acbacb9a170455701845b7e940e2d7b9519db35a86768d86330a0b0deae1086", + "sha256:0bf69cbbab5016d938aac4d3ae70ba9ccb3f90363c588b3b97be434e6ba95403", + "sha256:2d37326bd6f379c64190a28947a586b949de3a76be00176b0732c8ee87d67ebe", + "sha256:3a07ecfabbbb8b93209f952a365549e65e658831e9231649f4f4e4263cad24b1", + "sha256:3ebbb9732ae3cc4ae7a0e97890bed0a57c11d6df28790c2b9c869f7da653c7c7", + "sha256:4bc745d0c06420fe2644c28d6ddccea9474fb68a2135904043676deb4fa1e6bc", + "sha256:5297a16e575be8173185e936b7765c89a3ca69d4ae217a4af161814a0f9745a7", + "sha256:5f1cd308a4c2f71d5e4eec6ee693819933a03b78edb2e4cc5e3ad1afd5fb3f07", + "sha256:63f75656fd733dc20354c46253fde3cf155613e37643c3eaf6f8818e95b7a3d1", + "sha256:73c086705ed34dbad16c62c9adca4249a556c1b022993d511da70ea85feaf669", + "sha256:75816647736f7e09c6120bdbf408456f99b248d6272277eed9a58cf50fb8bc7d", + "sha256:78b7a38ec4c1c009ac39027d51558b52851fb9234669ba5ba62283185963a31c", + "sha256:7ccaf53925f81118aeaadb068a911fac8abaff608817d7343da280616a5ca9c1", + "sha256:82d1271403509b0a4ee6ff7917c2d33b5a015f44d1e208abb1da06ba93b2a378", + "sha256:8ae11eb1ea55a16dc0cdc61b41b29ac347da70fec14cc4381248e141ee2fbe6c", + "sha256:8afb6101b8b3c86c5f9cec6b90ab4da16c3c236fe7396f88e8b93542bb341f7c", + "sha256:8c1f2da45f1c45634090fd8672c15e0159fdc46853336686959b2d093b6e10fa", + "sha256:97fbc73c87e9040e148881041fd5ffa2a6ebf11f64b4ccb5b52e574b95df1a15", + "sha256:99fdc1929703fd9e7408aed2e03f58701c5280b05c8911753a8d8619f7dfdda5", + "sha256:9dffa1795c2804d183efb01c0f1efd20a7831db6a21a0311edf90b4100d67436", + "sha256:bca1841693941fdd18371824bb19a9702d5784cd347cb8231317dbdc7062c5bc", + "sha256:c653d9121d6572d8b7f8abf20f88b0a41aab77ff5a6a36e5a0ec0f19af0072e8", + "sha256:c8f26250f87ca849a7303ed7a4fd6b2c7ac4dec16b7d7e68ca6a476d7c9bfcdb", + "sha256:cc9b6ac36d7ec9db2f053014ea488086ca2ed9c322be104fbe2c71ca759da4bb", + "sha256:d22d1abfe49aa60fc61fa867e10875a9b3024ba5a801112f4d7ba42d8d53242e", + "sha256:d68c34e3038d3d1d6324eb47744cbf13f2c65e1214cf49db6ff2a6603c1cd838", + "sha256:e3d8cf974cfa2487b28f23f56c4bff90d550ef16505e58b0dca0493d5293784b", + "sha256:f36f592e035ef84f312a12b75989dde6a5f6767fe99146cdae6a9ee9aff40dd0", + "sha256:f561c9a84eca18be92f4ad90aa9bd873111efbea995449301719a1a7805dbc5c", + "sha256:fe41b33aa73635b1651f64633f429f7aa21f86d2db5748659a99d9b7b1ed2a90" ], - "markers": "python_version >= '3.7'", - "version": "==1.0.1" + "markers": "python_version >= '3.8'", + "version": "==1.1.0" }, "libsass": { "hashes": [ @@ -1241,27 +1263,27 @@ }, "urllib3": { "hashes": [ - "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594", - "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e" + "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2", + "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564" ], "markers": "python_version >= '3.7'", - "version": "==2.0.5" + "version": "==2.0.6" }, "websocket-client": { "hashes": [ - "sha256:3aad25d31284266bcfcfd1fd8a743f63282305a364b8d0948a43bd606acc652f", - "sha256:6cfc30d051ebabb73a5fa246efdcc14c8fbebbd0330f8984ac3bb6d9edd2ad03" + "sha256:084072e0a7f5f347ef2ac3d8698a5e0b4ffbfcab607628cadabc650fc9a83a24", + "sha256:b3324019b3c28572086c4a319f91d1dcd44e6e11cd340232978c684a7650d0df" ], "markers": "python_version >= '3.8'", - "version": "==1.6.3" + "version": "==1.6.4" }, "werkzeug": { "hashes": [ - "sha256:2b8c0e447b4b9dbcc85dd97b6eeb4dcbaf6c8b6c3be0bd654e25553e0a2157d8", - "sha256:effc12dba7f3bd72e605ce49807bbe692bd729c3bb122a3b91747a6ae77df528" + "sha256:3ffff4dcc32db52ef3cc94dff3000a3c2846890f3a5a51800a27b909c5e770f0", + "sha256:cbb2600f7eabe51dbc0502f58be0b3e1b96b893b05695ea2b35b43d4de2d9962" ], "markers": "python_version >= '3.8'", - "version": "==2.3.7" + "version": "==3.0.0" }, "whitenoise": { "hashes": [ @@ -1306,31 +1328,27 @@ }, "black": { "hashes": [ - "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f", - "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7", - "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100", - "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573", - "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d", - "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f", - "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9", - "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300", - "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948", - "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325", - "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9", - "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71", - "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186", - "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f", - "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe", - "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855", - "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80", - "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393", - "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c", - "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204", - "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377", - "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301" + "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884", + "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916", + "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258", + "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1", + "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce", + "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d", + "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982", + "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7", + "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173", + "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9", + "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb", + "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad", + "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc", + "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0", + "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a", + "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe", + "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace", + "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69" ], "index": "pypi", - "version": "==23.9.1" + "version": "==23.10.1" }, "certifi": { "hashes": [ @@ -1342,84 +1360,99 @@ }, "charset-normalizer": { "hashes": [ - "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96", - "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c", - "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710", - "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706", - "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020", - "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252", - "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad", - "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329", - "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a", - "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f", - "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6", - "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4", - "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a", - "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46", - "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2", - "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23", - "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace", - "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd", - "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982", - "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10", - "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2", - "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea", - "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09", - "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5", - "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149", - "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489", - "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9", - "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80", - "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592", - "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3", - "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6", - "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed", - "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c", - "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200", - "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a", - "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e", - "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d", - "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6", - "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623", - "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669", - "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3", - "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa", - "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9", - "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2", - "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f", - "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1", - "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4", - "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a", - "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8", - "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3", - "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029", - "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f", - "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959", - "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22", - "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7", - "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952", - "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346", - "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e", - "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d", - "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299", - "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd", - "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a", - "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3", - "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037", - "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94", - "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c", - "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858", - "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a", - "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449", - "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c", - "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918", - "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1", - "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c", - "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", - "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" + "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" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.2.0" + "version": "==3.3.0" }, "click": { "hashes": [ @@ -1434,61 +1467,61 @@ "toml" ], "hashes": [ - "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375", - "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344", - "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e", - "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745", - "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f", - "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194", - "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a", - "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f", - "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760", - "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8", - "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392", - "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d", - "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc", - "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40", - "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981", - "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0", - "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92", - "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3", - "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0", - "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086", - "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7", - "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465", - "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140", - "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952", - "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3", - "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8", - "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f", - "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593", - "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0", - "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204", - "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037", - "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276", - "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9", - "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26", - "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce", - "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7", - "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136", - "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a", - "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4", - "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c", - "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f", - "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832", - "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3", - "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969", - "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520", - "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887", - "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3", - "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6", - "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1", - "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff", - "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981", - "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e" + "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1", + "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63", + "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9", + "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312", + "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3", + "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb", + "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25", + "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92", + "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda", + "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148", + "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6", + "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216", + "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a", + "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640", + "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836", + "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c", + "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f", + "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2", + "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901", + "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed", + "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a", + "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074", + "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc", + "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84", + "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083", + "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f", + "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c", + "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c", + "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637", + "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2", + "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82", + "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f", + "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce", + "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef", + "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f", + "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611", + "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c", + "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76", + "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9", + "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce", + "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9", + "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf", + "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf", + "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9", + "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6", + "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2", + "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a", + "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a", + "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf", + "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738", + "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a", + "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4" ], "markers": "python_version >= '3.8'", - "version": "==7.3.1" + "version": "==7.3.2" }, "defusedxml": { "hashes": [ @@ -1632,11 +1665,11 @@ }, "platformdirs": { "hashes": [ - "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d", - "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d" + "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", + "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e" ], "markers": "python_version >= '3.7'", - "version": "==3.10.0" + "version": "==3.11.0" }, "pluggy": { "hashes": [ @@ -1688,11 +1721,11 @@ }, "pytest-mock": { "hashes": [ - "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39", - "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f" + "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f", + "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9" ], "index": "pypi", - "version": "==3.11.1" + "version": "==3.12.0" }, "pytest-order": { "hashes": [ @@ -1849,11 +1882,11 @@ }, "urllib3": { "hashes": [ - "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594", - "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e" + "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2", + "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564" ], "markers": "python_version >= '3.7'", - "version": "==2.0.5" + "version": "==2.0.6" }, "wasmer": { "hashes": [ @@ -1909,4 +1942,4 @@ "version": "==1.3.0" } } -} +} \ No newline at end of file diff --git a/backend/portal/helpers/domain.py b/backend/portal/helpers/domain.py index a845f56c..24ba51ac 100644 --- a/backend/portal/helpers/domain.py +++ b/backend/portal/helpers/domain.py @@ -5,7 +5,6 @@ def get_domain(request): """ Returns the domain of the request """ - return ( "http://localhost:3000" if os.environ.get("DJANGO_DEBUG") == "True" diff --git a/backend/portal/serializers/__init__.py b/backend/portal/serializers/__init__.py new file mode 100644 index 00000000..d577449b --- /dev/null +++ b/backend/portal/serializers/__init__.py @@ -0,0 +1,3 @@ +from .klass import ClassSerializer +from .school import SchoolSerializer +from .user import UserSerializer diff --git a/backend/portal/serializers/klass.py b/backend/portal/serializers/klass.py new file mode 100644 index 00000000..209ff3b9 --- /dev/null +++ b/backend/portal/serializers/klass.py @@ -0,0 +1,6 @@ +from codeforlife.user.serializers import ClassSerializer as _ClassSerializer + + +class ClassSerializer(_ClassSerializer): + class Meta(_ClassSerializer.Meta): + pass diff --git a/backend/portal/serializers/school.py b/backend/portal/serializers/school.py new file mode 100644 index 00000000..6047a143 --- /dev/null +++ b/backend/portal/serializers/school.py @@ -0,0 +1,6 @@ +from codeforlife.user.serializers import SchoolSerializer as _SchoolSerializer + + +class SchoolSerializer(_SchoolSerializer): + class Meta(_SchoolSerializer.Meta): + pass diff --git a/backend/portal/serializers/user.py b/backend/portal/serializers/user.py new file mode 100644 index 00000000..2f476ce9 --- /dev/null +++ b/backend/portal/serializers/user.py @@ -0,0 +1,16 @@ +from codeforlife.user.serializers import UserSerializer as _UserSerializer +from rest_framework import serializers + + +class UserSerializer(_UserSerializer): + current_password = serializers.CharField(write_only=True) + + class Meta(_UserSerializer.Meta): + extra_kwargs = { + **_UserSerializer.Meta.extra_kwargs, + "username": {"read_only": True}, + "isActive": {"read_only": True}, + "isStaff": {"read_only": True}, + "dateJoined": {"read_only": True}, + "lastLogin": {"read_only": True}, + } diff --git a/backend/portal/urls/__init__.py b/backend/portal/urls/__init__.py index 9df33bbe..ce3c4881 100644 --- a/backend/portal/urls/__init__.py +++ b/backend/portal/urls/__init__.py @@ -1,18 +1,26 @@ from django.urls import include, path +from rest_framework.routers import DefaultRouter +from ..views import ClassViewSet, SchoolViewSet, UserViewSet from .admin import urlpatterns as admin_urlpatterns from .cron import urlpatterns as cron_urlpatterns from .dotmailer import urlpatterns as dotmailer_urlpatterns from .email import urlpatterns as email_urlpatterns from .home import urlpatterns as home_urlpatterns from .organisation import urlpatterns as organisation_urlpatterns -from .teacher.dashboard import urlpatterns as teach_dashboard_urlpatterns from .registration import urlpatterns as registration_urlpatterns from .student import urlpatterns as student_urlpatterns from .teacher import urlpatterns as teacher_urlpatterns +from .teacher.dashboard import urlpatterns as teach_dashboard_urlpatterns + +router = DefaultRouter() +router.register("classes", ClassViewSet, basename="class") +router.register("users", UserViewSet, basename="user") +router.register("schools", SchoolViewSet, basename="school") urlpatterns = [ path("cron/", include(cron_urlpatterns)), + path("", include(router.urls)), *dotmailer_urlpatterns, *email_urlpatterns, *home_urlpatterns, diff --git a/backend/portal/urls/teacher/dashboard.py b/backend/portal/urls/teacher/dashboard.py index a63c199e..5f3aade2 100644 --- a/backend/portal/urls/teacher/dashboard.py +++ b/backend/portal/urls/teacher/dashboard.py @@ -18,6 +18,7 @@ create_new_class, get_student_request_data, get_students_from_access_code, + get_student_details, ) from ...helpers.regexes import ACCESS_CODE_REGEX @@ -88,4 +89,9 @@ get_students_from_access_code, name="get_students_from_acccess_code", ), + re_path( + r"^class/student/(?P[0-9]+)/$", + get_student_details, + name="get_student_details", + ), ] diff --git a/backend/portal/urls/teacher/teach.py b/backend/portal/urls/teacher/teach.py index aff66af3..d60de271 100644 --- a/backend/portal/urls/teacher/teach.py +++ b/backend/portal/urls/teacher/teach.py @@ -5,6 +5,8 @@ teacher_edit_class, teacher_delete_class, teacher_move_class, + teacher_edit_student, + teacher_print_reminder_cards, ) @@ -24,4 +26,14 @@ teacher_move_class, name="teacher_move_class", ), + path( + "student/edit/", + teacher_edit_student, + name="teacher_edit_student", + ), + re_path( + rf"onboarding-class/(?P{ACCESS_CODE_REGEX})/print-reminder-cards/", + teacher_print_reminder_cards, + name="teacher_print_reminder_cards", + ), ] diff --git a/backend/portal/views/__init__.py b/backend/portal/views/__init__.py index e69de29b..ffcd7b4e 100644 --- a/backend/portal/views/__init__.py +++ b/backend/portal/views/__init__.py @@ -0,0 +1,3 @@ +from .klass import ClassViewSet +from .school import SchoolViewSet +from .user import UserViewSet diff --git a/backend/portal/views/klass.py b/backend/portal/views/klass.py new file mode 100644 index 00000000..15f28510 --- /dev/null +++ b/backend/portal/views/klass.py @@ -0,0 +1,8 @@ +from codeforlife.user.views import ClassViewSet as _ClassViewSet + +from ..serializers import ClassSerializer + + +class ClassViewSet(_ClassViewSet): + http_method_names = ["get", "post", "patch", "delete"] + serializer_class = ClassSerializer diff --git a/backend/portal/views/school.py b/backend/portal/views/school.py new file mode 100644 index 00000000..71121e39 --- /dev/null +++ b/backend/portal/views/school.py @@ -0,0 +1,8 @@ +from codeforlife.user.views import SchoolViewSet as _SchoolViewSet + +from ..serializers import SchoolSerializer + + +class SchoolViewSet(_SchoolViewSet): + http_method_names = ["get", "post", "patch"] + serializer_class = SchoolSerializer diff --git a/backend/portal/views/teacher/dashboard.py b/backend/portal/views/teacher/dashboard.py index 2bfebb25..b695979d 100644 --- a/backend/portal/views/teacher/dashboard.py +++ b/backend/portal/views/teacher/dashboard.py @@ -5,8 +5,8 @@ from django.contrib.auth import logout from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.models import User -from django.core import serializers -from django.db.models import F, Value +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import F from django.http import ( Http404, HttpResponse, @@ -68,10 +68,12 @@ @login_required(login_url=reverse_lazy("teacher_login")) def get_students_from_access_code(request, access_code): - check_teacher_authorised(request, request.user.new_teacher) + student_class = Class.objects.get(access_code=access_code) + check_teacher_authorised(request, student_class.teacher) students_query = Student.objects.filter( class_field__access_code=access_code ) + # TODO: make this into a method for the student so we can reuse it students = [ { "id": student.id, @@ -94,6 +96,39 @@ def get_students_from_access_code(request, access_code): return JsonResponse({"students_per_access_code": students}) +def get_student_details(request, student_id): + student = get_object_or_404(Student, id=student_id) + try: + student_class = Class.objects.get( + access_code=student.class_field.access_code + ) + check_teacher_authorised(request, student_class.teacher) + except (ObjectDoesNotExist, AttributeError) as error: + return JsonResponse({"error": str(error)}) + # TODO: make this into a method for the student so we can reuse it + return JsonResponse( + { + "student": { + "id": student.id, + "class_field": getattr(student.class_field, "id", 0), + "new_user": { + "id": getattr(student.new_user, "id", 0), + "first_name": getattr(student.new_user, "first_name", ""), + "last_name": getattr(student.new_user, "last_name", ""), + }, + "pending_class_request": getattr( + student.pending_class_request, "id", 0 + ), + "blocked_time": student.blocked_time.strftime( + "%Y-%m-%d %H:%M:%S" + ) + if student.blocked_time + else "", + } + } + ) + + def _get_update_account_rate(group, request): """ Custom rate which checks in a POST request is performed on the update diff --git a/backend/portal/views/teacher/teach.py b/backend/portal/views/teacher/teach.py index cba9b363..57e51af8 100644 --- a/backend/portal/views/teacher/teach.py +++ b/backend/portal/views/teacher/teach.py @@ -60,6 +60,8 @@ from reportlab.pdfgen import canvas from rest_framework import status +from codeforlife import settings + STUDENT_PASSWORD_LENGTH = 6 REMINDER_CARDS_PDF_ROWS = 8 REMINDER_CARDS_PDF_COLUMNS = 1 @@ -314,6 +316,7 @@ def teacher_edit_class(request, access_code): - Locking or unlocking specific Rapid Router levels - Transferring the class to another teacher """ + access_code = access_code.upper() klass = get_object_or_404(Class, access_code=access_code) old_teacher = klass.teacher other_teachers = Teacher.objects.filter(school=old_teacher.school).exclude( @@ -529,7 +532,17 @@ def teacher_edit_student(request, pk): set_password_mode = False if request.method == "POST": - if "update_details" in request.POST: + if "name" in request.POST: + student_name = request.POST.get("name") + if Student.objects.filter( + new_user__first_name=student_name + ).exists(): + return JsonResponse( + { + "message": f"There is already a student called {student_name} in this class" + }, + status=400, + ) name_form = TeacherEditStudentForm(student, request.POST) if name_form.is_valid(): name = name_form.cleaned_data["name"] @@ -537,37 +550,27 @@ def teacher_edit_student(request, pk): student.new_user.save() student.save() - messages.success( - request, - "The student's details have been changed successfully.", - ) - - return HttpResponseRedirect( - reverse_lazy( - "view_class", - kwargs={"access_code": student.class_field.access_code}, - ) + return JsonResponse( + { + "message": "The student's details have been changed successfully.", + } ) - else: + elif "confirm_password" in request.POST: password_form = TeacherSetStudentPass(request.POST) if password_form.is_valid(): return process_reset_password_form( request, student, password_form ) - set_password_mode = True - return render( - request, - "portal/teach/teacher_edit_student.html", - { - "name_form": name_form, - "password_form": password_form, - "student": student, - "class": student.class_field, - "set_password_mode": set_password_mode, - }, - ) + return JsonResponse( + { + "message": "Your details were not updated due to incorrect details" + }, + status=400, + ) + else: + return HttpResponse(status=405) def process_reset_password_form(request, student, password_form): @@ -577,11 +580,10 @@ def process_reset_password_form(request, student, password_form): # generate uuid for url and store the hashed uuidstr = uuid4().hex login_id = get_hashed_login_id(uuidstr) - login_url = request.build_absolute_uri( - reverse( - "student_direct_login", - kwargs={"user_id": student.new_user.id, "login_id": uuidstr}, - ) + protocol = settings.SERVICE_PROTOCOL + domain = request.headers.get("host", "") + login_url = ( + f"{protocol}://{domain}/api/u/{student.new_user.id}/{uuidstr}/" ) students_info = [ @@ -602,22 +604,20 @@ def process_reset_password_form(request, student, password_form): ) student.blocked_time = datetime.now(tz=pytz.utc) - timedelta(days=1) student.save() - - return render( - request, - "portal/teach/onboarding_print.html", + access_code = student.class_field.access_code + frontend_link = request.headers.get("referer", "") + student_login_link = "".join([frontend_link, "login/student"]) + class_login_link = "/".join([student_login_link, access_code]) + return JsonResponse( { - "class": student.class_field, + "student_login_link": student_login_link, + "class_link": class_login_link, + "access_code": access_code, + "class": student.class_field.name, "students_info": students_info, "onboarding_done": True, - "query_data": json.dumps(students_info), - "class_url": request.build_absolute_uri( - reverse( - "student_login", - kwargs={"access_code": student.class_field.access_code}, - ) - ), - }, + "query_data": students_info, # this field could be redundant + } ) @@ -931,159 +931,222 @@ class DownloadType(Enum): PYTHON_PACK = 4 -@login_required(login_url=reverse_lazy("teacher_login")) -@user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login")) +# This method was added due to weird behavior of +# dictionaries keys and list indexes when passing +# data from frontend to backend. If you know a method +# to fix the dictionary issue, feel free to use it and +# delete this method +def expand_key(dictionary): + expanded = {} + + for key, value in dictionary.items(): + keys = key.split("[") + keys = [k.replace("]", "") for k in keys] + + temp = expanded + for i, k in enumerate(keys[:-1]): + if k.isdigit(): + k = int(k) + if not isinstance(temp, list): + temp = [] + while len(temp) <= k: + temp.append({}) + if keys[i - 1] in expanded: + expanded[keys[i - 1]] = temp + else: + temp[keys[i - 1]] = temp + temp = temp[k] + else: + if k not in temp: + temp[k] = {} + temp = temp[k] + + temp[keys[-1]] = value[0] if len(value) == 1 else value + + return expanded + + +# TODO: this function had a method that adds the download PDF analytic, we should +# add it back in somewhere else at some point +# @login_required(login_url=reverse_lazy("teacher_login")) +# @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login")) +# def teacher_print_reminder_cards(request, access_code): +# response = HttpResponse(content_type="application/pdf") +# response["Content-Disposition"] = 'filename="student_reminder_cards.pdf"' + +# p = canvas.Canvas(response, pagesize=A4) + +# # Define constants that determine the look of the cards +# PAGE_WIDTH, PAGE_HEIGHT = A4 +# PAGE_MARGIN = PAGE_WIDTH // 16 +# INTER_CARD_MARGIN = PAGE_WIDTH // 64 +# CARD_PADDING = PAGE_WIDTH // 48 + +# # rows and columns on page +# NUM_X = REMINDER_CARDS_PDF_COLUMNS +# NUM_Y = REMINDER_CARDS_PDF_ROWS + +# CARD_WIDTH = (PAGE_WIDTH - PAGE_MARGIN * 2) // NUM_X +# CARD_HEIGHT = (PAGE_HEIGHT - PAGE_MARGIN * 4) // NUM_Y + +# CARD_INNER_HEIGHT = CARD_HEIGHT - CARD_PADDING * 2 + +# # logo_image = ImageReader( +# # staticfiles_storage.path("portal/img/logo_cfl_reminder_cards.jpg") +# # ) + +# klass = get_object_or_404(Class, access_code=access_code) +# # Check auth +# check_teacher_authorised(request, klass.teacher) + +# # Use data from the query string if given +# frontend_link = request.headers.get("referer", "") +# student_login_link = "/".join([frontend_link, "login/student"]) +# class_login_link = "/".join([student_login_link, access_code]) +# current_student_reminder_data = expand_key(dict(request.POST)) + +# # Now draw everything +# x = 0 +# y = 0 + +# current_student_count = 0 +# for student in current_student_reminder_data["students_info"]: +# # warning text for every new page +# if current_student_count % (NUM_X * NUM_Y) == 0: +# p.setFillColor(red) +# p.setFont("Helvetica-Bold", 10) +# p.drawString( +# PAGE_MARGIN, PAGE_MARGIN / 2, REMINDER_CARDS_PDF_WARNING_TEXT +# ) + +# left = PAGE_MARGIN + x * CARD_WIDTH + x * INTER_CARD_MARGIN * 2 +# bottom = ( +# PAGE_HEIGHT +# - PAGE_MARGIN +# - (y + 1) * CARD_HEIGHT +# - y * INTER_CARD_MARGIN +# ) + +# inner_bottom = bottom + CARD_PADDING + +# # card border +# p.setStrokeColor(black) +# p.rect(left, bottom, CARD_WIDTH, CARD_HEIGHT) + +# # logo +# # p.drawImage( +# # logo_image, +# # left, +# # bottom + INTER_CARD_MARGIN, +# # height=CARD_HEIGHT - INTER_CARD_MARGIN * 2, +# # preserveAspectRatio=True, +# # ) + +# text_left = left # + logo_image.getSize()[0] + +# # student details +# p.setFillColor(black) +# p.setFont("Helvetica", 12) +# p.drawString( +# text_left, +# inner_bottom + CARD_INNER_HEIGHT * 0.9, +# f"Class code: {klass.access_code} at {student_login_link}", +# ) +# p.setFont("Helvetica-BoldOblique", 12) +# p.drawString(text_left, inner_bottom + CARD_INNER_HEIGHT * 0.6, "OR") +# p.setFont("Helvetica", 12) +# p.drawString( +# text_left + 22, +# inner_bottom + CARD_INNER_HEIGHT * 0.6, +# f"class link: {class_login_link}", +# ) +# p.drawString( +# text_left, +# inner_bottom + CARD_INNER_HEIGHT * 0.3, +# f"Name: {student['name']}", +# ) +# p.drawString( +# text_left, inner_bottom, f"Password: {student['password']}" +# ) + +# x = (x + 1) % NUM_X +# y = compute_show_page_character(p, x, y, NUM_Y) +# current_student_count += 1 + +# compute_show_page_end(p, x, y) + +# p.save() + +# count_student_details_click(DownloadType.LOGIN_CARDS) TODO: <--- this had to be implemented before deploying for data +# return response + + +@login_required(login_url=reverse_lazy("session-expired")) +@user_passes_test( + logged_in_as_teacher, login_url=reverse_lazy("session-expired") +) def teacher_print_reminder_cards(request, access_code): - response = HttpResponse(content_type="application/pdf") - response["Content-Disposition"] = 'filename="student_reminder_cards.pdf"' - - p = canvas.Canvas(response, pagesize=A4) - - # Define constants that determine the look of the cards - PAGE_WIDTH, PAGE_HEIGHT = A4 - PAGE_MARGIN = PAGE_WIDTH // 16 - INTER_CARD_MARGIN = PAGE_WIDTH // 64 - CARD_PADDING = PAGE_WIDTH // 48 - - # rows and columns on page - NUM_X = REMINDER_CARDS_PDF_COLUMNS - NUM_Y = REMINDER_CARDS_PDF_ROWS - - CARD_WIDTH = (PAGE_WIDTH - PAGE_MARGIN * 2) // NUM_X - CARD_HEIGHT = (PAGE_HEIGHT - PAGE_MARGIN * 4) // NUM_Y - - CARD_INNER_HEIGHT = CARD_HEIGHT - CARD_PADDING * 2 - - logo_image = ImageReader( - staticfiles_storage.path("portal/img/logo_cfl_reminder_cards.jpg") - ) - klass = get_object_or_404(Class, access_code=access_code) - # Check auth check_teacher_authorised(request, klass.teacher) - # Use data from the query string if given - student_data = get_student_data(request) - student_login_link = request.build_absolute_uri( - reverse("student_login_access_code") - ) - class_login_link = request.build_absolute_uri( - reverse("student_login", kwargs={"access_code": access_code}) - ) - - # Now draw everything - x = 0 - y = 0 - - current_student_count = 0 - for student in student_data: - # warning text for every new page - if current_student_count % (NUM_X * NUM_Y) == 0: - p.setFillColor(red) - p.setFont("Helvetica-Bold", 10) - p.drawString( - PAGE_MARGIN, PAGE_MARGIN / 2, REMINDER_CARDS_PDF_WARNING_TEXT - ) - - left = PAGE_MARGIN + x * CARD_WIDTH + x * INTER_CARD_MARGIN * 2 - bottom = ( - PAGE_HEIGHT - - PAGE_MARGIN - - (y + 1) * CARD_HEIGHT - - y * INTER_CARD_MARGIN - ) - - inner_bottom = bottom + CARD_PADDING - - # card border - p.setStrokeColor(black) - p.rect(left, bottom, CARD_WIDTH, CARD_HEIGHT) - - # logo - p.drawImage( - logo_image, - left, - bottom + INTER_CARD_MARGIN, - height=CARD_HEIGHT - INTER_CARD_MARGIN * 2, - preserveAspectRatio=True, - ) + frontend_link = request.headers.get("referer", "") + student_login_link = "/".join([frontend_link, "login/student"]) + class_login_link = "/".join([student_login_link, access_code]) + current_student_reminder_data = expand_key(dict(request.POST)) - text_left = left + logo_image.getSize()[0] + students_data = [] - # student details - p.setFillColor(black) - p.setFont("Helvetica", 12) - p.drawString( - text_left, - inner_bottom + CARD_INNER_HEIGHT * 0.9, - f"Class code: {klass.access_code} at {student_login_link}", - ) - p.setFont("Helvetica-BoldOblique", 12) - p.drawString(text_left, inner_bottom + CARD_INNER_HEIGHT * 0.6, "OR") - p.setFont("Helvetica", 12) - p.drawString( - text_left + 22, - inner_bottom + CARD_INNER_HEIGHT * 0.6, - f"class link: {class_login_link}", - ) - p.drawString( - text_left, - inner_bottom + CARD_INNER_HEIGHT * 0.3, - f"Name: {student['name']}", - ) - p.drawString( - text_left, inner_bottom, f"Password: {student['password']}" - ) - - x = (x + 1) % NUM_X - y = compute_show_page_character(p, x, y, NUM_Y) - current_student_count += 1 - - compute_show_page_end(p, x, y) - - p.save() + for student in current_student_reminder_data["students_info"]: + student_data = { + "class_code": klass.access_code, + "student_login_link": student_login_link, + "class_login_link": class_login_link, + "name": student["name"], + "password": student["password"], + } + students_data.append(student_data) count_student_details_click(DownloadType.LOGIN_CARDS) - - return response - - -@login_required(login_url=reverse_lazy("teacher_login")) -@user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login")) -def teacher_download_csv(request, access_code): - response = HttpResponse(content_type="text/csv") - response[ - "Content-Disposition" - ] = 'attachment; filename="student_login_urls.csv"' - - klass = get_object_or_404(Class, access_code=access_code) - # Check auth - check_teacher_authorised(request, klass.teacher) - - class_url = request.build_absolute_uri( - reverse("student_login", kwargs={"access_code": access_code}) - ) - - # Use data from the query string if given - student_data = get_student_data(request) - if student_data: - writer = csv.writer(response) - writer.writerow([access_code, class_url]) - for student in student_data: - writer.writerow( - [student["name"], student["password"], student["login_url"]] - ) - - count_student_details_click(DownloadType.CSV) - - return response - - -def get_student_data(request): - if request.method == "POST": - data = request.POST.get("data", "[]") - return json.loads(data) - return [] + return JsonResponse({"students": students_data}) + + +# TODO: before deploying make sure the count_student_details_click is implemented +# @login_required(login_url=reverse_lazy("teacher_login")) +# @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login")) +# def teacher_download_csv(request, access_code): +# response = HttpResponse(content_type="text/csv") +# response[ +# "Content-Disposition" +# ] = 'attachment; filename="student_login_urls.csv"' + +# klass = get_object_or_404(Class, access_code=access_code) +# # Check auth +# check_teacher_authorised(request, klass.teacher) + +# class_url = request.build_absolute_uri( +# reverse("student_login", kwargs={"access_code": access_code}) +# ) + +# # Use data from the query string if given +# student_data = get_student_data(request) +# if student_data: +# writer = csv.writer(response) +# writer.writerow([access_code, class_url]) +# for student in student_data: +# writer.writerow( +# [student["name"], student["password"], student["login_url"]] +# ) + +# count_student_details_click(DownloadType.CSV) # <-- implement this + +# return response + + +# def get_student_data(request): +# if request.method == "POST": +# data = request.POST.get("data", "[]") +# return json.loads(data) +# return [] def compute_show_page_character(p, x, y, NUM_Y): diff --git a/backend/portal/views/user.py b/backend/portal/views/user.py new file mode 100644 index 00000000..d3cb3727 --- /dev/null +++ b/backend/portal/views/user.py @@ -0,0 +1,8 @@ +from codeforlife.user.views import UserViewSet as _UserViewSet + +from ..serializers import UserSerializer + + +class UserViewSet(_UserViewSet): + http_method_names = ["get", "post", "patch", "delete"] + serializer_class = UserSerializer diff --git a/backend/service/settings.py b/backend/service/settings.py index 3867c233..9c7dc1fd 100644 --- a/backend/service/settings.py +++ b/backend/service/settings.py @@ -10,7 +10,6 @@ https://docs.djangoproject.com/en/3.2/ref/settings/ """ import os - from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -23,35 +22,6 @@ # Application definition -INSTALLED_APPS = [ - "aimmo", - "game", - "pipeline", - "portal", - "captcha", - "common", - "django.contrib.admin", - "django.contrib.admindocs", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.sites", - "django.contrib.staticfiles", - "rest_framework", - "import_export", - "django_js_reverse", - "django_otp", - "django_otp.plugins.otp_static", - "django_otp.plugins.otp_totp", - "sekizai", # for javascript and css management - "treebeard", - "two_factor", - "preventconcurrentlogins", - "codeforlife", - "corsheaders", -] - MIDDLEWARE = [ "deploy.middleware.admin_access.AdminAccessMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -266,3 +236,30 @@ def domain(): from codeforlife.settings import * + +INSTALLED_APPS = [ + "aimmo", + "game", + "pipeline", + "portal", + "captcha", + "common", + "django.contrib.admin", + "django.contrib.admindocs", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.sites", + "django.contrib.staticfiles", + "import_export", + "django_js_reverse", + "django_otp", + "django_otp.plugins.otp_static", + "django_otp.plugins.otp_totp", + "sekizai", # for javascript and css management + "treebeard", + "two_factor", + "preventconcurrentlogins", + *INSTALLED_APPS, +] diff --git a/backend/service/urls.py b/backend/service/urls.py index 28b62605..416b4544 100644 --- a/backend/service/urls.py +++ b/backend/service/urls.py @@ -19,4 +19,5 @@ urlpatterns = service_urlpatterns( api_urls_path="portal.urls", # TODO: standardize path frontend_template_name="portal.html", # TODO: standardize name + include_user_urls=False, ) diff --git a/frontend/package.json b/frontend/package.json index aa1cd9fa..6f64bdfa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,8 +27,9 @@ "@emotion/styled": "^11.10.6", "@mui/icons-material": "^5.11.11", "@mui/material": "^5.11.12", + "@react-pdf/renderer": "^3.1.12", "@reduxjs/toolkit": "^1.9.3", - "codeforlife": "github:ocadotechnology/codeforlife-package-javascript#v1.27.7", + "codeforlife": "github:ocadotechnology/codeforlife-package-javascript#v1.27.5", "country-list": "^2.3.0", "crypto-js": "^4.1.1", "formik": "^2.2.9", diff --git a/frontend/src/app/api/api.ts b/frontend/src/app/api/api.ts index 350b5280..adc5f79c 100644 --- a/frontend/src/app/api/api.ts +++ b/frontend/src/app/api/api.ts @@ -1,10 +1,7 @@ import { fetchBaseQuery } from '@reduxjs/toolkit/query'; import { createApi } from '@reduxjs/toolkit/query/react'; -import { - endpoints, - tagTypes -} from 'codeforlife/lib/esm/api'; +import { endpoints, tagTypes } from 'codeforlife/lib/esm/api'; import { FetchBaseQuery, fetch, @@ -14,10 +11,7 @@ import { parseResponseBody } from 'codeforlife/lib/esm/api/baseQuery'; -import { - SSO_SERVICE_API_URL, - SSO_SERVICE_NAME -} from './env'; +import { SSO_SERVICE_API_URL, SSO_SERVICE_NAME } from './env'; // TODO: remove this hot switching code and migrate login pages to SSO service. const ssoFetch = fetchBaseQuery({ @@ -37,7 +31,11 @@ const baseQuery: FetchBaseQuery = async (args, api, extraOptions) => { parseRequestBody(args); // Send the HTTP request and fetch the response. - const result = await (isLoginRequest ? ssoFetch : fetch)(args, api, extraOptions); + const result = await (isLoginRequest ? ssoFetch : fetch)( + args, + api, + extraOptions + ); handleResponseError(result); diff --git a/frontend/src/app/api/index.ts b/frontend/src/app/api/index.ts index 7e74c6ad..12b06af3 100644 --- a/frontend/src/app/api/index.ts +++ b/frontend/src/app/api/index.ts @@ -1,101 +1,144 @@ import api, { useLogoutMutation } from './api'; import { - useSubscribeToNewsletterMutation, - useConsentFormMutation + useConsentFormMutation, + useSubscribeToNewsletterMutation } from './dotmailer'; import { - useRegisterUserMutation, - useDownloadStudentPackMutation + useDownloadStudentPackMutation, + useRegisterUserMutation } from './home'; -import { useLoginMutation } from './login'; +import { + Class, + useDestroyClassMutation, + useLazyCreateClassQuery, + useListClassesQuery, + useRetrieveClassQuery, + useUpdateClassMutation +} from './klass'; +import { + useLoginMutation +} from './login'; import { useCreateOrganisationMutation, useLeaveOrganisationMutation } from './organisation'; import { + useDeleteAccountMutation, useRequestIndependentStudentPasswordResetMutation, useRequestTeacherPasswordResetMutation, useResetPasswordMutation, - useVerifyPasswordMutation, - useDeleteAccountMutation + useVerifyPasswordMutation } from './registration'; - import { - useGetStudentScoreQuery, + School, + useDestroySchoolMutation, + useLazyCreateSchoolQuery, + useListSchoolsQuery, + useRetrieveSchoolQuery, + useUpdateSchoolMutation +} from './school'; +import { useGetStudentKuronoGameDataQuery, - useUpdateSchoolStudentDetailsMutation, - useUpdateStudentDetailsMutation, + useGetStudentScoreQuery, + useIsRequestingToJoinSchoolQuery, useJoinSchoolRequestMutation, useRevokeSchoolRequestMutation, - useIsRequestingToJoinSchoolQuery + useUpdateSchoolStudentDetailsMutation, + useUpdateStudentDetailsMutation } from './student'; import { + useUpdateTeacherAccountDetailsMutation +} from './teacher/account'; +import { + useDeleteInviteMutation, useGetTeacherDataQuery, useInviteTeacherMutation, - useUpdateSchoolMutation, - useToggleAdminMutation, - useOrganisationKickMutation, useInviteToggleAdminMutation, + useOldUpdateSchoolMutation, + useOrganisationKickMutation, useResendInviteMutation, - useDeleteInviteMutation + useToggleAdminMutation } from './teacher/dashboard'; import { + useDeleteClassMutation, + useDisable2faMutation, + useEditStudentNameMutation, + useEditStudentPasswordMutation, useGetClassQuery, useGetStudentsByAccessCodeQuery, - useUpdateClassMutation, - useDeleteClassMutation, useMoveClassMutation, - useTeacherHas2faQuery, - useDisable2faMutation + useTeacherHas2faQuery } from './teacher/teach'; -import { useUpdateTeacherAccountDetailsMutation } from './teacher/account'; +import { + User, + useBulkDestroyUsersMutation, + useBulkUpdateUsersMutation, + useDestroyUserMutation, + useLazyBulkCreateUsersQuery, + useLazyCreateUserQuery, + useListUsersQuery, + useRetrieveUserQuery, + useUpdateUserMutation +} from './user'; export default api; export { - // api - useLogoutMutation, - // dotmailer - useSubscribeToNewsletterMutation, + useBulkDestroyUsersMutation, + useBulkUpdateUsersMutation, useConsentFormMutation, - // home - useRegisterUserMutation, - useDownloadStudentPackMutation, - // login - useLoginMutation, - // organisation useCreateOrganisationMutation, - useLeaveOrganisationMutation, - // registration - useRequestIndependentStudentPasswordResetMutation, - useRequestTeacherPasswordResetMutation, - useResetPasswordMutation, - useVerifyPasswordMutation, useDeleteAccountMutation, - // student - useGetStudentScoreQuery, + useDeleteClassMutation, + useDeleteInviteMutation, + useDestroyClassMutation, + useDestroySchoolMutation, + useDestroyUserMutation, + useDisable2faMutation, + useDownloadStudentPackMutation, + useEditStudentNameMutation, + useEditStudentPasswordMutation, + useGetClassQuery, useGetStudentKuronoGameDataQuery, - useUpdateStudentDetailsMutation, - useUpdateSchoolStudentDetailsMutation, - useJoinSchoolRequestMutation, - useRevokeSchoolRequestMutation, - useIsRequestingToJoinSchoolQuery, - // teacher dashboard + useGetStudentScoreQuery, + useGetStudentsByAccessCodeQuery, useGetTeacherDataQuery, useInviteTeacherMutation, - useUpdateSchoolMutation, - useToggleAdminMutation, - useOrganisationKickMutation, useInviteToggleAdminMutation, + useIsRequestingToJoinSchoolQuery, + useJoinSchoolRequestMutation, + useLazyBulkCreateUsersQuery, + useLazyCreateClassQuery, + useLazyCreateSchoolQuery, + useLazyCreateUserQuery, + useLeaveOrganisationMutation, + useListClassesQuery, + useListSchoolsQuery, + useListUsersQuery, + useLoginMutation, + useLogoutMutation, + useMoveClassMutation, + useOldUpdateSchoolMutation, + useOrganisationKickMutation, + useRegisterUserMutation, + useRequestIndependentStudentPasswordResetMutation, + useRequestTeacherPasswordResetMutation, useResendInviteMutation, - useDeleteInviteMutation, - // teacher/teach - useGetClassQuery, - useGetStudentsByAccessCodeQuery, + useResetPasswordMutation, + useRetrieveClassQuery, + useRetrieveSchoolQuery, + useRetrieveUserQuery, + useRevokeSchoolRequestMutation, + useSubscribeToNewsletterMutation, + useTeacherHas2faQuery, + useToggleAdminMutation, useUpdateClassMutation, - useDeleteClassMutation, - useMoveClassMutation, - // teacher/account + useUpdateSchoolMutation, + useUpdateSchoolStudentDetailsMutation, + useUpdateStudentDetailsMutation, useUpdateTeacherAccountDetailsMutation, - useTeacherHas2faQuery, - useDisable2faMutation + useUpdateUserMutation, + useVerifyPasswordMutation, + type Class, + type School, + type User }; diff --git a/frontend/src/app/api/klass.ts b/frontend/src/app/api/klass.ts new file mode 100644 index 00000000..0adb1e2f --- /dev/null +++ b/frontend/src/app/api/klass.ts @@ -0,0 +1,124 @@ +import { + CreateArg, + CreateResult, + DestroyArg, + DestroyResult, + ListArg, + ListResult, + Model, + RetrieveArg, + RetrieveResult, + UpdateArg, + UpdateResult, + tagData +} from 'codeforlife/lib/esm/helpers/rtkQuery'; + +import api from './api'; + +export type Class = Model< + number, + { + name: string; + teacher: number; + classmatesDataViewable?: boolean; + alwaysAcceptRequests?: boolean; + acceptRequestsUntil: null | Date; + isActive?: boolean; + }, + { + accessCode: string; + creationTime: null | Date; + createdBy: number; + } +>; + +const classApi = api.injectEndpoints({ + endpoints: (build) => ({ + createClass: build.query< + CreateResult, + CreateArg + >({ + query: (body) => ({ + url: 'classes/', + method: 'POST', + body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }), + providesTags: (result, error, data) => result && !error + ? [ + 'private', + { type: 'class', id: result.accessCode } + ] + : [] + }), + retrieveClass: build.query< + RetrieveResult, + RetrieveArg + >({ + query: ({ accessCode }) => ({ + url: `classes/${accessCode}/`, + method: 'GET' + }), + providesTags: (result, error, { accessCode }) => result && !error + ? [ + 'private', + { type: 'class', id: accessCode } + ] + : [] + }), + listClasses: build.query< + ListResult, + ListArg + >({ + query: () => ({ + url: 'classes/', + method: 'GET' + }), + providesTags: (result, error, arg) => result && !error + ? [ + 'private', + ...tagData(result, 'class', 'accessCode') + ] + : [] + }), + updateClass: build.mutation< + UpdateResult, + UpdateArg + >({ + query: ({ accessCode, ...body }) => ({ + url: `classes/${accessCode}/`, + method: 'PATCH', + body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }), + invalidatesTags: (result, error, { accessCode }) => !error + ? [{ type: 'class', id: accessCode }] + : [] + }), + destroyClass: build.mutation< + DestroyResult, + DestroyArg + >({ + query: ({ accessCode }) => ({ + url: `classes/${accessCode}/`, + method: 'DELETE' + }), + invalidatesTags: (result, error, { accessCode }) => !error + ? [{ type: 'class', id: accessCode }] + : [] + }) + }) +}); + +export default classApi; +export const { + useLazyCreateClassQuery, + useRetrieveClassQuery, + useListClassesQuery, + useUpdateClassMutation, + useDestroyClassMutation +} = classApi; diff --git a/frontend/src/app/api/login.ts b/frontend/src/app/api/login.ts index 840b4ae8..5920e0a1 100644 --- a/frontend/src/app/api/login.ts +++ b/frontend/src/app/api/login.ts @@ -8,7 +8,7 @@ export type LoginQuery = { password: string; classId: string; } | { - userId: string; + userId: number; loginId: string; } | { otp: string; diff --git a/frontend/src/app/api/school.ts b/frontend/src/app/api/school.ts new file mode 100644 index 00000000..7a224b35 --- /dev/null +++ b/frontend/src/app/api/school.ts @@ -0,0 +1,121 @@ +import { + CreateArg, + CreateResult, + DestroyArg, + DestroyResult, + ListArg, + ListResult, + Model, + RetrieveArg, + RetrieveResult, + UpdateArg, + UpdateResult, + tagData +} from 'codeforlife/lib/esm/helpers/rtkQuery'; + +import api from './api'; + +export type School = Model< + number, + { + name: string; + postcode: null | string; + country: string; + county: null | string; + isActive?: boolean; + }, + { + creationTime: null | Date; + } +>; + +const schoolApi = api.injectEndpoints({ + endpoints: (build) => ({ + createSchool: build.query< + CreateResult, + CreateArg + >({ + query: (body) => ({ + url: 'schools/', + method: 'POST', + body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }), + providesTags: (result, error, data) => result && !error + ? [ + 'private', + { type: 'school', id: result.id } + ] + : [] + }), + retrieveSchool: build.query< + RetrieveResult, + RetrieveArg + >({ + query: ({ id }) => ({ + url: `schools/${id}/`, + method: 'GET' + }), + providesTags: (result, error, { id }) => result && !error + ? [ + 'private', + { type: 'school', id } + ] + : [] + }), + listSchools: build.query< + ListResult, + ListArg + >({ + query: () => ({ + url: 'schools/', + method: 'GET' + }), + providesTags: (result, error, arg) => result && !error + ? [ + 'private', + ...tagData(result, 'school') + ] + : [] + }), + updateSchool: build.mutation< + UpdateResult, + UpdateArg + >({ + query: ({ id, ...body }) => ({ + url: `schools/${id}/`, + method: 'PATCH', + body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }), + invalidatesTags: (result, error, { id }) => !error + ? [{ type: 'school', id }] + : [] + }), + destroySchool: build.mutation< + DestroyResult, + DestroyArg + >({ + query: ({ id }) => ({ + url: `schools/${id}/`, + method: 'DELETE' + }), + invalidatesTags: (result, error, { id }) => !error + ? [{ type: 'school', id }] + : [] + }) + }) +}); + +export default schoolApi; +export const { + useLazyCreateSchoolQuery, + useRetrieveSchoolQuery, + useListSchoolsQuery, + useUpdateSchoolMutation, + useDestroySchoolMutation +} = schoolApi; diff --git a/frontend/src/app/api/teacher/dashboard.ts b/frontend/src/app/api/teacher/dashboard.ts index 3757c07c..55d19ad1 100644 --- a/frontend/src/app/api/teacher/dashboard.ts +++ b/frontend/src/app/api/teacher/dashboard.ts @@ -113,7 +113,7 @@ const teacherDashboardApi = api.injectEndpoints({ }), invalidatesTags: ['teacher'] }), - updateSchool: build.mutation ({ teacherHas2fa: build.query<{ has2fa: boolean }, null>({ @@ -32,9 +28,6 @@ const teachApi = api.injectEndpoints({ }), getClass: build.query< { - // blockly_episodes - // python_episodes - // locked_levels class: { name: string; classmateProgress: boolean; @@ -53,10 +46,11 @@ const teachApi = api.injectEndpoints({ { type: 'class', id: accessCode } ] }), - getStudentsByAccessCode: build.query< - getStudentsByAccessCodeProps, - { accessCode: string } - >({ + getStudentsByAccessCode: build.query<{ + studentsPerAccessCode: studentPerAccessCode[]; + }, { + accessCode: string + }>({ query: ({ accessCode }) => ({ url: `class/students/${accessCode}/`, method: 'GET' @@ -65,18 +59,25 @@ const teachApi = api.injectEndpoints({ { type: 'student', id: accessCode } ] }), - updateClass: build.mutation< - null, - { - accessCode: string; - classEditSubmit?: boolean; - levelControlSubmit?: boolean; - classMoveSubmit?: boolean; - name: string; - classmateProgress: boolean; - externalRequests: string; - } - >({ + getStudent: build.query<{ + student: studentPerAccessCode; + }, { + studentId: string + }>({ + query: ({ studentId }) => ({ + url: `class/student/${studentId}/`, + method: 'GET' + }) + }), + updateClass: build.mutation({ query: ({ accessCode, ...body }) => ({ url: `teach/class/edit/${accessCode}`, method: 'POST', @@ -89,21 +90,53 @@ const teachApi = api.injectEndpoints({ { type: 'class', id: accessCode } ] }), - deleteClass: build.mutation< - null, - { accessCode: string; } - >({ - query: ({ accessCode, ...body }) => ({ + editStudentPassword: build.mutation<{ + accessCode: string; + }, { + studentId: string; + password: string; + confirmPassword: string; + }>({ + query: ({ studentId, password, confirmPassword }) => ({ + url: `teach/student/edit/${studentId}`, + method: 'POST', + body: { + password, + confirmPassword + }, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + }), + editStudentName: build.mutation({ + query: ({ studentId, name }) => ({ + url: `teach/student/edit/${studentId}`, + method: 'POST', + body: { + name + }, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + }), + deleteClass: build.mutation({ + query: ({ accessCode }) => ({ url: `teach/class/delete/${accessCode}`, method: 'POST', - body, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }), invalidatesTags: ['teacher'] }), - moveClass: build.mutation({ @@ -116,6 +149,21 @@ const teachApi = api.injectEndpoints({ } }), invalidatesTags: ['teacher'] + }), + getReminderCards: build.mutation({ + query: ({ accessCode, data }) => ({ + url: `teach/onboarding-class/${accessCode}/print-reminder-cards/`, + method: 'POST', + body: data, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/pdf' + }, + responseType: 'blob' + }) }) }) }); @@ -123,10 +171,14 @@ const teachApi = api.injectEndpoints({ export default teachApi; export const { useGetClassQuery, - useGetStudentsByAccessCodeQuery, useUpdateClassMutation, + useEditStudentNameMutation, + useEditStudentPasswordMutation, + useGetReminderCardsMutation, + useGetStudentsByAccessCodeQuery, + useGetStudentQuery, + useTeacherHas2faQuery, useDeleteClassMutation, useMoveClassMutation, - useTeacherHas2faQuery, useDisable2faMutation } = teachApi; diff --git a/frontend/src/app/api/user.ts b/frontend/src/app/api/user.ts new file mode 100644 index 00000000..1c4a5a14 --- /dev/null +++ b/frontend/src/app/api/user.ts @@ -0,0 +1,203 @@ +import { + BulkCreateArg, + BulkCreateResult, + BulkDestroyArg, + BulkDestroyResult, + BulkUpdateArg, + BulkUpdateResult, + CreateArg, + CreateResult, + DestroyArg, + DestroyResult, + ListArg, + ListResult, + Model, + RetrieveArg, + RetrieveResult, + UpdateArg, + UpdateResult, + searchParamsToString, + tagData +} from 'codeforlife/lib/esm/helpers/rtkQuery'; + +import api from './api'; + +export type User = Model< + number, + { + firstName: string; + lastName: string; + email: string; + password: null | string; + student: null | { + classField: null | number; + loginId: null | string; + user: number; + newUser: null | number; + pendingClassRequest: null | number; + blockedTime: null | Date; + }; + teacher: null | { + user: number; + newUser: null | number; + school: null | number; + isAdmin: boolean; + blockedTime: null | Date; + invitedBy: null | number; + }; + }, + { + username: string; + isActive: boolean; + isStaff: boolean; + dateJoined: Date; + lastLogin: null | Date; + } +>; + +const baseUrl = 'users'; + +const userApi = api.injectEndpoints({ + endpoints: (build) => ({ + createUser: build.query< + CreateResult, + CreateArg + >({ + query: (body) => ({ + url: `${baseUrl}/`, + method: 'POST', + body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }), + providesTags: (result, error, data) => result && !error + ? [ + 'private', + { type: 'user', id: result.id } + ] + : [] + }), + bulkCreateUsers: build.query< + BulkCreateResult, + BulkCreateArg + >({ + query: (body) => ({ + url: `${baseUrl}/`, + method: 'POST', + body, + headers: { + 'Content-Type': 'application/json' + } + }), + providesTags: (result, error, arg) => result && !error + ? [ + 'private', + ...tagData(result, 'user') + ] + : [] + }), + retrieveUser: build.query< + RetrieveResult, + RetrieveArg + >({ + query: ({ id }) => ({ + url: `${baseUrl}/${id}/`, + method: 'GET' + }), + providesTags: (result, error, { id }) => result && !error + ? [ + 'private', + { type: 'user', id } + ] + : [] + }), + listUsers: build.query< + ListResult, + ListArg<{ studentClass: string; }> + >({ + query: (arg) => ({ + url: `${baseUrl}/${searchParamsToString(arg)}`, + method: 'GET' + }), + providesTags: (result, error, arg) => result && !error + ? [ + 'private', + ...tagData(result, 'user') + ] + : [] + }), + updateUser: build.mutation< + UpdateResult, + UpdateArg & { currentPassword?: string; } + >({ + query: ({ id, ...body }) => ({ + url: `${baseUrl}/${id}/`, + method: 'PATCH', + body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }), + invalidatesTags: (result, error, { id }) => !error + ? [{ type: 'user', id }] + : [] + }), + bulkUpdateUsers: build.mutation< + BulkUpdateResult, + BulkUpdateArg + >({ + query: (body) => ({ + url: `${baseUrl}/`, + method: 'PATCH', + body, + headers: { + 'Content-Type': 'application/json' + } + }), + invalidatesTags: (result, error, arg) => result && !error + ? tagData(result, 'user') + : [] + }), + destroyUser: build.mutation< + DestroyResult, + DestroyArg + >({ + query: ({ id }) => ({ + url: `${baseUrl}/${id}/`, + method: 'DELETE' + }), + invalidatesTags: (result, error, { id }) => !error + ? [{ type: 'user', id }] + : [] + }), + bulkDestroyUsers: build.mutation< + BulkDestroyResult, + BulkDestroyArg + >({ + query: (body) => ({ + url: `${baseUrl}/`, + method: 'DELETE', + body, + headers: { + 'Content-Type': 'application/json' + } + }), + invalidatesTags: (result, error, arg) => result && !error + ? tagData(result, 'user') + : [] + }) + }) +}); + +export default userApi; +export const { + useLazyCreateUserQuery, + useLazyBulkCreateUsersQuery, + useRetrieveUserQuery, + useListUsersQuery, + useUpdateUserMutation, + useBulkUpdateUsersMutation, + useDestroyUserMutation, + useBulkDestroyUsersMutation +} = userApi; diff --git a/frontend/src/app/router/paths.ts b/frontend/src/app/router/paths.ts index cb5983ac..b5da1b1f 100644 --- a/frontend/src/app/router/paths.ts +++ b/frontend/src/app/router/paths.ts @@ -25,6 +25,7 @@ const paths = _('', { classes: _('/classes', { editClass: _('/:accessCode', { additional: _('/additional'), + studentCredentials: _('/student-credentials'), editStudent: _('/edit/?studentIds={studentIds}'), resetStudents: _('/reset/?studentIds={studentIds}'), moveStudents: _('/move/?studentIds={studentIds}'), diff --git a/frontend/src/app/router/routes/teacher.tsx b/frontend/src/app/router/routes/teacher.tsx index e33811b1..91a88008 100644 --- a/frontend/src/app/router/routes/teacher.tsx +++ b/frontend/src/app/router/routes/teacher.tsx @@ -1,11 +1,12 @@ -import React from 'react'; import { Route } from 'react-router-dom'; -import paths from '../paths'; + import Teacher from '../../../pages/teacher/Teacher'; -import TeacherOnboarding from '../../../pages/teacherOnboarding/TeacherOnboarding'; import TeacherDashboard from '../../../pages/teacherDashboard/TeacherDashboard'; import AddExternalStudent from '../../../pages/teacherDashboard/classes/AddExternalStudent'; import AddedExternalStudent from '../../../pages/teacherDashboard/classes/AddedExternalStudent'; +import StudentCredentials from '../../../pages/teacherDashboard/classes/editClass/student/editStudent/StudentCredentials'; +import TeacherOnboarding from '../../../pages/teacherOnboarding/TeacherOnboarding'; +import paths from '../paths'; const teacher = <> path={`${paths.teacher.dashboard.classes._}/:accessCode?/:view?`} element={} /> + } + /> } diff --git a/frontend/src/app/schemas.ts b/frontend/src/app/schemas.ts index 4daa47f4..16dae7d3 100644 --- a/frontend/src/app/schemas.ts +++ b/frontend/src/app/schemas.ts @@ -1,4 +1,5 @@ import * as yup from 'yup'; -export const accessCodeSchema = yup.string() +export const accessCodeSchema = yup + .string() .matches(/^[A-Z0-9]{5}$/, 'Invalid access code'); diff --git a/frontend/src/components/TableOfContents.tsx b/frontend/src/components/TableOfContents.tsx index ba4ca599..34b13807 100644 --- a/frontend/src/components/TableOfContents.tsx +++ b/frontend/src/components/TableOfContents.tsx @@ -4,12 +4,11 @@ import { Typography, Stack, Divider, - Link, - Box + Link } from '@mui/material'; export interface TableOfContentsProps { - contents: Array<{ header: string, element: React.ReactElement }>; + contents: Array<{ header: string, element: React.ReactElement; }>; } export const ids = { diff --git a/frontend/src/components/form/ClassNameField.tsx b/frontend/src/components/form/ClassNameField.tsx index 1ea2b24f..37c776fd 100644 --- a/frontend/src/components/form/ClassNameField.tsx +++ b/frontend/src/components/form/ClassNameField.tsx @@ -15,7 +15,7 @@ export interface ClassNameFieldProps { } const ClassNameField: React.FC = ({ - name = 'class' + name = 'name' }) => { return ( { return ( & { @@ -9,7 +9,7 @@ const StudentNameField: React.FC & { disabled?: boolean; style?: CSSProperties; }> = ({ - name = 'name', + name = 'firstName', helperText = 'Choose a name', disabled = false, style diff --git a/frontend/src/features/addStudentsForm/AddStudentsForm.tsx b/frontend/src/features/addStudentsForm/AddStudentsForm.tsx index 75f1e243..584db24d 100644 --- a/frontend/src/features/addStudentsForm/AddStudentsForm.tsx +++ b/frontend/src/features/addStudentsForm/AddStudentsForm.tsx @@ -1,88 +1,91 @@ -import React from 'react'; import { - Typography, + Add as AddIcon, + Upload as UploadIcon +} from '@mui/icons-material'; +import { Button, - FormHelperText + FormHelperText, + Typography } from '@mui/material'; -import { - Upload as UploadIcon, - Add as AddIcon -} from '@mui/icons-material'; +import React from 'react'; import * as Yup from 'yup'; import { Form, - TextField, - SubmitButton + SubmitButton, + TextField } from 'codeforlife/lib/esm/components/form'; +import { setFormErrors } from 'codeforlife/lib/esm/helpers/formik'; +import { + BulkCreateResult +} from 'codeforlife/lib/esm/helpers/rtkQuery'; + +import { useLazyBulkCreateUsersQuery, User } from '../../app/api'; -const AddStudentsForm: React.FC<{ - onSubmit: () => void -}> = ({ +export interface AddStudentsFormProps { + onSubmit: (users: BulkCreateResult) => void; +} + +const AddStudentsForm: React.FC = ({ onSubmit }) => { - interface Values { - students: string[] - } - - const initialValues: Values = { - students: [] - }; + const [bulkCreateUsers] = useLazyBulkCreateUsersQuery(); - return <> - - Add the student names to the box with one name per line or separated by a comma. - - - Student names and the class access code are required to sign in. - - {/* TODO: call API */} - - - Please note: if using the import option, student names must be under a heading labelled 'name'. - -
{ - // TODO: call backend - console.log(values); - setSubmitting(false); - onSubmit(); - }} - stackProps={{ - gap: 3, - alignItems: 'end', - direction: { xs: 'column', md: 'row' }, - width: { xs: '100%', md: '75%' } - }} - > - - }> - Add students - - - ; - }; + return <> + + Add the student names to the box with one name per line or separated by a comma. + + + Student names and the class access code are required to sign in. + + {/* TODO: call API */} + + + Please note: if using the import option, student names must be under a heading labelled 'name'. + +
{ + // TODO: convert students to data. + bulkCreateUsers({ data: [] }) + .unwrap() + .then(onSubmit) + .catch((error) => { setFormErrors(error, setErrors); }); + }} + stackProps={{ + gap: 3, + alignItems: 'end', + direction: { xs: 'column', md: 'row' }, + width: { xs: '100%', md: '75%' } + }} + > + + }> + Add students + + + ; +}; export default AddStudentsForm; diff --git a/frontend/src/features/newStudentsTable/NewStudentsTable.tsx b/frontend/src/features/newStudentsTable/NewStudentsTable.tsx index 7e7b3eda..221230bf 100644 --- a/frontend/src/features/newStudentsTable/NewStudentsTable.tsx +++ b/frontend/src/features/newStudentsTable/NewStudentsTable.tsx @@ -1,26 +1,128 @@ -import React from 'react'; import { - Stack, - Typography, + Print as PrintIcon, + SaveAlt as SaveAltIcon +} from '@mui/icons-material'; +import { + Box, Button, + Stack, Table, - TableHead, TableBody, - TableRow, - TableRowProps, TableCell, TableCellProps, + TableHead, + TableRow, + TableRowProps, + Typography, typographyClasses, - useTheme, - Box + useTheme } from '@mui/material'; -import { - Print as PrintIcon, - SaveAlt as SaveAltIcon -} from '@mui/icons-material'; +import { pdf } from '@react-pdf/renderer'; +import React from 'react'; +import { generatePath, useLocation } from 'react-router-dom'; +import { BulkCreateResult } from 'codeforlife/lib/esm/helpers/rtkQuery'; import { primary } from 'codeforlife/lib/esm/theme/colors'; + +import { User } from '../../app/api'; +import { paths } from '../../app/router'; import CopyToClipboardIcon from '../../components/CopyToClipboardIcon'; +import MyDocument from '../../pages/login/MyDocument'; + +interface StudentInfo { + name: string; + password: string; + classLink: string; + loginUrl: string; +} + +const DownloadButtonCSV: React.FC = () => { + const generateCSV: ( + studentsInfo: StudentInfo[], + classLink: string + ) => string = (studentsInfo, classLink) => { + let csvContent = 'Name,Password,Class Link,Login URL\n'; + studentsInfo.forEach((student) => { + csvContent += `${student.name},${student.password},${classLink},${student.loginUrl}\n`; + }); + return csvContent; + }; + const location = useLocation(); + const { studentsInfo } = location.state.updatedStudentCredentials; + const { classLink } = location.state.updatedStudentCredentials; + + const downloadCSV: () => void = () => { + const csvContent = generateCSV(studentsInfo, classLink); + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const linkRef = React.useRef(null); + if (linkRef.current) { + linkRef.current.href = url; + linkRef.current.download = 'data.csv'; + linkRef.current.click(); + } + URL.revokeObjectURL(url); + }; + + return ( + + ); +}; + +interface DownloadButtonPDFProps { + isButtonBanner?: boolean; +} + +export const DownloadButtonPDF: React.FC = ({ isButtonBanner }) => { + const location = useLocation(); + const { studentsInfo, classLink } = location.state.updatedStudentCredentials; + const linkRef = React.useRef(null); + + const downloadPdf = async (): Promise => { + try { + const blob = await pdf( + + ).toBlob(); + const url = URL.createObjectURL(blob); + + if (linkRef.current) { + linkRef.current.href = url; + linkRef.current.download = 'document.pdf'; + linkRef.current.click(); + URL.revokeObjectURL(url); + } + } catch (error) { + console.error(error); + } + }; + const buttonStyles = !isButtonBanner + ? {} + : { + sx: { + border: '2px solid black', + '&:hover': { + border: '2px solid black' + } + } + }; + + return ( + <> + + {/* Invisible anchor tag to trigger the download */} + + + ); +}; const WhiteTableCell: React.FC = ({ style, @@ -68,22 +170,21 @@ const BodyRowTableCell: React.FC = (props) => ( ); export interface NewStudentsTableProps { - classLink: string; - students: Array<{ - name: string; - password: string; - link: string; - }>; + accessCode: string; + users: BulkCreateResult; } const NewStudentsTable: React.FC = ({ - classLink, - students + accessCode, + users }) => { + const theme = useTheme(); + + const classLink = generatePath(paths.login.student.class._, { accessCode }); + const nameCellWidth = '40%'; const passwordCellWidth = '60%'; - const theme = useTheme(); return ( @@ -131,7 +232,9 @@ const NewStudentsTable: React.FC = ({ Class link: - {classLink} + + {classLink} + = ({ - {students.map((student) => ( - - - - {student.name} - - - {student.password} - - - - - - - - {student.link} - - - - - - - ))} + {users.map((user) => { + if (!user.student?.loginId) throw new Error(); + + const link = classLink + + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `?userId=${user.id}&loginId=${user.student.loginId}`; + + return ( + + + + {user.firstName} + + + {user.password} + + + + + + + + {link} + + + + + + + ); + })} {/* TODO: fix margin bottom */} - - + + ); diff --git a/frontend/src/pages/login/MyDocument.tsx b/frontend/src/pages/login/MyDocument.tsx new file mode 100644 index 00000000..204c5587 --- /dev/null +++ b/frontend/src/pages/login/MyDocument.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Page, Text, View, Document, StyleSheet, Image } from '@react-pdf/renderer'; +import CflLogo from '../../images/cfl_logo.png'; + +const styles = StyleSheet.create({ + mainView: { + border: '2px solid black', + display: 'flex', + flexDirection: 'row', + gap: 5, + padding: 10 + }, + page: { + padding: 20 + }, + text: { + marginBottom: 5, + fontSize: 12 + }, + image: { + width: 100, + height: 100 + } +}); + +interface StudentInfo { + name: string, + password: string; + classLink: string; + loginUrl: string; +} +const MyDocument: React.FC<{ + studentsInfo: StudentInfo[]; + classLink: string; +}> = ({ studentsInfo, classLink }) => ( + + + {studentsInfo.map((student: StudentInfo) => + + + + + Please ensure students keep login details in a secure place + Directly login with {student.loginUrl} + OR class link: {classLink} + Name: {student.name} + Password: {student.password} + + + )} + + +); +export default MyDocument; diff --git a/frontend/src/pages/login/studentForms/AccessCode.tsx b/frontend/src/pages/login/studentForms/AccessCode.tsx index 1a4bede7..0e3b637c 100644 --- a/frontend/src/pages/login/studentForms/AccessCode.tsx +++ b/frontend/src/pages/login/studentForms/AccessCode.tsx @@ -21,7 +21,7 @@ const AccessCode: React.FC = () => { const searchParams = tryValidateSync( fromSearchParams(), Yup.object({ - userId: Yup.string().required(), + userId: Yup.number().required(), loginId: Yup.string().required() }) ); diff --git a/frontend/src/pages/privacyNotice/ForAdults/HowWeUseInfo.tsx b/frontend/src/pages/privacyNotice/ForAdults/HowWeUseInfo.tsx index 711211d7..22e49f34 100644 --- a/frontend/src/pages/privacyNotice/ForAdults/HowWeUseInfo.tsx +++ b/frontend/src/pages/privacyNotice/ForAdults/HowWeUseInfo.tsx @@ -6,7 +6,6 @@ import { TableBody, TableRow, TableCell, - tableCellClasses, ListItemText } from '@mui/material'; diff --git a/frontend/src/pages/teacherDashboard/YourSchool.tsx b/frontend/src/pages/teacherDashboard/YourSchool.tsx index 281b1c19..0f67f531 100644 --- a/frontend/src/pages/teacherDashboard/YourSchool.tsx +++ b/frontend/src/pages/teacherDashboard/YourSchool.tsx @@ -1,13 +1,3 @@ -import { - Grid, - Typography, - Button, - useTheme, - InputAdornment, - Stack, - Dialog -} from '@mui/material'; -import React from 'react'; import { Add, Create, @@ -17,35 +7,45 @@ import { EmailOutlined, PersonOutlined } from '@mui/icons-material'; -import { INVITE_TEACHER_SCHEMA, SCHOOL_DETAILS_UPDATE_SCHEMA } from './schemas'; -import { INVITE_TEACHER_INITIAL_VALUES } from './constants'; -import CflTable, { - CflTableBody, - CflTableCellElement -} from '../../components/CflTable'; import { - TextField, + Button, + Dialog, + Grid, + InputAdornment, + Stack, + Typography, + useTheme +} from '@mui/material'; +import { CheckboxField, - SubmitButton + SubmitButton, + TextField } from 'codeforlife/lib/esm/components/form'; -import { CflHorizontalForm } from '../../components/form/CflForm'; import Page from 'codeforlife/lib/esm/components/page'; -import SchoolNameField from '../../components/form/SchoolNameField'; -import SchoolPostcodeField from '../../components/form/SchoolPostcodeField'; -import SchoolCountryField from '../../components/form/SchoolCountryField'; +import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useLeaveOrganisationMutation } from '../../app/api'; -import { paths } from '../../app/router'; import { TeacherDashboardProps, useDeleteInviteMutation, useInviteTeacherMutation, useInviteToggleAdminMutation, + useOldUpdateSchoolMutation, useOrganisationKickMutation, useResendInviteMutation, - useToggleAdminMutation, - useUpdateSchoolMutation + useToggleAdminMutation } from '../../app/api/teacher/dashboard'; +import { paths } from '../../app/router'; +import CflTable, { + CflTableBody, + CflTableCellElement +} from '../../components/CflTable'; +import { CflHorizontalForm } from '../../components/form/CflForm'; +import SchoolCountryField from '../../components/form/SchoolCountryField'; +import SchoolNameField from '../../components/form/SchoolNameField'; +import SchoolPostcodeField from '../../components/form/SchoolPostcodeField'; +import { INVITE_TEACHER_INITIAL_VALUES } from './constants'; +import { INVITE_TEACHER_SCHEMA, SCHOOL_DETAILS_UPDATE_SCHEMA } from './schemas'; interface DialogProps { open: boolean; @@ -198,7 +198,7 @@ const UpdateSchoolDetailsForm: React.FC<{ const schoolName = schoolData.name; const schoolPostcode = schoolData.postcode; const schoolCountry = schoolData.country; - const [updateSchool] = useUpdateSchoolMutation(); + const [updateSchool] = useOldUpdateSchoolMutation(); return ( - @@ -313,7 +306,7 @@ const EditClass: React.FC<{ case 'edit': return ; case 'release': diff --git a/frontend/src/pages/teacherDashboard/classes/editClass/student/editStudent/EditStudent.tsx b/frontend/src/pages/teacherDashboard/classes/editClass/student/editStudent/EditStudent.tsx index da68d76f..069ff72a 100644 --- a/frontend/src/pages/teacherDashboard/classes/editClass/student/editStudent/EditStudent.tsx +++ b/frontend/src/pages/teacherDashboard/classes/editClass/student/editStudent/EditStudent.tsx @@ -1,100 +1,138 @@ -import { SubmitButton } from 'codeforlife/lib/esm/components/form'; -import Page from 'codeforlife/lib/esm/components/page'; +import { Link, Typography, useTheme } from '@mui/material'; import React from 'react'; -import { Link, Typography, useTheme } from '@mui/material'; +import { + Form, + SubmitButton +} from 'codeforlife/lib/esm/components/form'; +import Page from 'codeforlife/lib/esm/components/page'; +import { submitForm } from 'codeforlife/lib/esm/helpers/formik'; +import { useNavigate } from 'codeforlife/lib/esm/hooks'; -import { CflHorizontalForm } from '../../../../../../components/form/CflForm'; +import { + useRetrieveClassQuery, + useRetrieveUserQuery, + useUpdateUserMutation +} from '../../../../../../app/api'; +import { paths } from '../../../../../../app/router'; import StudentNameField from '../../../../../../components/form/StudentNameField'; import CflPasswordFields from '../../../../../../features/cflPasswordFields/CflPasswordFields'; +import { StudentCredentialsState } from './StudentCredentials'; -const UpdateNameForm: React.FC = () => { - interface Values { - name: string; - } - - // TODO: Initial value should be student name - const initialValues: Values = { - name: 'Florian' - }; - return ( - { - // TODO: Connect to backend - alert(JSON.stringify(values, null, 2)); - setSubmitting(false); - }} - // TODO: Disable button by default - submitButton={Update} - > - - - ); -}; - -const UpdatePasswordForm: React.FC = () => { - interface Values { - password: string; - confirmPassword: string; - } - - const initialValues: Values = { - password: '', - confirmPassword: '' - }; - return ( - { - // TODO: Connect to backend - alert(JSON.stringify(values, null, 2)); - setSubmitting(false); - }} - submitButton={Update} - > - - - ); -}; - -const EditStudent: React.FC<{ +interface EditStudentProps { + id: number; accessCode: string; - // TODO: Get actual ID from backend in previous page and use it to populate page data - studentId: number; goBack: () => void; -}> = ({ accessCode, studentId, goBack }) => { +} + +const EditStudent: React.FC = ({ + id, + accessCode, + goBack +}) => { const theme = useTheme(); - return ( - <> - - - Edit student details for Florian from class Class 1 ({accessCode}) - - - Class - - - Edit this student's name and manage their password and direct - access link. - - - - - - - - - - ); + const navigate = useNavigate(); + const [updateUser] = useUpdateUserMutation(); + const user = useRetrieveUserQuery({ id }); + const klass = useRetrieveClassQuery({ accessCode }); + + return <>{user.data !== undefined && klass.data !== undefined && <> + + + Edit student details + for {user.data.firstName} + from class {klass.data.name} ({klass.data.accessCode}) + + + Class + + + Edit this student's name and manage their password and direct + access link. + + + + {/* TODO: create global fix for margin bottom */} + + Update name + + + Remember this is the name they use to log in with, so you should tell + them what you've changed it to. + +
{ + navigate(paths.teacher.dashboard.classes._, { + state: { + notifications: [ + { + index: 1, + props: { + children: 'Student\'s details successfully updated.' + } + } + ] + } + }); + } + })} + > + + + Update + + +
+ + {/* TODO: create global fix for margin bottom */} + + Update password + + + You can set this student's password. Setting the password will also + regenerate their direct access link. Enter and confirm the password in + the boxes below. Try to prevent others from being able to guess the new + password when making this decision. + +
{ + navigate( + paths.teacher.dashboard.classes.editClass.studentCredentials._, + { + state: { + notifications: [ + { + index: 1, + props: { + children: 'Student\'s details successfully updated.' + } + } + ], + users: [user] + } + } + ); + } + })} + > + + + Update + + +
+ }; }; export default EditStudent; diff --git a/frontend/src/pages/teacherDashboard/classes/editClass/student/editStudent/StudentCredentials.tsx b/frontend/src/pages/teacherDashboard/classes/editClass/student/editStudent/StudentCredentials.tsx new file mode 100644 index 00000000..81bd8c14 --- /dev/null +++ b/frontend/src/pages/teacherDashboard/classes/editClass/student/editStudent/StudentCredentials.tsx @@ -0,0 +1,89 @@ +import { Stack, Typography } from '@mui/material'; +import { useLocation, useParams } from 'react-router-dom'; +import * as Yup from 'yup'; + +import Page from 'codeforlife/lib/esm/components/page'; +import { tryValidateSync } from 'codeforlife/lib/esm/helpers/yup'; +import { useNavigate } from 'codeforlife/lib/esm/hooks'; + +import { useRetrieveClassQuery } from '../../../../../../app/api'; +import paths from '../../../../../../app/router/paths'; +import { accessCodeSchema } from '../../../../../../app/schemas'; +import NewStudentsTable, { + DownloadButtonPDF, + NewStudentsTableProps +} from '../../../../../../features/newStudentsTable/NewStudentsTable'; + +export interface StudentCredentialsState { + users: NewStudentsTableProps['users']; +} + +const StudentCredentials: React.FC = () => { + const navigate = useNavigate(); + const { state }: { state: StudentCredentialsState } = useLocation(); + + const accessCode = tryValidateSync( + useParams(), + Yup.object({ accessCode: accessCodeSchema.required() }) + )?.accessCode; + + if (accessCode === undefined || + typeof state !== 'object' || + state === null || + !('users' in state) || + !Array.isArray(state.users) || + state.users.length === 0 + ) { + navigate(paths.teacher.dashboard.classes._, { + replace: true, + state: { + notifications: [ + { + index: 1, + props: { + error: true, + children: accessCode === undefined + ? 'Invalid class access code.' + : 'Missing student details.' + } + } + ] + } + }); + return <>; + } + + const klass = useRetrieveClassQuery({ accessCode }); + + return ( + + {klass.data !== undefined && <> + + + + + This is the only time you will be able to view this page. You can + print reminder cards or download as a CSV file. + + + + + + + + } + + ); +}; + +export default StudentCredentials; diff --git a/frontend/src/pages/teacherDashboard/classes/editClass/student/resetStudent/ResetStudent.tsx b/frontend/src/pages/teacherDashboard/classes/editClass/student/resetStudent/ResetStudent.tsx index f492820a..755f3af7 100644 --- a/frontend/src/pages/teacherDashboard/classes/editClass/student/resetStudent/ResetStudent.tsx +++ b/frontend/src/pages/teacherDashboard/classes/editClass/student/resetStudent/ResetStudent.tsx @@ -1,35 +1,26 @@ -import React from 'react'; -import Page from 'codeforlife/lib/esm/components/page'; +import { ChevronLeft } from '@mui/icons-material'; import { Button, Stack, Typography, useTheme } from '@mui/material'; +import Page from 'codeforlife/lib/esm/components/page'; +import React from 'react'; import NewStudentsTable from '../../../../../../features/newStudentsTable/NewStudentsTable'; -import { ChevronLeft } from '@mui/icons-material'; const ResetStudent: React.FC<{ accessCode: string; studentIds: number[]; goBack: () => void; }> = ({ accessCode, studentIds, goBack }) => { - // TODO: get from API and use params - const classLink = 'https://www.codeforlife.education/'; - const students: Array<{ - name: string; - password: string; - link: string; - }> = [ - { - name: 'John', - password: 'ioykms', - link: 'https://www.codeforlife.education/' - } - ]; const theme = useTheme(); + return ( <> Students' passwords reset for class Class 1 ({accessCode}) - +